From 937f26bdaaca64849570c472914e8d8a2e7f1fbf Mon Sep 17 00:00:00 2001 From: opi Date: Thu, 21 Jul 2016 11:43:02 +0200 Subject: [PATCH 0001/1066] [fix] Raise error on malformed SSOwat persistent conf. --- locales/en.json | 1 + src/yunohost/app.py | 3 +++ src/yunohost/tools.py | 3 +++ src/yunohost/user.py | 15 +++++++++------ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index e939b26fa..a91b0c525 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,6 +209,7 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", + "ssowat_persistent_conf_error": "Erro while reading SSOwat persistent configuration", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index af61ff63b..27046fd01 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -806,6 +806,9 @@ def app_makedefault(auth, app, domain=None): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_error')) except IOError: ssowat_conf = {} diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f78e32363..1898e7023 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -226,6 +226,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_error')) except IOError: ssowat_conf = {} diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ec7dd539c..e04ba9883 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -177,14 +177,17 @@ def user_create(auth, username, firstname, lastname, mail, password, try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) + except ValueError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_error')) + except IOError: + ssowat_conf = {} - if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: - del ssowat_conf['redirected_urls']['/'] + if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: + del ssowat_conf['redirected_urls']['/'] - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - - except IOError: pass + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) if auth.add(rdn, attr_dict): From d95052a9e9a1b26115339bb2ed22e1ed01f7c6ed Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 28 Aug 2016 15:39:45 +0200 Subject: [PATCH 0002/1066] [enh] Catch SSOwat persistent configuration write error. --- locales/en.json | 3 ++- src/yunohost/app.py | 2 +- src/yunohost/tools.py | 2 +- src/yunohost/user.py | 10 +++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index a91b0c525..3a3c8b0b2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,7 +209,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_error": "Erro while reading SSOwat persistent configuration", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 27046fd01..4f4a678d1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -808,7 +808,7 @@ def app_makedefault(auth, app, domain=None): ssowat_conf = json.loads(str(json_conf.read())) except ValueError: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_error')) + m18n.n('ssowat_persistent_conf_read_error')) except IOError: ssowat_conf = {} diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 1898e7023..7af818a30 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -228,7 +228,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ssowat_conf = json.loads(str(json_conf.read())) except ValueError: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_error')) + m18n.n('ssowat_persistent_conf_read_error')) except IOError: ssowat_conf = {} diff --git a/src/yunohost/user.py b/src/yunohost/user.py index e04ba9883..4cef87429 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -179,15 +179,19 @@ def user_create(auth, username, firstname, lastname, mail, password, ssowat_conf = json.loads(str(json_conf.read())) except ValueError: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_error')) + m18n.n('ssowat_persistent_conf_read_error')) except IOError: ssowat_conf = {} if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_write_error')) if auth.add(rdn, attr_dict): From 6149e6c6deba4664cc2e3fa4008efd1b0d823627 Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 28 Aug 2016 15:42:47 +0200 Subject: [PATCH 0003/1066] [fix] Write SSOwat configuration file only if needed. --- src/yunohost/user.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 4cef87429..51c87f59b 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -185,13 +185,12 @@ def user_create(auth, username, firstname, lastname, mail, password, if 'redirected_urls' in ssowat_conf and '/' in ssowat_conf['redirected_urls']: del ssowat_conf['redirected_urls']['/'] - - try: - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) - except IOError: - raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_write_error')) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ssowat_persistent_conf_write_error')) if auth.add(rdn, attr_dict): From 81e464ce1f4031475ed7051712b75a75745629e7 Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 28 Aug 2016 23:36:45 +0200 Subject: [PATCH 0004/1066] [enh] Display full exception error message. --- locales/en.json | 4 ++-- src/yunohost/app.py | 13 +++++++++---- src/yunohost/tools.py | 13 +++++++++---- src/yunohost/user.py | 10 +++++----- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3a3c8b0b2..dc801a908 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,8 +209,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration", - "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4f4a678d1..b8290f1bb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -806,9 +806,9 @@ def app_makedefault(auth, app, domain=None): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) - except ValueError: + except ValueError as e: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error')) + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} @@ -817,8 +817,13 @@ def app_makedefault(auth, app, domain=None): ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + os.system('chmod 644 /etc/ssowat/conf.json.persistent') diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 7af818a30..906b34e65 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -226,9 +226,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False): try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) - except ValueError: + except ValueError as e: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error')) + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} @@ -237,8 +237,13 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' - with open('/etc/ssowat/conf.json.persistent', 'w+') as f: - json.dump(ssowat_conf, f, sort_keys=True, indent=4) + try: + with open('/etc/ssowat/conf.json.persistent', 'w+') as f: + json.dump(ssowat_conf, f, sort_keys=True, indent=4) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + os.system('chmod 644 /etc/ssowat/conf.json.persistent') diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 51c87f59b..677ba05d4 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -177,9 +177,9 @@ def user_create(auth, username, firstname, lastname, mail, password, try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) - except ValueError: + except ValueError as e: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error')) + m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) except IOError: ssowat_conf = {} @@ -188,9 +188,9 @@ def user_create(auth, username, firstname, lastname, mail, password, try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) - except IOError: - raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_write_error')) + except IOError as e: + raise MoulinetteError(errno.EPERM, + m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) if auth.add(rdn, attr_dict): From 9a66a00278f10a7a4f167147f78a17c6b0f82d50 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 8 Aug 2016 07:46:38 +0200 Subject: [PATCH 0005/1066] [fix] wait for admin user to be available after a slapd regen-conf, this fix install on slow hardware/vps --- data/hooks/conf_regen/06-slapd | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index b3353962e..aef47c347 100755 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -102,6 +102,23 @@ do_post_regen() { fi sudo service slapd force-reload + + # on slow hardware/vm this regen conf would exit before the admin user that + # is stored in ldap is available because ldap seems to slow to restart + # so we'll wait either until we are able to log as admin or until a timeout + # is reached + # we need to do this because the next hooks executed after this one during + # postinstall requires to run as admin thus breaking postinstall on slow + # hardware which mean yunohost can't be correctly installed on those hardware + # and this sucks + # wait a maximum time of 5 minutes + # yes, force-reload behave like a restart + number_of_wait=0 + while ! sudo su admin -c '' && ((number_of_wait < 60)) + do + sleep 5 + ((number_of_wait += 1)) + done } FORCE=${2:-0} From 9225d12da88a1d3eb46c49d0f47d2c06621bfd70 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Wed, 7 Sep 2016 17:57:04 +0200 Subject: [PATCH 0006/1066] [enh] Remove useless comments autogenerated in the past --- data/templates/dovecot/dovecot.conf | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index 3daa670bf..244b78780 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -1,5 +1,3 @@ -# 2.1.7: /etc/dovecot/dovecot.conf -# OS: Linux 3.2.0-3-686-pae i686 Debian wheezy/sid ext4 listen = *, :: auth_mechanisms = plain login login_greeting = Dovecot ready!! From e3f4686c90f83dcbd985145e8aab25f0f89e17a6 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Wed, 7 Sep 2016 18:04:29 +0200 Subject: [PATCH 0007/1066] [enh] Hide the fact that we are speaking to dovecot --- data/templates/dovecot/dovecot.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index 244b78780..00916d88d 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -1,6 +1,5 @@ listen = *, :: auth_mechanisms = plain login -login_greeting = Dovecot ready!! mail_gid = 8 mail_home = /var/mail/%n mail_location = maildir:/var/mail/%n From e4832234185d45322318041dde99131b9b30c2f5 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Wed, 7 Sep 2016 18:20:21 +0200 Subject: [PATCH 0008/1066] [enh] reorder dovecot main configuration so that it is easier to read and extend --- data/templates/dovecot/dovecot.conf | 64 ++++++++++++++++------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index 00916d88d..af745c93d 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -1,15 +1,44 @@ listen = *, :: auth_mechanisms = plain login + mail_gid = 8 mail_home = /var/mail/%n mail_location = maildir:/var/mail/%n mail_uid = 500 + +protocols = imap sieve + +mail_plugins = $mail_plugins quota + +ssl = yes +ssl_cert = Date: Wed, 7 Sep 2016 18:37:53 +0200 Subject: [PATCH 0009/1066] [enh] Remove autocreate plugin related settings, unused now autosubscribe & autosubscribe2 are part of the autocreate plugin configuration. Should have been removed with f36e4e2a837ae1947aa6f196622f5e3bed637a9 See https://github.com/YunoHost/yunohost-config-dovecot/pull/3 --- data/templates/dovecot/dovecot.conf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index af745c93d..fbaa61ca9 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -77,11 +77,6 @@ plugin { antispam_pipe_program_notspam_arg = learn_ham } -plugin { - autosubscribe = Trash - autosubscribe2 = Junk -} - plugin { quota = maildir:User quota quota_rule2 = SPAM:ignore From ea17e9fa80094d1a1c47059cdf428f876bd749f9 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Fri, 9 Sep 2016 17:12:50 +0200 Subject: [PATCH 0010/1066] [enh] Allow for dovecot configuration extensions --- data/hooks/conf_regen/25-dovecot | 7 +++++++ data/templates/dovecot/dovecot.conf | 5 +++++ data/templates/dovecot/post-ext.conf | 1 + data/templates/dovecot/pre-ext.conf | 1 + 4 files changed, 14 insertions(+) create mode 100644 data/templates/dovecot/post-ext.conf create mode 100644 data/templates/dovecot/pre-ext.conf diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot index 5d82470a5..4c5ae24c1 100755 --- a/data/hooks/conf_regen/25-dovecot +++ b/data/hooks/conf_regen/25-dovecot @@ -26,11 +26,18 @@ do_pre_regen() { 's/^\(listen =\).*/\1 */' \ "${dovecot_dir}/dovecot.conf" fi + + mkdir -p "${dovecot_dir}/yunohost.d" + cp pre-ext.conf "${dovecot_dir}/yunohost.d" + cp post-ext.conf "${dovecot_dir}/yunohost.d" } do_post_regen() { regen_conf_files=$1 + sudo mkdir -p "/etc/dovecot/yunohost.d/pre-ext.d" + sudo mkdir -p "/etc/dovecot/yunohost.d/post-ext.d" + # create vmail user id vmail > /dev/null 2>&1 \ || sudo adduser --system --ingroup mail --uid 500 vmail diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index fbaa61ca9..5ea10ea79 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -1,3 +1,5 @@ +!include yunohost.d/pre-ext.conf + listen = *, :: auth_mechanisms = plain login @@ -10,6 +12,7 @@ protocols = imap sieve mail_plugins = $mail_plugins quota + ssl = yes ssl_cert = Date: Sun, 9 Oct 2016 21:34:03 +0200 Subject: [PATCH 0011/1066] [enh] no cli option to avoid removing an application on installation failure --- data/actionsmap/yunohost.yml | 3 +++ src/yunohost/app.py | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5a1465258..126993076 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -436,6 +436,9 @@ app: -a: full: --args help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + -n: + full: --no-remove-on-fail + help: Debug option to avoid removing the app on a filed installation ### app_remove() TODO: Write help remove: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index caa38e95b..5ac0cb971 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -440,7 +440,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) -def app_install(auth, app, label=None, args=None): +def app_install(auth, app, label=None, args=None, no_remove_on_fail=False): """ Install apps @@ -448,6 +448,7 @@ def app_install(auth, app, label=None, args=None): app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation + no_remove_on_fail -- Debug option to avoid removing the app on a filed installation """ from yunohost.hook import hook_add, hook_remove, hook_exec @@ -541,19 +542,20 @@ def app_install(auth, app, label=None, args=None): logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - # Setup environment for remove script - env_dict_remove = {} - env_dict_remove["YNH_APP_ID"] = app_id - env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + if not no_remove_on_fail: + # Setup environment for remove script + env_dict_remove = {} + env_dict_remove["YNH_APP_ID"] = app_id + env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name + env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) - # Execute remove script - remove_retcode = hook_exec( - os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove) - if remove_retcode != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) + # Execute remove script + remove_retcode = hook_exec( + os.path.join(extracted_app_folder, 'scripts/remove'), + args=[app_instance_name], env=env_dict_remove) + if remove_retcode != 0: + logger.warning(m18n.n('app_not_properly_removed', + app=app_instance_name)) # Clean tmp folders shutil.rmtree(app_setting_path) From 76a294b28438e12b2d29f381ad941dc2bacf3bc8 Mon Sep 17 00:00:00 2001 From: Moul Date: Wed, 12 Oct 2016 09:39:05 +0200 Subject: [PATCH 0012/1066] =?UTF-8?q?[fix]=20typo:=20install=20filed=20?= =?UTF-8?q?=E2=80=93>=20failed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/actionsmap/yunohost.yml | 2 +- src/yunohost/app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 126993076..56b28cf38 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -438,7 +438,7 @@ app: help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") -n: full: --no-remove-on-fail - help: Debug option to avoid removing the app on a filed installation + help: Debug option to avoid removing the app on a failed installation ### app_remove() TODO: Write help remove: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5ac0cb971..f85e9f191 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -448,7 +448,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_fail=False): app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation - no_remove_on_fail -- Debug option to avoid removing the app on a filed installation + no_remove_on_fail -- Debug option to avoid removing the app on a failed installation """ from yunohost.hook import hook_add, hook_remove, hook_exec From 35fa386ce3b9c52b469b1ae16400ab4c067e2bef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 28 Oct 2016 13:59:42 -0400 Subject: [PATCH 0013/1066] First draft of certificate management integration (e.g. Let's Encrypt certificate install) --- data/actionsmap/yunohost.yml | 58 +++- locales/de.json | 2 +- locales/en.json | 18 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/nl.json | 2 +- locales/pt.json | 2 +- src/yunohost/app.py | 16 + src/yunohost/certificate.py | 641 +++++++++++++++++++++++++++++++++++ src/yunohost/domain.py | 46 +-- 10 files changed, 748 insertions(+), 41 deletions(-) create mode 100644 src/yunohost/certificate.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5a1465258..4d0db97ec 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -305,8 +305,64 @@ domain: - !!str ^[0-9]+$ - "pattern_positive_number" + ### certificate_status() + cert-status: + action_help: List status of current certificates (all by default). + api: GET /certs/status/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains to check + nargs: "*" + --full: + help: Show more details + action: store_true - ### domain_info() + ### certificate_install() + cert-install: + action_help: Install Let's Encrypt certificates for given domains (all by default). + api: POST /certs/enable/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains for which to install the certificates + nargs: "*" + --force: + help: Install even if current certificate is not self-signed + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to install. (Not recommended) + action: store_true + --self-signed: + help: Install self-signed certificate instead of Let's Encrypt + action: store_true + + ### certificate_renew() + cert-renew: + action_help: Renew the Let's Encrypt certificates for given domains (all by default). + api: POST /certs/renew/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains for which to renew the certificates + nargs: "*" + --force: + help: Ignore the validity treshold (30 days) + action: store_true + --email: + help: Send an email to root with logs if some renewing fails + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) + action: store_true + + ### domain_info() # info: # action_help: Get domain informations # api: GET /domains/ diff --git a/locales/de.json b/locales/de.json index 1331c56b4..e57315caa 100644 --- a/locales/de.json +++ b/locales/de.json @@ -57,7 +57,7 @@ "custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben", "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus", - "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", + "certmanager_domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domain erfolgreich erzeugt", "domain_creation_failed": "Konnte Domain nicht erzeugen", "domain_deleted": "Domain erfolgreich gelöscht", diff --git a/locales/en.json b/locales/en.json index e939b26fa..35da2eef0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -71,7 +71,6 @@ "diagnostic_monitor_system_error": "Can't monitor system: {error}", "diagnostic_no_apps": "No installed application", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "Unable to generate certificate", "domain_created": "The domain has been created", "domain_creation_failed": "Unable to create domain", "domain_deleted": "The domain has been deleted", @@ -237,5 +236,20 @@ "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", + "certmanager_domain_cert_gen_failed": "Unable to generate certificate", + "certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_domain_cert_not_selfsigned" : "The certificate of domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", + "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", + "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}). Use --no-checks to disable checks.", + "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", + "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", + "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s})", + "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", + "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" } diff --git a/locales/es.json b/locales/es.json index 549cbe29a..fdd04d10f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -72,7 +72,7 @@ "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "No se pudo crear el certificado", + "certmanager_domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", "domain_deleted": "El dominio ha sido eliminado", diff --git a/locales/fr.json b/locales/fr.json index 7898de57f..6691b2f28 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -73,7 +73,7 @@ "diagnostic_monitor_system_error": "Impossible de superviser le système : {error}", "diagnostic_no_apps": "Aucune application installée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »", - "domain_cert_gen_failed": "Impossible de générer le certificat", + "certmanager_domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", "domain_creation_failed": "Impossible de créer le domaine", "domain_deleted": "Le domaine a été supprimé", diff --git a/locales/nl.json b/locales/nl.json index c2bfed31e..57b05e309 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -37,7 +37,7 @@ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken", "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst", "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "Kan certificaat niet genereren", + "certmanager_domain_cert_gen_failed": "Kan certificaat niet genereren", "domain_created": "Domein succesvol aangemaakt", "domain_creation_failed": "Kan domein niet aanmaken", "domain_deleted": "Domein succesvol verwijderd", diff --git a/locales/pt.json b/locales/pt.json index d3796d2e9..b9c9e4bce 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -36,7 +36,7 @@ "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}", "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", - "domain_cert_gen_failed": "Não foi possível gerar o certificado", + "certmanager_domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", "domain_creation_failed": "Não foi possível criar o domínio", "domain_deleted": "Domínio removido com êxito", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index caa38e95b..2326995ae 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1031,6 +1031,22 @@ def app_ssowatconf(auth): for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) + # Authorize ACME challenge url if a domain seems configured for it... + for domain in domains: + + # Check ACME challenge file is present in nginx conf + nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" + if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue + + # Check the file contains the ACME challenge uri + acme_uri = '/.well-known/acme-challenge' + if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue + + # If so, then authorize the ACME challenge uri to unprotected regex + regex = domain+"/%.well%-known/acme%-challenge/.*$" + unprotected_regex.append(regex) + + conf_dict = { 'portal_domain': main_domain, 'portal_path': '/yunohost/sso/', diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py new file mode 100644 index 000000000..4ed5cb588 --- /dev/null +++ b/src/yunohost/certificate.py @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_certificate.py + + Manage certificates, in particular Let's encrypt +""" + +import os +import sys +import errno +import requests +import shutil +import pwd +import grp +import json +import smtplib + +from OpenSSL import crypto +from datetime import datetime +from tabulate import tabulate +from acme_tiny import get_crt as sign_certificate + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +import yunohost.domain +from yunohost.service import _run_service_command +from yunohost.app import app_setting, app_ssowatconf + + +logger = getActionLogger('yunohost.certmanager') + +# Misc stuff we need + +cert_folder = "/etc/yunohost/certs/" +tmp_folder = "/tmp/acme-challenge-private/" +webroot_folder = "/tmp/acme-challenge-public/" + +selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" +account_key_file = "/etc/yunohost/letsencrypt_account.pem" + +key_size = 2048 + +validity_limit = 15 # days + +# For tests +#certification_authority = "https://acme-staging.api.letsencrypt.org" +# For prod +certification_authority = "https://acme-v01.api.letsencrypt.org" + +intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" + +############################################################################### +# Front-end stuff # +############################################################################### + +# Status + +def certificate_status(auth, domainList, full = False): + """ + Print the status of certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains to be checked + full -- Display more info about the certificates + """ + + # If no domains given, consider all yunohost domains + if (domainList == []) : domainList = yunohost.domain.domain_list(auth)['domains'] + # Else, validate that yunohost knows the domains given + else : + for domain in domainList : + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + # Get status for each domain, and prepare display using tabulate + if not full : + headers = [ "Domain", "Certificate status", "Authority type", "Days remaining"] + else : + headers = [ "Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + lines = [] + for domain in domainList : + status = _get_status(domain) + + line = [] + line.append(domain) + if (full) : line.append(status["subject"]) + line.append(_summary_code_to_string(status["summaryCode"])) + line.append(status["CAtype"]) + if (full) : line.append(status["CAname"]) + line.append(status["validity"]) + lines.append(line) + + print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) + + +def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False) : + """ + Install a Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains on which to install certificates + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server + before attempting the install + self-signed -- Instal self-signed certificates instead of Let's Encrypt + """ + if (self_signed) : + certificate_install_selfsigned(domainList, force) + else : + certificate_install_letsencrypt(auth, domainList, force, no_checks) + + +# Install self-signed + +def certificate_install_selfsigned(domainList, force=False) : + + for domain in domainList : + + # Check we ain't trying to overwrite a good cert ! + status = _get_status(domain) + if (status != {}) and (status["summaryCode"] > 0) and (not force) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + + + cert_folder_domain = cert_folder+"/"+domain + + # Create cert folder if it does not exists yet + try: + os.listdir(cert_folder_domain) + except OSError: + os.makedirs(cert_folder_domain) + + # Get serial + ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' + with open('%s/serial' % ssl_dir, 'r') as f: + serial = f.readline().rstrip() + + # FIXME : should refactor this to avoid so many os.system() calls... + # We should be able to do all this using OpenSSL.crypto and os/shutil + command_list = [ + 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' + % (cert_folder_domain, ssl_dir, ssl_dir), + 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' + % (cert_folder_domain, ssl_dir, ssl_dir), + 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, + 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), + 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), + 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) + ] + + for command in command_list: + if os.system(command) != 0: + raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + + _set_permissions(cert_folder_domain, "root", "root", 0755); + _set_permissions(cert_folder_domain+"/key.pem", "root", "metronome", 0640); + _set_permissions(cert_folder_domain+"/crt.pem", "root", "metronome", 0640); + _set_permissions(cert_folder_domain+"/openssl.cnf", "root", "root", 0600); + + + +# Install ACME / Let's Encrypt certificate + +def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): + + + if not os.path.exists(account_key_file) : + _generate_account_key() + + # If no domains given, consider all yunohost domains with self-signed + # certificates + if (domainList == []) : + for domain in yunohost.domain.domain_list(auth)['domains'] : + + # Is it self-signed ? + status = _get_status(domain) + if (status["CAtype"] != "Self-signed") : continue + + domainList.append(domain) + + # Else, validate that yunohost knows the domains given + else : + for domain in domainList : + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + # Is it self-signed ? + status = _get_status(domain) + if (not force) and (status["CAtype"] != "Self-signed") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + + + # Actual install steps + for domain in domainList : + + logger.info("Now attempting install of certificate for domain "+domain+" !") + + try : + + if not no_checks : _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) + _configure_for_acme_challenge(auth, domain) + _fetch_and_enable_new_certificate(domain) + _install_cron() + + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) + + except Exception as e : + + logger.error("Certificate installation for "+domain+" failed !") + logger.error(str(e)) + + +# Renew + + +def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): + """ + Renew Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains for which to renew the certificates + force -- Ignore the validity threshold (15 days) + no-check -- Disable some checks about the reachability of web server + before attempting the renewing + email -- Emails root if some renewing failed + """ + + # If no domains given, consider all yunohost domains with Let's Encrypt + # certificates + if (domainList == []) : + for domain in yunohost.domain.domain_list(auth)['domains'] : + + # Does it has a Let's Encrypt cert ? + status = _get_status(domain) + if (status["CAtype"] != "Let's Encrypt") : continue + + # Does it expires soon ? + if (force) or (status["validity"] <= validity_limit) : + domainList.append(domain) + + if (len(domainList) == 0) : + logger.info("No certificate needs to be renewed.") + + # Else, validate the domain list given + else : + for domain in domainList : + + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + status = _get_status(domain) + + # Does it expires soon ? + if not ((force) or (status["validity"] <= validity_limit)) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) + + + # Does it has a Let's Encrypt cert ? + if (status["CAtype"] != "Let's Encrypt") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + + + # Actual renew steps + for domain in domainList : + + logger.info("Now attempting renewing of certificate for domain "+domain+" !") + + try : + + if not no_checks : _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) + _fetch_and_enable_new_certificate(domain) + + logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) + + except Exception as e : + + logger.error("Certificate renewing for "+domain+" failed !") + logger.error(str(e)) + + if (email) : + logger.error("Sending email with details to root ...") + _email_renewing_failed(domain, e) + + + + + +############################################################################### +# Back-end stuff # +############################################################################### + +def _install_cron() : + + cron_job_file = "/etc/cron.weekly/certificateRenewer" + + with open(cron_job_file, "w") as f : + + f.write("#!/bin/bash\n") + f.write("yunohost domain cert-renew --email\n") + + _set_permissions(cron_job_file, "root", "root", 0755); + +def _email_renewing_failed(domain, e) : + + from_ = "certmanager@"+domain+" (Certificate Manager)" + to_ = "root" + subject_ = "Certificate renewing attempt for "+domain+" failed!" + + exceptionMessage = str(e) + logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") + text = """ +At attempt for renewing the certificate for domain %s failed with the following +error : + +%s + +Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to +investigate : + +%s + +-- Certificate Manager + +""" % (domain, exceptionMessage, logs) + + message = """ +From: %s +To: %s +Subject: %s + +%s +""" % (from_, to_, subject_, text) + + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [ to_ ], message) + smtp.quit() + + + +def _configure_for_acme_challenge(auth, domain) : + + nginx_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" + + nginx_configuration = ''' +location '/.well-known/acme-challenge' +{ + default_type "text/plain"; + alias '''+webroot_folder+'''; +} + ''' + + # Write the conf + if os.path.exists(nginx_conf_file) : + logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") + else : + logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + with open(nginx_conf_file, "w") as f : + f.write(nginx_configuration) + + # Assume nginx conf is okay, and reload it + # (FIXME : maybe add a check that it is, using nginx -t, haven't found + # any clean function already implemented in yunohost to do this though) + _run_service_command("reload", "nginx") + + app_ssowatconf(auth) + +def _fetch_and_enable_new_certificate(domain) : + + # Make sure tmp folder exists + logger.debug("Making sure tmp folders exists...") + + if not (os.path.exists(webroot_folder)) : os.makedirs(webroot_folder) + if not (os.path.exists(tmp_folder)) : os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650); + _set_permissions(tmp_folder, "root", "root", 0640); + + # Prepare certificate signing request + logger.info("Prepare key and certificate signing request (CSR) for "+domain+"...") + + domain_key_file = tmp_folder+"/"+domain+".pem" + _generate_key(domain_key_file) + _set_permissions(domain_key_file, "root", "metronome", 0640); + + _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) + + # Sign the certificate + logger.info("Now using ACME Tiny to sign the certificate...") + + domain_csr_file = tmp_folder+"/"+domain+".csr" + + signed_certificate = sign_certificate(account_key_file, + domain_csr_file, + webroot_folder, + log=logger, + CA=certification_authority) + intermediate_certificate = requests.get(intermediate_certificate_url).text + + # Now save the key and signed certificate + logger.info("Saving the key and signed certificate...") + + # Create corresponding directory + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + new_cert_folder = cert_folder+"/" + domain + "." + date_tag + os.makedirs(new_cert_folder) + _set_permissions(new_cert_folder, "root", "root", 0655); + + # Move the private key + shutil.move(domain_key_file, new_cert_folder+"/key.pem") + + # Write the cert + domain_cert_file = new_cert_folder+"/crt.pem" + with open(domain_cert_file, "w") as f : + f.write(signed_certificate) + f.write(intermediate_certificate) + _set_permissions(domain_cert_file, "root", "metronome", 0640); + + + + logger.info("Enabling the new certificate...") + + # Replace (if necessary) the link or folder for live cert + live_link = cert_folder+"/"+domain + + if not os.path.islink(live_link) : + shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link) : + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + # Check the status of the certificate is now good + + statusSummaryCode = _get_status(domain)["summaryCode"] + if (statusSummaryCode < 20) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + + + logger.info("Restarting services...") + + for s in [ "nginx", "postfix", "dovecot", "metronome" ] : + _run_service_command("restart", s) + + +def _prepare_certificate_signing_request(domain, key_file, output_folder) : + + # Init a request + csr = crypto.X509Req() + + # Set the domain + csr.get_subject().CN = domain + + # Set the key + with open(key_file, 'rt') as f : + key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + csr.set_pubkey(key) + + # Sign the request + csr.sign(key, "sha256") + + # Save the request in tmp folder + csr_file = output_folder+domain+".csr" + logger.info("Saving to "+csr_file+" .") + with open(csr_file, "w") as f : + f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) + + +def _get_status(domain) : + + cert_file = cert_folder+"/"+domain+"/crt.pem" + + if (not os.path.isfile(cert_file)) : return {} + + try : + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + + certSubject = cert.get_subject().CN + certIssuer = cert.get_issuer().CN + validUpTo = datetime.strptime(cert.get_notAfter(),"%Y%m%d%H%M%SZ") + daysRemaining = (validUpTo - datetime.now()).days + + CAtype = None + if (certIssuer == _name_selfCA()) : + CAtype = "Self-signed" + elif (certIssuer.startswith("Let's Encrypt")) : + CAtype = "Let's Encrypt" + elif (certIssuer.startswith("Fake LE")) : + CAtype = "Fake Let's Encrypt" + else : + CAtype = "Other / Unknown" + + # Unknown by default + statusSummaryCode = 0 + # Critical + if (daysRemaining <= 0) : statusSummaryCode = -30 + # Warning, self-signed, browser will display a warning discouraging visitors to enter website + elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt") : statusSummaryCode = -20 + # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) + elif (daysRemaining < validity_limit) : statusSummaryCode = -10 + # CA not known, but still a valid certificate, so okay ! + elif (CAtype == "Other / Unknown") : statusSummaryCode = 10 + # Let's Encrypt, great ! + elif (CAtype == "Let's Encrypt") : statusSummaryCode = 20 + + return { "domain" : domain, + "subject" : certSubject, + "CAname" : certIssuer, + "CAtype" : CAtype, + "validity" : daysRemaining, + "summaryCode" : statusSummaryCode + } + +############################################################################### +# Misc small stuff ... # +############################################################################### + +def _generate_account_key() : + + logger.info("Generating account key ...") + _generate_key(account_key_file) + _set_permissions(account_key_file, "root", "root", 0400) + +def _generate_key(destinationPath) : + + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, key_size) + + with open(destinationPath, "w") as f : + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) + +def _set_permissions(path, user, group, permissions) : + + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + + os.chown(path, uid, gid) + os.chmod(path, permissions) + +def _backup_current_cert(domain): + + logger.info("Backuping existing certificate for domain "+domain) + + cert_folder_domain = cert_folder+"/"+domain + + dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") + backup_folder = cert_folder_domain+"-backup-"+dateTag + + shutil.copytree(cert_folder_domain, backup_folder) + + +def _check_domain_is_correctly_configured(domain) : + + public_ip = yunohost.domain.get_public_ip() + + # Check if IP from DNS matches public IP + if not _dns_ip_match_public_ip(public_ip, domain) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + + # Check if domain seems to be accessible through HTTP ? + if not _domain_is_accessible_through_HTTP(public_ip, domain) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) + +def _dns_ip_match_public_ip(public_ip, domain) : + + try : + r = requests.get("http://dns-api.org/A/"+domain) + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) + + if (r.text == "[{\"error\":\"NXDOMAIN\"}]") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) + + try : + dns_ip = json.loads(r.text)[0]["value"] + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) + + if (dns_ip != public_ip) : + return False + else : + return True + +def _domain_is_accessible_through_HTTP(ip, domain) : + + try : + requests.head("http://"+ip, headers = { "Host" : domain }) + except Exception : + return False + + return True + +def _summary_code_to_string(code) : + + if (code <= -30) : return "CRITICAL" + elif (code <= -20) : return "WARNING" + elif (code <= -10) : return "Attention" + elif (code <= 0) : return "Unknown?" + elif (code <= 10) : return "Good" + elif (code <= 20) : return "Great!" + + return "Unknown?" + +def _name_selfCA() : + + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) + return cert.get_subject().CN + + +def _tail(n, filePath): + stdin,stdout = os.popen2("tail -n "+str(n)+" "+filePath) + stdin.close() + lines = stdout.readlines(); stdout.close() + lines = "".join(lines) + return lines diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 98fa368ed..58ca41b08 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -34,10 +34,11 @@ import errno import requests from urllib import urlopen -from moulinette.core import MoulinetteError +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf +from yunohost.service import service_regen_conf +import yunohost.certificate logger = getActionLogger('yunohost.domain') @@ -113,37 +114,7 @@ def domain_add(auth, domain, dyndns=False): m18n.n('domain_dyndns_root_unknown')) try: - # Commands - ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - ssl_domain_path = '/etc/yunohost/certs/%s' % domain - with open('%s/serial' % ssl_dir, 'r') as f: - serial = f.readline().rstrip() - try: os.listdir(ssl_domain_path) - except OSError: os.makedirs(ssl_domain_path) - - command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path), - 'chmod 755 %s' % ssl_domain_path, - 'chmod 640 %s/key.pem' % ssl_domain_path, - 'chmod 640 %s/crt.pem' % ssl_domain_path, - 'chmod 600 %s/openssl.cnf' % ssl_domain_path, - 'chown root:metronome %s/key.pem' % ssl_domain_path, - 'chown root:metronome %s/crt.pem' % ssl_domain_path, - 'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, ssl_domain_path) - ] - - for command in command_list: - if os.system(command) != 0: - raise MoulinetteError(errno.EIO, - m18n.n('domain_cert_gen_failed')) + yunohost.certificate.certificate_install_selfsigned([domain], False) try: auth.validate_uniqueness({ 'virtualdomain': domain }) @@ -285,6 +256,14 @@ def domain_dns_conf(domain, ttl=None): return result +def domain_cert_status(auth, domainList, full=False): + return yunohost.certificate.certificate_status(auth, domainList, full) + +def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): + return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) + +def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): + return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" @@ -301,3 +280,4 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + From d47f5919d679e9cbb87b6980e700735e975b72d8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:22:56 +0100 Subject: [PATCH 0014/1066] [mod] remove unused imports --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4ed5cb588..69a78964e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -25,7 +25,6 @@ """ import os -import sys import errno import requests import shutil @@ -43,7 +42,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.service import _run_service_command -from yunohost.app import app_setting, app_ssowatconf +from yunohost.app import app_ssowatconf logger = getActionLogger('yunohost.certmanager') From bec8f6347959cb5ab38cb24842703e5858bef8f4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:24:54 +0100 Subject: [PATCH 0015/1066] [mod] autopep8 --- src/yunohost/certificate.py | 415 ++++++++++++++++++------------------ 1 file changed, 210 insertions(+), 205 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 69a78964e..e11ea9e3b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,32 +33,34 @@ import grp import json import smtplib -from OpenSSL import crypto -from datetime import datetime -from tabulate import tabulate -from acme_tiny import get_crt as sign_certificate +from OpenSSL import crypto +from datetime import datetime +from tabulate import tabulate +from acme_tiny import get_crt as sign_certificate -from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.service import _run_service_command -from yunohost.app import app_ssowatconf + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +from yunohost.app import app_ssowatconf +from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') # Misc stuff we need -cert_folder = "/etc/yunohost/certs/" -tmp_folder = "/tmp/acme-challenge-private/" -webroot_folder = "/tmp/acme-challenge-public/" +cert_folder = "/etc/yunohost/certs/" +tmp_folder = "/tmp/acme-challenge-private/" +webroot_folder = "/tmp/acme-challenge-public/" -selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" +selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" account_key_file = "/etc/yunohost/letsencrypt_account.pem" -key_size = 2048 +key_size = 2048 -validity_limit = 15 # days +validity_limit = 15 # days # For tests #certification_authority = "https://acme-staging.api.letsencrypt.org" @@ -73,7 +75,8 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Status -def certificate_status(auth, domainList, full = False): + +def certificate_status(auth, domainList, full=False): """ Print the status of certificate for given domains (all by default) @@ -83,36 +86,39 @@ def certificate_status(auth, domainList, full = False): """ # If no domains given, consider all yunohost domains - if (domainList == []) : domainList = yunohost.domain.domain_list(auth)['domains'] + if (domainList == []): + domainList = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Get status for each domain, and prepare display using tabulate - if not full : - headers = [ "Domain", "Certificate status", "Authority type", "Days remaining"] - else : - headers = [ "Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + if not full: + headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] + else: + headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] lines = [] - for domain in domainList : + for domain in domainList: status = _get_status(domain) line = [] line.append(domain) - if (full) : line.append(status["subject"]) + if (full): + line.append(status["subject"]) line.append(_summary_code_to_string(status["summaryCode"])) line.append(status["CAtype"]) - if (full) : line.append(status["CAname"]) + if (full): + line.append(status["CAname"]) line.append(status["validity"]) lines.append(line) print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) -def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False) : +def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -122,31 +128,29 @@ def certificate_install(auth, domainList, force=False, no_checks=False, self_sig no-check -- Disable some checks about the reachability of web server before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt - """ - if (self_signed) : + """ + if (self_signed): certificate_install_selfsigned(domainList, force) - else : + else: certificate_install_letsencrypt(auth, domainList, force, no_checks) -# Install self-signed +# Install self-signed -def certificate_install_selfsigned(domainList, force=False) : - - for domain in domainList : +def certificate_install_selfsigned(domainList, force=False): + for domain in domainList: # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if (status != {}) and (status["summaryCode"] > 0) and (not force) : + if (status != {}) and (status["summaryCode"] > 0) and (not force): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - - cert_folder_domain = cert_folder+"/"+domain + cert_folder_domain = cert_folder + "/" + domain # Create cert folder if it does not exists yet - try: + try: os.listdir(cert_folder_domain) - except OSError: + except OSError: os.makedirs(cert_folder_domain) # Get serial @@ -157,79 +161,77 @@ def certificate_install_selfsigned(domainList, force=False) : # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), - 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) + 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), + 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), + 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) - _set_permissions(cert_folder_domain, "root", "root", 0755); - _set_permissions(cert_folder_domain+"/key.pem", "root", "metronome", 0640); - _set_permissions(cert_folder_domain+"/crt.pem", "root", "metronome", 0640); - _set_permissions(cert_folder_domain+"/openssl.cnf", "root", "root", 0600); - + _set_permissions(cert_folder_domain, "root", "root", 0755) + _set_permissions(cert_folder_domain + "/key.pem", "root", "metronome", 0640) + _set_permissions(cert_folder_domain + "/crt.pem", "root", "metronome", 0640) + _set_permissions(cert_folder_domain + "/openssl.cnf", "root", "root", 0600) # Install ACME / Let's Encrypt certificate def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): - - - if not os.path.exists(account_key_file) : + if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if (domainList == []) : - for domain in yunohost.domain.domain_list(auth)['domains'] : + if (domainList == []): + for domain in yunohost.domain.domain_list(auth)['domains']: # Is it self-signed ? status = _get_status(domain) - if (status["CAtype"] != "Self-signed") : continue + if (status["CAtype"] != "Self-signed"): + continue domainList.append(domain) # Else, validate that yunohost knows the domains given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Is it self-signed ? status = _get_status(domain) - if (not force) and (status["CAtype"] != "Self-signed") : + if (not force) and (status["CAtype"] != "Self-signed"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) - # Actual install steps - for domain in domainList : - - logger.info("Now attempting install of certificate for domain "+domain+" !") + for domain in domainList: - try : + logger.info("Now attempting install of certificate for domain " + domain + " !") - if not no_checks : _check_domain_is_correctly_configured(domain) + try: + + if not no_checks: + _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) _install_cron() - + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) - - except Exception as e : - - logger.error("Certificate installation for "+domain+" failed !") + + except Exception as e: + + logger.error("Certificate installation for " + domain + " failed !") logger.error(str(e)) @@ -250,86 +252,82 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if (domainList == []) : - for domain in yunohost.domain.domain_list(auth)['domains'] : + if (domainList == []): + for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if (status["CAtype"] != "Let's Encrypt") : continue + if (status["CAtype"] != "Let's Encrypt"): + continue # Does it expires soon ? - if (force) or (status["validity"] <= validity_limit) : + if (force) or (status["validity"] <= validity_limit): domainList.append(domain) - if (len(domainList) == 0) : + if (len(domainList) == 0): logger.info("No certificate needs to be renewed.") # Else, validate the domain list given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - + status = _get_status(domain) # Does it expires soon ? - if not ((force) or (status["validity"] <= validity_limit)) : + if not ((force) or (status["validity"] <= validity_limit)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it has a Let's Encrypt cert ? - if (status["CAtype"] != "Let's Encrypt") : + if (status["CAtype"] != "Let's Encrypt"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) - # Actual renew steps - for domain in domainList : - - logger.info("Now attempting renewing of certificate for domain "+domain+" !") + for domain in domainList: - try : + logger.info("Now attempting renewing of certificate for domain " + domain + " !") - if not no_checks : _check_domain_is_correctly_configured(domain) + try: + + if not no_checks: + _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) - - except Exception as e : - - logger.error("Certificate renewing for "+domain+" failed !") + + except Exception as e: + + logger.error("Certificate renewing for " + domain + " failed !") logger.error(str(e)) - if (email) : + if (email): logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e) - - - ############################################################################### # Back-end stuff # ############################################################################### -def _install_cron() : - +def _install_cron(): cron_job_file = "/etc/cron.weekly/certificateRenewer" - with open(cron_job_file, "w") as f : + with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") f.write("yunohost domain cert-renew --email\n") - _set_permissions(cron_job_file, "root", "root", 0755); + _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, e) : - from_ = "certmanager@"+domain+" (Certificate Manager)" - to_ = "root" - subject_ = "Certificate renewing attempt for "+domain+" failed!" +def _email_renewing_failed(domain, e): + from_ = "certmanager@" + domain + " (Certificate Manager)" + to_ = "root" + subject_ = "Certificate renewing attempt for " + domain + " failed!" exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") @@ -357,29 +355,27 @@ Subject: %s """ % (from_, to_, subject_, text) smtp = smtplib.SMTP("localhost") - smtp.sendmail(from_, [ to_ ], message) + smtp.sendmail(from_, [to_], message) smtp.quit() - -def _configure_for_acme_challenge(auth, domain) : - - nginx_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" +def _configure_for_acme_challenge(auth, domain): + nginx_conf_file = "/etc/nginx/conf.d/" + domain + ".d/000-acmechallenge.conf" nginx_configuration = ''' location '/.well-known/acme-challenge' { default_type "text/plain"; - alias '''+webroot_folder+'''; + alias ''' + webroot_folder + '''; } ''' # Write the conf - if os.path.exists(nginx_conf_file) : + if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") - else : + else: logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") - with open(nginx_conf_file, "w") as f : + with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) # Assume nginx conf is okay, and reload it @@ -389,29 +385,31 @@ location '/.well-known/acme-challenge' app_ssowatconf(auth) -def _fetch_and_enable_new_certificate(domain) : +def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not (os.path.exists(webroot_folder)) : os.makedirs(webroot_folder) - if not (os.path.exists(tmp_folder)) : os.makedirs(tmp_folder) - _set_permissions(webroot_folder, "root", "www-data", 0650); - _set_permissions(tmp_folder, "root", "root", 0640); + if not (os.path.exists(webroot_folder)): + os.makedirs(webroot_folder) + if not (os.path.exists(tmp_folder)): + os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650) + _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for "+domain+"...") + logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") - domain_key_file = tmp_folder+"/"+domain+".pem" + domain_key_file = tmp_folder + "/" + domain + ".pem" _generate_key(domain_key_file) - _set_permissions(domain_key_file, "root", "metronome", 0640); + _set_permissions(domain_key_file, "root", "metronome", 0640) _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = tmp_folder+"/"+domain+".csr" + domain_csr_file = tmp_folder + "/" + domain + ".csr" signed_certificate = sign_certificate(account_key_file, domain_csr_file, @@ -425,30 +423,28 @@ def _fetch_and_enable_new_certificate(domain) : # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = cert_folder+"/" + domain + "." + date_tag + new_cert_folder = cert_folder + "/" + domain + "." + date_tag os.makedirs(new_cert_folder) - _set_permissions(new_cert_folder, "root", "root", 0655); + _set_permissions(new_cert_folder, "root", "root", 0655) # Move the private key - shutil.move(domain_key_file, new_cert_folder+"/key.pem") + shutil.move(domain_key_file, new_cert_folder + "/key.pem") # Write the cert - domain_cert_file = new_cert_folder+"/crt.pem" - with open(domain_cert_file, "w") as f : + domain_cert_file = new_cert_folder + "/crt.pem" + with open(domain_cert_file, "w") as f: f.write(signed_certificate) f.write(intermediate_certificate) - _set_permissions(domain_cert_file, "root", "metronome", 0640); - - + _set_permissions(domain_cert_file, "root", "metronome", 0640) logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = cert_folder+"/"+domain + live_link = cert_folder + "/" + domain - if not os.path.islink(live_link) : - shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) - elif os.path.lexists(live_link) : + if not os.path.islink(live_link): + shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link): os.remove(live_link) os.symlink(new_cert_folder, live_link) @@ -456,18 +452,16 @@ def _fetch_and_enable_new_certificate(domain) : # Check the status of the certificate is now good statusSummaryCode = _get_status(domain)["summaryCode"] - if (statusSummaryCode < 20) : + if (statusSummaryCode < 20): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) - logger.info("Restarting services...") - for s in [ "nginx", "postfix", "dovecot", "metronome" ] : + for s in ["nginx", "postfix", "dovecot", "metronome"]: _run_service_command("restart", s) -def _prepare_certificate_signing_request(domain, key_file, output_folder) : - +def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request csr = crypto.X509Req() @@ -475,7 +469,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder) : csr.get_subject().CN = domain # Set the key - with open(key_file, 'rt') as f : + with open(key_file, 'rt') as f: key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) csr.set_pubkey(key) @@ -483,158 +477,169 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder) : csr.sign(key, "sha256") # Save the request in tmp folder - csr_file = output_folder+domain+".csr" - logger.info("Saving to "+csr_file+" .") - with open(csr_file, "w") as f : + csr_file = output_folder + domain + ".csr" + logger.info("Saving to " + csr_file + " .") + with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) -def _get_status(domain) : +def _get_status(domain): + cert_file = cert_folder + "/" + domain + "/crt.pem" - cert_file = cert_folder+"/"+domain+"/crt.pem" + if (not os.path.isfile(cert_file)): + return {} - if (not os.path.isfile(cert_file)) : return {} - - try : + try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) - except : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + except: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) certSubject = cert.get_subject().CN certIssuer = cert.get_issuer().CN - validUpTo = datetime.strptime(cert.get_notAfter(),"%Y%m%d%H%M%SZ") + validUpTo = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") daysRemaining = (validUpTo - datetime.now()).days CAtype = None - if (certIssuer == _name_selfCA()) : + if (certIssuer == _name_selfCA()): CAtype = "Self-signed" - elif (certIssuer.startswith("Let's Encrypt")) : + elif (certIssuer.startswith("Let's Encrypt")): CAtype = "Let's Encrypt" - elif (certIssuer.startswith("Fake LE")) : + elif (certIssuer.startswith("Fake LE")): CAtype = "Fake Let's Encrypt" - else : + else: CAtype = "Other / Unknown" # Unknown by default statusSummaryCode = 0 # Critical - if (daysRemaining <= 0) : statusSummaryCode = -30 + if (daysRemaining <= 0): + statusSummaryCode = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt") : statusSummaryCode = -20 + elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): + statusSummaryCode = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif (daysRemaining < validity_limit) : statusSummaryCode = -10 + elif (daysRemaining < validity_limit): + statusSummaryCode = -10 # CA not known, but still a valid certificate, so okay ! - elif (CAtype == "Other / Unknown") : statusSummaryCode = 10 + elif (CAtype == "Other / Unknown"): + statusSummaryCode = 10 # Let's Encrypt, great ! - elif (CAtype == "Let's Encrypt") : statusSummaryCode = 20 + elif (CAtype == "Let's Encrypt"): + statusSummaryCode = 20 - return { "domain" : domain, - "subject" : certSubject, - "CAname" : certIssuer, - "CAtype" : CAtype, - "validity" : daysRemaining, - "summaryCode" : statusSummaryCode - } + return {"domain": domain, + "subject": certSubject, + "CAname": certIssuer, + "CAtype": CAtype, + "validity": daysRemaining, + "summaryCode": statusSummaryCode + } ############################################################################### # Misc small stuff ... # ############################################################################### -def _generate_account_key() : +def _generate_account_key(): logger.info("Generating account key ...") _generate_key(account_key_file) _set_permissions(account_key_file, "root", "root", 0400) -def _generate_key(destinationPath) : +def _generate_key(destinationPath): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, key_size) - with open(destinationPath, "w") as f : + with open(destinationPath, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) -def _set_permissions(path, user, group, permissions) : +def _set_permissions(path, user, group, permissions): uid = pwd.getpwnam(user).pw_uid gid = grp.getgrnam(group).gr_gid os.chown(path, uid, gid) os.chmod(path, permissions) + def _backup_current_cert(domain): + logger.info("Backuping existing certificate for domain " + domain) - logger.info("Backuping existing certificate for domain "+domain) - - cert_folder_domain = cert_folder+"/"+domain + cert_folder_domain = cert_folder + "/" + domain dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = cert_folder_domain+"-backup-"+dateTag + backup_folder = cert_folder_domain + "-backup-" + dateTag shutil.copytree(cert_folder_domain, backup_folder) -def _check_domain_is_correctly_configured(domain) : - +def _check_domain_is_correctly_configured(domain): public_ip = yunohost.domain.get_public_ip() # Check if IP from DNS matches public IP - if not _dns_ip_match_public_ip(public_ip, domain) : + if not _dns_ip_match_public_ip(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP ? - if not _domain_is_accessible_through_HTTP(public_ip, domain) : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) - -def _dns_ip_match_public_ip(public_ip, domain) : + if not _domain_is_accessible_through_HTTP(public_ip, domain): + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) - try : - r = requests.get("http://dns-api.org/A/"+domain) - except : + +def _dns_ip_match_public_ip(public_ip, domain): + try: + r = requests.get("http://dns-api.org/A/" + domain) + except: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) - - if (r.text == "[{\"error\":\"NXDOMAIN\"}]") : + + if (r.text == "[{\"error\":\"NXDOMAIN\"}]"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - try : + try: dns_ip = json.loads(r.text)[0]["value"] - except : + except: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) - if (dns_ip != public_ip) : + if (dns_ip != public_ip): return False - else : + else: return True -def _domain_is_accessible_through_HTTP(ip, domain) : - try : - requests.head("http://"+ip, headers = { "Host" : domain }) - except Exception : +def _domain_is_accessible_through_HTTP(ip, domain): + try: + requests.head("http://" + ip, headers={"Host": domain}) + except Exception: return False return True -def _summary_code_to_string(code) : - if (code <= -30) : return "CRITICAL" - elif (code <= -20) : return "WARNING" - elif (code <= -10) : return "Attention" - elif (code <= 0) : return "Unknown?" - elif (code <= 10) : return "Good" - elif (code <= 20) : return "Great!" +def _summary_code_to_string(code): + if (code <= -30): + return "CRITICAL" + elif (code <= -20): + return "WARNING" + elif (code <= -10): + return "Attention" + elif (code <= 0): + return "Unknown?" + elif (code <= 10): + return "Good" + elif (code <= 20): + return "Great!" return "Unknown?" -def _name_selfCA() : +def _name_selfCA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN def _tail(n, filePath): - stdin,stdout = os.popen2("tail -n "+str(n)+" "+filePath) - stdin.close() - lines = stdout.readlines(); stdout.close() - lines = "".join(lines) - return lines + stdin, stdout = os.popen2("tail -n " + str(n) + " " + filePath) + stdin.close() + lines = stdout.readlines() + stdout.close() + lines = "".join(lines) + return lines From 32bf7423676938f88d561038781dfed94d37dbf4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:25:23 +0100 Subject: [PATCH 0016/1066] [mod] trailing spaces --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2326995ae..c8e9ff481 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1037,11 +1037,11 @@ def app_ssowatconf(auth): # Check ACME challenge file is present in nginx conf nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue - + # Check the file contains the ACME challenge uri acme_uri = '/.well-known/acme-challenge' if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue - + # If so, then authorize the ACME challenge uri to unprotected regex regex = domain+"/%.well%-known/acme%-challenge/.*$" unprotected_regex.append(regex) From 02fc92d21083b869a7f8afe1ed34c24b2d0a0601 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:27:12 +0100 Subject: [PATCH 0017/1066] [mod] pep8 --- src/yunohost/app.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c8e9ff481..e3d5ad202 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1035,15 +1035,19 @@ def app_ssowatconf(auth): for domain in domains: # Check ACME challenge file is present in nginx conf - nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" - if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue + nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain + + if not os.path.isfile(nginx_acme_challenge_conf_file): + continue # Check the file contains the ACME challenge uri acme_uri = '/.well-known/acme-challenge' - if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue + + if not acme_uri in open(nginx_acme_challenge_conf_file).read(): + continue # If so, then authorize the ACME challenge uri to unprotected regex - regex = domain+"/%.well%-known/acme%-challenge/.*$" + regex = domain + "/%.well%-known/acme%-challenge/.*$" unprotected_regex.append(regex) From 6a1727da89f26de8b10a7a83099af245e21e7cfb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:27:39 +0100 Subject: [PATCH 0018/1066] [mod] remove useless imports --- src/yunohost/domain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 58ca41b08..614409fa7 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -24,10 +24,8 @@ Manage domains """ import os -import sys import datetime import re -import shutil import json import yaml import errno From ef6287795299baf7b4b89d28387f5b9feca8ae54 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:29:16 +0100 Subject: [PATCH 0019/1066] [mod] pep8 --- src/yunohost/domain.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 614409fa7..1a296ee97 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -30,14 +30,16 @@ import json import yaml import errno import requests + from urllib import urlopen -from moulinette.core import MoulinetteError +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf import yunohost.certificate +from yunohost.service import service_regen_conf + logger = getActionLogger('yunohost.domain') @@ -115,7 +117,7 @@ def domain_add(auth, domain, dyndns=False): yunohost.certificate.certificate_install_selfsigned([domain], False) try: - auth.validate_uniqueness({ 'virtualdomain': domain }) + auth.validate_uniqueness({'virtualdomain': domain}) except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) @@ -254,15 +256,19 @@ def domain_dns_conf(domain, ttl=None): return result + def domain_cert_status(auth, domainList, full=False): return yunohost.certificate.certificate_status(auth, domainList, full) + def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) + def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" if protocol == 4: @@ -278,4 +284,3 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) - From aff4dc40868b8e0fbfbc740bed17bc9ffd3991bb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:42:21 +0100 Subject: [PATCH 0020/1066] [mod] more verbose error --- locales/en.json | 2 +- src/yunohost/certificate.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 35da2eef0..c2f7b7402 100644 --- a/locales/en.json +++ b/locales/en.json @@ -249,7 +249,7 @@ "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", - "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s})", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e11ea9e3b..9d19d84cd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -491,8 +491,10 @@ def _get_status(domain): try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + except Exception as exception: + import traceback + traceback.print_exc(file=sys.stdout) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) certSubject = cert.get_subject().CN certIssuer = cert.get_issuer().CN From 917c23073581d2f2d5bbc814ef50c48b8426af5c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:53:16 +0100 Subject: [PATCH 0021/1066] [mod] more pythonic and explicit tests with more verbose errors --- locales/en.json | 2 +- src/yunohost/certificate.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/locales/en.json b/locales/en.json index c2f7b7402..bdfd1b14a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,7 +245,7 @@ "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", - "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}). Use --no-checks to disable checks.", + "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}), reason: {reason:s}. Use --no-checks to disable checks.", "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9d19d84cd..89c00943c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -589,22 +589,25 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - r = requests.get("http://dns-api.org/A/" + domain) - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) + result = requests.get("http://dns-api.org/A/" + domain) + except Exception as exception: + import traceback + traceback.print_exc(file=sys.stdout) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) - if (r.text == "[{\"error\":\"NXDOMAIN\"}]"): + dns_ip = result.json() + if not dns_ip or "value" not in dns_ip[0]: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip[0]["value"] + + if dns_ip.get("error") == "NXDOMAIN": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - try: - dns_ip = json.loads(r.text)[0]["value"] - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) - - if (dns_ip != public_ip): - return False - else: + if dns_ip == public_ip: return True + else: + return False def _domain_is_accessible_through_HTTP(ip, domain): From 29f5f2d7534da9edaae2cd97ea9adb7b27cb09cb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:56:06 +0100 Subject: [PATCH 0022/1066] [mod] more pythonic code --- src/yunohost/certificate.py | 127 ++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 89c00943c..c318b63c2 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -25,12 +25,12 @@ """ import os +import sys import errno import requests import shutil import pwd import grp -import json import smtplib from OpenSSL import crypto @@ -100,18 +100,23 @@ def certificate_status(auth, domainList, full=False): headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] else: headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + lines = [] for domain in domainList: status = _get_status(domain) line = [] line.append(domain) - if (full): + + if full: line.append(status["subject"]) + line.append(_summary_code_to_string(status["summaryCode"])) line.append(status["CAtype"]) - if (full): + + if full: line.append(status["CAname"]) + line.append(status["validity"]) lines.append(line) @@ -142,20 +147,19 @@ def certificate_install_selfsigned(domainList, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if (status != {}) and (status["summaryCode"] > 0) and (not force): + + if status != {} and status["summaryCode"] > 0 and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = cert_folder + "/" + domain + cert_folder_domain = os.path.join(cert_folder, domain) # Create cert folder if it does not exists yet - try: - os.listdir(cert_folder_domain) - except OSError: + if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - with open('%s/serial' % ssl_dir, 'r') as f: + with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() # FIXME : should refactor this to avoid so many os.system() calls... @@ -178,25 +182,22 @@ def certificate_install_selfsigned(domainList, force=False): raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) _set_permissions(cert_folder_domain, "root", "root", 0755) - _set_permissions(cert_folder_domain + "/key.pem", "root", "metronome", 0640) - _set_permissions(cert_folder_domain + "/crt.pem", "root", "metronome", 0640) - _set_permissions(cert_folder_domain + "/openssl.cnf", "root", "root", 0600) + _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) -# Install ACME / Let's Encrypt certificate - def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if (domainList == []): + if domainList == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Is it self-signed ? status = _get_status(domain) - if (status["CAtype"] != "Self-signed"): + if status["CAtype"] != "Self-signed": continue domainList.append(domain) @@ -210,7 +211,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal # Is it self-signed ? status = _get_status(domain) - if (not force) and (status["CAtype"] != "Self-signed"): + if not force and status["CAtype"] != "Self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps @@ -219,9 +220,9 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.info("Now attempting install of certificate for domain " + domain + " !") try: - if not no_checks: _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) @@ -230,14 +231,10 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - - logger.error("Certificate installation for " + domain + " failed !") + logger.error("Certificate installation for %s failed !" % domain) logger.error(str(e)) -# Renew - - def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -257,7 +254,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if (status["CAtype"] != "Let's Encrypt"): + if status["CAtype"] != "Let's Encrypt": continue # Does it expires soon ? @@ -278,20 +275,18 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals status = _get_status(domain) # Does it expires soon ? - if not ((force) or (status["validity"] <= validity_limit)): + if not (force or status["validity"] <= validity_limit): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? - if (status["CAtype"] != "Let's Encrypt"): + if status["CAtype"] != "Let's Encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps for domain in domainList: - logger.info("Now attempting renewing of certificate for domain " + domain + " !") try: - if not no_checks: _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) @@ -300,11 +295,10 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: - logger.error("Certificate renewing for " + domain + " failed !") logger.error(str(e)) - if (email): + if email: logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e) @@ -317,7 +311,6 @@ def _install_cron(): cron_job_file = "/etc/cron.weekly/certificateRenewer" with open(cron_job_file, "w") as f: - f.write("#!/bin/bash\n") f.write("yunohost domain cert-renew --email\n") @@ -325,9 +318,9 @@ def _install_cron(): def _email_renewing_failed(domain, e): - from_ = "certmanager@" + domain + " (Certificate Manager)" + from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" - subject_ = "Certificate renewing attempt for " + domain + " failed!" + subject_ = "Certificate renewing attempt for %s failed!" % domain exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") @@ -360,7 +353,7 @@ Subject: %s def _configure_for_acme_challenge(auth, domain): - nginx_conf_file = "/etc/nginx/conf.d/" + domain + ".d/000-acmechallenge.conf" + nginx_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain nginx_configuration = ''' location '/.well-known/acme-challenge' @@ -375,6 +368,7 @@ location '/.well-known/acme-challenge' logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") else: logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -390,17 +384,19 @@ def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not (os.path.exists(webroot_folder)): + if not os.path.exists(webroot_folder): os.makedirs(webroot_folder) - if not (os.path.exists(tmp_folder)): + + if not os.path.exists(tmp_folder): os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650) _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") - domain_key_file = tmp_folder + "/" + domain + ".pem" + domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) @@ -409,13 +405,14 @@ def _fetch_and_enable_new_certificate(domain): # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = tmp_folder + "/" + domain + ".csr" + domain_csr_file = "%s/%s.csr" % (tmp_folder, domain) signed_certificate = sign_certificate(account_key_file, domain_csr_file, webroot_folder, log=logger, CA=certification_authority) + intermediate_certificate = requests.get(intermediate_certificate_url).text # Now save the key and signed certificate @@ -423,42 +420,47 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = cert_folder + "/" + domain + "." + date_tag + + new_cert_folder = "%s/%s.%s" % (cert_folder, domain, date_tag) os.makedirs(new_cert_folder) + _set_permissions(new_cert_folder, "root", "root", 0655) # Move the private key - shutil.move(domain_key_file, new_cert_folder + "/key.pem") + shutil.move(domain_key_file, os.path.join(new_cert_folder, "key.pem")) # Write the cert - domain_cert_file = new_cert_folder + "/crt.pem" + domain_cert_file = os.path.join(new_cert_folder, "crt.pem") + with open(domain_cert_file, "w") as f: f.write(signed_certificate) f.write(intermediate_certificate) + _set_permissions(domain_cert_file, "root", "metronome", 0640) logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = cert_folder + "/" + domain + live_link = os.path.join(cert_folder, domain) if not os.path.islink(live_link): shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link): os.remove(live_link) os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - statusSummaryCode = _get_status(domain)["summaryCode"] - if (statusSummaryCode < 20): + + if statusSummaryCode < 20: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") - for s in ["nginx", "postfix", "dovecot", "metronome"]: - _run_service_command("restart", s) + for service in ("nginx", "postfix", "dovecot", "metronome"): + _run_service_command("restart", service) def _prepare_certificate_signing_request(domain, key_file, output_folder): @@ -471,6 +473,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the key with open(key_file, 'rt') as f: key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + csr.set_pubkey(key) # Sign the request @@ -479,6 +482,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Save the request in tmp folder csr_file = output_folder + domain + ".csr" logger.info("Saving to " + csr_file + " .") + with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) @@ -486,7 +490,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): cert_file = cert_folder + "/" + domain + "/crt.pem" - if (not os.path.isfile(cert_file)): + if not os.path.isfile(cert_file): return {} try: @@ -504,38 +508,47 @@ def _get_status(domain): CAtype = None if (certIssuer == _name_selfCA()): CAtype = "Self-signed" + elif (certIssuer.startswith("Let's Encrypt")): CAtype = "Let's Encrypt" + elif (certIssuer.startswith("Fake LE")): CAtype = "Fake Let's Encrypt" + else: CAtype = "Other / Unknown" # Unknown by default statusSummaryCode = 0 + # Critical if (daysRemaining <= 0): statusSummaryCode = -30 + # Warning, self-signed, browser will display a warning discouraging visitors to enter website elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): statusSummaryCode = -20 + # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) elif (daysRemaining < validity_limit): statusSummaryCode = -10 + # CA not known, but still a valid certificate, so okay ! elif (CAtype == "Other / Unknown"): statusSummaryCode = 10 + # Let's Encrypt, great ! elif (CAtype == "Let's Encrypt"): statusSummaryCode = 20 - return {"domain": domain, - "subject": certSubject, - "CAname": certIssuer, - "CAtype": CAtype, - "validity": daysRemaining, - "summaryCode": statusSummaryCode - } + return { + "domain": domain, + "subject": certSubject, + "CAname": certIssuer, + "CAtype": CAtype, + "validity": daysRemaining, + "summaryCode": statusSummaryCode + } ############################################################################### # Misc small stuff ... # @@ -567,10 +580,10 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain " + domain) - cert_folder_domain = cert_folder + "/" + domain + cert_folder_domain = os.path.join(cert_folder, domain) dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = cert_folder_domain + "-backup-" + dateTag + backup_folder = "%s-backup-%s" % (cert_folder_domain, dateTag) shutil.copytree(cert_folder_domain, backup_folder) From 39aaa1763984c0ee48eef15c4fc6b2a2fb1028db Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:08:44 +0100 Subject: [PATCH 0023/1066] [mod] remove useless () --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c318b63c2..f94a2d4e6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -86,7 +86,7 @@ def certificate_status(auth, domainList, full=False): """ # If no domains given, consider all yunohost domains - if (domainList == []): + if domainList == []: domainList = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: From 7dbbd7fdc2bb919af490f213573da5221ce82ce6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:19:15 +0100 Subject: [PATCH 0024/1066] [fix] correctly handle all cases --- src/yunohost/certificate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f94a2d4e6..5dc16cba8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -608,15 +608,24 @@ def _dns_ip_match_public_ip(public_ip, domain): traceback.print_exc(file=sys.stdout) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) - dns_ip = result.json() - if not dns_ip or "value" not in dns_ip[0]: + try: + dns_ip = result.json() + except Exception as exception: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - dns_ip = dns_ip[0]["value"] + if len(dns_ip) == 0: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip[0] if dns_ip.get("error") == "NXDOMAIN": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) + if "value" not in dns_ip: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip["value"] + if dns_ip == public_ip: return True else: From ea179b1683b9ac96eb7fbb8ab493319cb7fd5a72 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:20:13 +0100 Subject: [PATCH 0025/1066] [mod] simplier condition --- src/yunohost/certificate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5dc16cba8..7d96aebca 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -626,10 +626,7 @@ def _dns_ip_match_public_ip(public_ip, domain): dns_ip = dns_ip["value"] - if dns_ip == public_ip: - return True - else: - return False + return dns_ip == public_ip def _domain_is_accessible_through_HTTP(ip, domain): From 9a4dbd5d31abb3956f55956875ce0f9f863295eb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:20:40 +0100 Subject: [PATCH 0026/1066] [fix] uses https --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 7d96aebca..932bf1855 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -602,7 +602,7 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - result = requests.get("http://dns-api.org/A/" + domain) + result = requests.get("https://dns-api.org/A/" + domain) except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) From ac9f61c7b1f808c2e93c977831a9a54a7d0552b5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:21:27 +0100 Subject: [PATCH 0027/1066] [mod] more pythonic code --- src/yunohost/certificate.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 932bf1855..5f8c7ff2c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -639,17 +639,22 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _summary_code_to_string(code): - if (code <= -30): + if code <= -30: return "CRITICAL" - elif (code <= -20): - return "WARNING" - elif (code <= -10): - return "Attention" - elif (code <= 0): - return "Unknown?" - elif (code <= 10): - return "Good" - elif (code <= 20): + + if code <= -20: + return "WARNING" + + if code <= -10: + return "Attention" + + if code <= 0: + return "Unknown?" + + if code <= 10: + return "Good" + + if code <= 20: return "Great!" return "Unknown?" From ac901528c325bfc7f62e7a3c5c7f1a8c43f3d18a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:22:30 +0100 Subject: [PATCH 0028/1066] [mod] uses + for strings --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5f8c7ff2c..de21299d1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -666,7 +666,7 @@ def _name_selfCA(): def _tail(n, filePath): - stdin, stdout = os.popen2("tail -n " + str(n) + " " + filePath) + stdin, stdout = os.popen2("tail -n %s '%s'" % (n, filePath)) stdin.close() lines = stdout.readlines() stdout.close() From cd21edb26704250305275f61fdbca9c357d9f8aa Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:22:43 +0100 Subject: [PATCH 0029/1066] [mod] pep8 --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index de21299d1..94defcd2b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -665,8 +665,8 @@ def _name_selfCA(): return cert.get_subject().CN -def _tail(n, filePath): - stdin, stdout = os.popen2("tail -n %s '%s'" % (n, filePath)) +def _tail(n, file_path): + stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path)) stdin.close() lines = stdout.readlines() stdout.close() From a7b9226667935158501fe39a56bcd1066359fa18 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:23:32 +0100 Subject: [PATCH 0030/1066] [mod] lisibility --- src/yunohost/certificate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 94defcd2b..b1efb3725 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -667,8 +667,10 @@ def _name_selfCA(): def _tail(n, file_path): stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path)) + stdin.close() + lines = stdout.readlines() stdout.close() - lines = "".join(lines) - return lines + + return "".join(lines) From dd893b0838d78e2588102ee71b6c70c068bc34c5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:26:52 +0100 Subject: [PATCH 0031/1066] [mod] remove useless () --- src/yunohost/certificate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b1efb3725..1196a2e93 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -506,13 +506,13 @@ def _get_status(domain): daysRemaining = (validUpTo - datetime.now()).days CAtype = None - if (certIssuer == _name_selfCA()): + if certIssuer == _name_selfCA(): CAtype = "Self-signed" - elif (certIssuer.startswith("Let's Encrypt")): + elif certIssuer.startswith("Let's Encrypt"): CAtype = "Let's Encrypt" - elif (certIssuer.startswith("Fake LE")): + elif certIssuer.startswith("Fake LE"): CAtype = "Fake Let's Encrypt" else: @@ -522,23 +522,23 @@ def _get_status(domain): statusSummaryCode = 0 # Critical - if (daysRemaining <= 0): + if daysRemaining <= 0: statusSummaryCode = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): + elif CAtype == "Self-signed" or CAtype == "Fake Let's Encrypt": statusSummaryCode = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif (daysRemaining < validity_limit): + elif daysRemaining < validity_limit: statusSummaryCode = -10 # CA not known, but still a valid certificate, so okay ! - elif (CAtype == "Other / Unknown"): + elif CAtype == "Other / Unknown": statusSummaryCode = 10 # Let's Encrypt, great ! - elif (CAtype == "Let's Encrypt"): + elif CAtype == "Let's Encrypt": statusSummaryCode = 20 return { From 34f98905175033f5ecf5575797517df573b72123 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:29:10 +0100 Subject: [PATCH 0032/1066] [mod] more pythonic string concatenation --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 1196a2e93..12ee4dbf8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -359,15 +359,15 @@ def _configure_for_acme_challenge(auth, domain): location '/.well-known/acme-challenge' { default_type "text/plain"; - alias ''' + webroot_folder + '''; + alias %s; } - ''' + ''' % webroot_folder # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") else: - logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -394,7 +394,7 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") + logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) From 55d007f13037a0efc2ffe1661ec7e907e466fec7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:30:54 +0100 Subject: [PATCH 0033/1066] [mod] avoid useless indentation --- src/yunohost/certificate.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 12ee4dbf8..f83f98570 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -366,18 +366,19 @@ location '/.well-known/acme-challenge' # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") - else: - logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) + return - with open(nginx_conf_file, "w") as f: - f.write(nginx_configuration) + logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) - # Assume nginx conf is okay, and reload it - # (FIXME : maybe add a check that it is, using nginx -t, haven't found - # any clean function already implemented in yunohost to do this though) - _run_service_command("reload", "nginx") + with open(nginx_conf_file, "w") as f: + f.write(nginx_configuration) - app_ssowatconf(auth) + # Assume nginx conf is okay, and reload it + # (FIXME : maybe add a check that it is, using nginx -t, haven't found + # any clean function already implemented in yunohost to do this though) + _run_service_command("reload", "nginx") + + app_ssowatconf(auth) def _fetch_and_enable_new_certificate(domain): From e8e07d464add2dc54ac84a613fc64a970354a87c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:31:36 +0100 Subject: [PATCH 0034/1066] [mod] remove useless variable --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f83f98570..426e0f66a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -317,12 +317,11 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, e): +def _email_renewing_failed(domain, exceptionMessage): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain - exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") text = """ At attempt for renewing the certificate for domain %s failed with the following From 5615e3f4fe7dc9045bfa699b7e43484bb9b4dba3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:33:21 +0100 Subject: [PATCH 0035/1066] [mod] uses logger string concatenation api --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 426e0f66a..c35d15f8e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Actual renew steps for domain in domainList: - logger.info("Now attempting renewing of certificate for domain " + domain + " !") + logger.info("Now attempting renewing of certificate for domain %s !", domain) try: if not no_checks: @@ -295,7 +295,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: - logger.error("Certificate renewing for " + domain + " failed !") + logger.error("Certificate renewing for %s failed !", domain) logger.error(str(e)) if email: From 8ca5d59a96366c1ec285f5e400e8e34d01a4d3b2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:33:57 +0100 Subject: [PATCH 0036/1066] [mod] remove useless () --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c35d15f8e..9562a3a5f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -249,7 +249,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if (domainList == []): + if domainList == []: for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? @@ -258,10 +258,10 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals continue # Does it expires soon ? - if (force) or (status["validity"] <= validity_limit): + if force or status["validity"] <= validity_limit: domainList.append(domain) - if (len(domainList) == 0): + if len(domainList) == 0: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given @@ -275,7 +275,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals status = _get_status(domain) # Does it expires soon ? - if not (force or status["validity"] <= validity_limit): + if not force or status["validity"] <= validity_limit: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? From 65e9a4b6d8ac456b61a1bc9fdd59c012f10003b4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:34:30 +0100 Subject: [PATCH 0037/1066] [mod] uses logger string concatenation api --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9562a3a5f..f903c266a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -217,7 +217,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal # Actual install steps for domain in domainList: - logger.info("Now attempting install of certificate for domain " + domain + " !") + logger.info("Now attempting install of certificate for domain %s!", domain) try: if not no_checks: @@ -231,7 +231,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - logger.error("Certificate installation for %s failed !" % domain) + logger.error("Certificate installation for %s failed !", domain) logger.error(str(e)) From c1252120a13f720cd811f9379fbc86f91a17734f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:36:02 +0100 Subject: [PATCH 0038/1066] [mod] typo --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f903c266a..a547d2000 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -91,7 +91,7 @@ def certificate_status(auth, domainList, full=False): # Else, validate that yunohost knows the domains given else: for domain in domainList: - # Is it in Yunohost dmomain list ? + # Is it in Yunohost domain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) From 02b5ea62feff550a522d4dead957ee6ed048dc4e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:42:16 +0100 Subject: [PATCH 0039/1066] [mod] pep8 --- data/actionsmap/yunohost.yml | 6 ++--- src/yunohost/certificate.py | 48 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 4d0db97ec..8f08a98b8 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -313,7 +313,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains to check nargs: "*" --full: @@ -328,7 +328,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains for which to install the certificates nargs: "*" --force: @@ -349,7 +349,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains for which to renew the certificates nargs: "*" --force: diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a547d2000..c2f51270d 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -76,21 +76,21 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Status -def certificate_status(auth, domainList, full=False): +def certificate_status(auth, domain_list, full=False): """ Print the status of certificate for given domains (all by default) Keyword argument: - domainList -- Domains to be checked + domain_list -- Domains to be checked full -- Display more info about the certificates """ # If no domains given, consider all yunohost domains - if domainList == []: - domainList = yunohost.domain.domain_list(auth)['domains'] + if domain_list == []: + domain_list = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost domain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) @@ -102,7 +102,7 @@ def certificate_status(auth, domainList, full=False): headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] lines = [] - for domain in domainList: + for domain in domain_list: status = _get_status(domain) line = [] @@ -123,27 +123,27 @@ def certificate_status(auth, domainList, full=False): print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) -def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False): +def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) Keyword argument: - domainList -- Domains on which to install certificates + domain_list -- Domains on which to install certificates force -- Install even if current certificate is not self-signed no-check -- Disable some checks about the reachability of web server before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ if (self_signed): - certificate_install_selfsigned(domainList, force) + certificate_install_selfsigned(domain_list, force) else: - certificate_install_letsencrypt(auth, domainList, force, no_checks) + certificate_install_letsencrypt(auth, domain_list, force, no_checks) # Install self-signed -def certificate_install_selfsigned(domainList, force=False): - for domain in domainList: +def certificate_install_selfsigned(domain_list, force=False): + for domain in domain_list: # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) @@ -187,24 +187,24 @@ def certificate_install_selfsigned(domainList, force=False): _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) -def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): +def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if domainList == []: + if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: status = _get_status(domain) if status["CAtype"] != "Self-signed": continue - domainList.append(domain) + domain_list.append(domain) # Else, validate that yunohost knows the domains given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) @@ -215,7 +215,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps - for domain in domainList: + for domain in domain_list: logger.info("Now attempting install of certificate for domain %s!", domain) @@ -235,12 +235,12 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.error(str(e)) -def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): +def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) Keyword argument: - domainList -- Domains for which to renew the certificates + domain_list -- Domains for which to renew the certificates force -- Ignore the validity threshold (15 days) no-check -- Disable some checks about the reachability of web server before attempting the renewing @@ -249,7 +249,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if domainList == []: + if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? @@ -259,14 +259,14 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Does it expires soon ? if force or status["validity"] <= validity_limit: - domainList.append(domain) + domain_list.append(domain) - if len(domainList) == 0: + if len(domain_list) == 0: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: @@ -283,7 +283,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps - for domain in domainList: + for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) try: From 8b24ab73c25db0ce095c25bf9c03644990b47180 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:42:49 +0100 Subject: [PATCH 0040/1066] [mod] small opti, getting domain list can be slow --- src/yunohost/certificate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c2f51270d..73e9ae22b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -90,9 +90,10 @@ def certificate_status(auth, domain_list, full=False): domain_list = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] for domain in domain_list: # Is it in Yunohost domain list ? - if domain not in yunohost.domain.domain_list(auth)['domains']: + if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Get status for each domain, and prepare display using tabulate From 3b5cadb907f1ca00d1c3710221f57dca4befce41 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:43:32 +0100 Subject: [PATCH 0041/1066] [mod] realign stuff --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 73e9ae22b..81219eab5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -82,7 +82,7 @@ def certificate_status(auth, domain_list, full=False): Keyword argument: domain_list -- Domains to be checked - full -- Display more info about the certificates + full -- Display more info about the certificates """ # If no domains given, consider all yunohost domains @@ -130,10 +130,10 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si Keyword argument: domain_list -- Domains on which to install certificates - force -- Install even if current certificate is not self-signed - no-check -- Disable some checks about the reachability of web server + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server before attempting the install - self-signed -- Instal self-signed certificates instead of Let's Encrypt + self-signed -- Instal self-signed certificates instead of Let's Encrypt """ if (self_signed): certificate_install_selfsigned(domain_list, force) From 487e1d25888939b9d6af577ca21e2dfac305a2ac Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:54:45 +0100 Subject: [PATCH 0042/1066] [mod] pylint --- src/yunohost/certificate.py | 98 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81219eab5..b62118257 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -17,9 +17,7 @@ You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses -""" - -""" yunohost_certificate.py + yunohost_certificate.py Manage certificates, in particular Let's encrypt """ @@ -27,22 +25,22 @@ import os import sys import errno -import requests import shutil import pwd import grp import smtplib +import requests from OpenSSL import crypto from datetime import datetime from tabulate import tabulate from acme_tiny import get_crt as sign_certificate -import yunohost.domain - from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +import yunohost.domain + from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command @@ -135,7 +133,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ - if (self_signed): + if self_signed: certificate_install_selfsigned(domain_list, force) else: certificate_install_letsencrypt(auth, domain_list, force, no_checks) @@ -318,13 +316,13 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, exceptionMessage): +def _email_renewing_failed(domain, exception_message): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") - text = """ + text = """ At attempt for renewing the certificate for domain %s failed with the following error : @@ -337,7 +335,7 @@ investigate : -- Certificate Manager -""" % (domain, exceptionMessage, logs) +""" % (domain, exception_message, logs) message = """ From: %s @@ -397,7 +395,7 @@ def _fetch_and_enable_new_certificate(domain): # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) - domain_key_file = "%s/%s.pem" % (tmp_folder, domain) + domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) @@ -453,9 +451,9 @@ def _fetch_and_enable_new_certificate(domain): os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - statusSummaryCode = _get_status(domain)["summaryCode"] + status_summary_code = _get_status(domain)["summaryCode"] - if statusSummaryCode < 20: + if status_summary_code < 20: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") @@ -501,54 +499,54 @@ def _get_status(domain): traceback.print_exc(file=sys.stdout) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) - certSubject = cert.get_subject().CN - certIssuer = cert.get_issuer().CN - validUpTo = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") - daysRemaining = (validUpTo - datetime.now()).days + cert_subject = cert.get_subject().CN + cert_issuer = cert.get_issuer().CN + valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") + days_remaining = (valid_up_to - datetime.now()).days - CAtype = None - if certIssuer == _name_selfCA(): - CAtype = "Self-signed" + CA_type = None + if cert_issuer == _name_self_CA(): + CA_type = "Self-signed" - elif certIssuer.startswith("Let's Encrypt"): - CAtype = "Let's Encrypt" + elif cert_issuer.startswith("Let's Encrypt"): + CA_type = "Let's Encrypt" - elif certIssuer.startswith("Fake LE"): - CAtype = "Fake Let's Encrypt" + elif cert_issuer.startswith("Fake LE"): + CA_type = "Fake Let's Encrypt" else: - CAtype = "Other / Unknown" + CA_type = "Other / Unknown" # Unknown by default - statusSummaryCode = 0 + status_summary_code = 0 # Critical - if daysRemaining <= 0: - statusSummaryCode = -30 + if days_remaining <= 0: + status_summary_code = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif CAtype == "Self-signed" or CAtype == "Fake Let's Encrypt": - statusSummaryCode = -20 + elif CA_type == "Self-signed" or CA_type == "Fake Let's Encrypt": + status_summary_code = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif daysRemaining < validity_limit: - statusSummaryCode = -10 + elif days_remaining < validity_limit: + status_summary_code = -10 # CA not known, but still a valid certificate, so okay ! - elif CAtype == "Other / Unknown": - statusSummaryCode = 10 + elif CA_type == "Other / Unknown": + status_summary_code = 10 # Let's Encrypt, great ! - elif CAtype == "Let's Encrypt": - statusSummaryCode = 20 + elif CA_type == "Let's Encrypt": + status_summary_code = 20 return { "domain": domain, - "subject": certSubject, - "CAname": certIssuer, - "CAtype": CAtype, - "validity": daysRemaining, - "summaryCode": statusSummaryCode + "subject": cert_subject, + "CAname": cert_issuer, + "CAtype": CA_type, + "validity": days_remaining, + "summaryCode": status_summary_code } ############################################################################### @@ -562,11 +560,11 @@ def _generate_account_key(): _set_permissions(account_key_file, "root", "root", 0400) -def _generate_key(destinationPath): +def _generate_key(destination_path): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, key_size) - with open(destinationPath, "w") as f: + with open(destination_path, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) @@ -583,8 +581,8 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(cert_folder, domain) - dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = "%s-backup-%s" % (cert_folder_domain, dateTag) + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) shutil.copytree(cert_folder_domain, backup_folder) @@ -644,16 +642,16 @@ def _summary_code_to_string(code): return "CRITICAL" if code <= -20: - return "WARNING" + return "WARNING" if code <= -10: - return "Attention" + return "Attention" if code <= 0: - return "Unknown?" + return "Unknown?" if code <= 10: - return "Good" + return "Good" if code <= 20: return "Great!" @@ -661,7 +659,7 @@ def _summary_code_to_string(code): return "Unknown?" -def _name_selfCA(): +def _name_self_CA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN From 3fc2d45a6ab139a101a78205cdaecb1912eb6c5e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:40:05 +0100 Subject: [PATCH 0043/1066] [mod] remove useless comments --- src/yunohost/certificate.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b62118257..43c64aa34 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -47,8 +47,6 @@ from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') -# Misc stuff we need - cert_folder = "/etc/yunohost/certs/" tmp_folder = "/tmp/acme-challenge-private/" webroot_folder = "/tmp/acme-challenge-public/" @@ -71,8 +69,6 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Front-end stuff # ############################################################################### -# Status - def certificate_status(auth, domain_list, full=False): """ @@ -139,8 +135,6 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si certificate_install_letsencrypt(auth, domain_list, force, no_checks) -# Install self-signed - def certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: @@ -152,7 +146,6 @@ def certificate_install_selfsigned(domain_list, force=False): cert_folder_domain = os.path.join(cert_folder, domain) - # Create cert folder if it does not exists yet if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) From ac9f2643a755d5ebb2e2a11c4b5f08877b55e367 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:40:26 +0100 Subject: [PATCH 0044/1066] [mod] move a part of os.system calls to native shutil/os --- src/yunohost/certificate.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 43c64aa34..c8d55170c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -154,25 +154,32 @@ def certificate_install_selfsigned(domain_list, force=False): with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() + shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(cert_folder_domain, "openssl.cnf")) + # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' % (cert_folder_domain, ssl_dir, ssl_dir), - 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), - 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(cert_folder_domain, "ca.pem")) + shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(cert_folder_domain, "key.pem")) + shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(cert_folder_domain, "crt.pem")) + + # append ca.pem at the end of crt.pem + with open(os.path.join(cert_folder_domain, "ca.pem"), "r") as ca_pem: + with open(os.path.join(cert_folder_domain, "crt.pem"), "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) + _set_permissions(cert_folder_domain, "root", "root", 0755) _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) From a1b42e65f8ddc61902fdc6066b7ea59d7de77cd7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:49:40 +0100 Subject: [PATCH 0045/1066] [mod] remove useless variables --- src/yunohost/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e3d5ad202..83fc8614c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1041,14 +1041,11 @@ def app_ssowatconf(auth): continue # Check the file contains the ACME challenge uri - acme_uri = '/.well-known/acme-challenge' - - if not acme_uri in open(nginx_acme_challenge_conf_file).read(): + if not '/.well-known/acme-challenge' in open(nginx_acme_challenge_conf_file).read(): continue # If so, then authorize the ACME challenge uri to unprotected regex - regex = domain + "/%.well%-known/acme%-challenge/.*$" - unprotected_regex.append(regex) + unprotected_regex.append(domain + "/%.well%-known/acme%-challenge/.*$") conf_dict = { From bd41f0d4a2c1555ea63940db21713e73c9bfe823 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:55:04 +0100 Subject: [PATCH 0046/1066] [mod] os.path.join --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c8d55170c..e71d6cdeb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -487,7 +487,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - cert_file = cert_folder + "/" + domain + "/crt.pem" + cert_file = os.path.join(cert_folder, domain, "crt.pem") if not os.path.isfile(cert_file): return {} From 248508dcaa824018d031aeb75e447df6bb8fb95c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 14:00:42 +0100 Subject: [PATCH 0047/1066] [mod] do not uses tabulate, the api needs json --- src/yunohost/certificate.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e71d6cdeb..c8ea58d71 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -90,32 +90,19 @@ def certificate_status(auth, domain_list, full=False): if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - # Get status for each domain, and prepare display using tabulate - if not full: - headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] - else: - headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] - lines = [] + for domain in domain_list: status = _get_status(domain) + status["summaryCode"] = _summary_code_to_string(status["summaryCode"]) - line = [] - line.append(domain) + if not full: + del status["subject"] + del status["CAname"] - if full: - line.append(status["subject"]) + lines.append(status) - line.append(_summary_code_to_string(status["summaryCode"])) - line.append(status["CAtype"]) - - if full: - line.append(status["CAname"]) - - line.append(status["validity"]) - lines.append(line) - - print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) + return lines def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 299aa978f717a9d3e3f04b9f0a3cbb6d2d031caa Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 14:06:34 +0100 Subject: [PATCH 0048/1066] [mod] simplify code --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c8ea58d71..8c2662148 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -128,7 +128,7 @@ def certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if status != {} and status["summaryCode"] > 0 and not force: + if status and status["summaryCode"] > 0 and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) cert_folder_domain = os.path.join(cert_folder, domain) From 289eb014806de3d7c4609bb989c4abbbc002b175 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 15:21:23 +0100 Subject: [PATCH 0049/1066] [mod] small opti, getting domain list can be long --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8c2662148..9a3c564b0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -191,8 +191,8 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa # Else, validate that yunohost knows the domains given else: for domain in domain_list: - # Is it in Yunohost dmomain list ? - if domain not in yunohost.domain.domain_list(auth)['domains']: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] + if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Is it self-signed ? From fef1ca08c56d8b0c3530f861cca3982ad4bab619 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 30 Oct 2016 11:48:27 -0400 Subject: [PATCH 0050/1066] Changing name of cron job to be consistent with other yunohost crons, as requested by @opi --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9a3c564b0..19e9cea38 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -294,7 +294,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal ############################################################################### def _install_cron(): - cron_job_file = "/etc/cron.weekly/certificateRenewer" + cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") From 7f8aa4cc75d4e644bd80b148988cb4449ae51477 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 16:14:19 +0100 Subject: [PATCH 0051/1066] [mod] remove useless assign --- src/yunohost/certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 19e9cea38..aa8a77efb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -491,7 +491,6 @@ def _get_status(domain): valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") days_remaining = (valid_up_to - datetime.now()).days - CA_type = None if cert_issuer == _name_self_CA(): CA_type = "Self-signed" From 2d89964bc7ef5aae06f253dd67a5d387e99c676b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:15:06 +0100 Subject: [PATCH 0052/1066] [enh] include tracebak into error email --- src/yunohost/certificate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index aa8a77efb..94be8ee3e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -281,12 +281,17 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: + import traceback + from StringIO import StringIO + stack = StringIO() + traceback.print_exc(file=stack) logger.error("Certificate renewing for %s failed !", domain) + logger.error(stack.getvalue()) logger.error(str(e)) if email: logger.error("Sending email with details to root ...") - _email_renewing_failed(domain, e) + _email_renewing_failed(domain, e, stack.getvalue()) ############################################################################### @@ -303,7 +308,7 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, exception_message): +def _email_renewing_failed(domain, exception_message, stack): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain @@ -313,6 +318,7 @@ def _email_renewing_failed(domain, exception_message): At attempt for renewing the certificate for domain %s failed with the following error : +%s %s Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to @@ -322,7 +328,7 @@ investigate : -- Certificate Manager -""" % (domain, exception_message, logs) +""" % (domain, exception_message, stack, logs) message = """ From: %s From 5495281e83eaefd1b8aadc63ad65953efd253129 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:34:16 +0100 Subject: [PATCH 0053/1066] [mod] remove the summary code concept and switch to code/verbose duet instead --- src/yunohost/certificate.py | 110 ++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 94be8ee3e..680149506 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -94,11 +94,10 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: status = _get_status(domain) - status["summaryCode"] = _summary_code_to_string(status["summaryCode"]) if not full: del status["subject"] - del status["CAname"] + del status["CA_name"] lines.append(status) @@ -128,7 +127,7 @@ def certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if status and status["summaryCode"] > 0 and not force: + if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) cert_folder_domain = os.path.join(cert_folder, domain) @@ -183,7 +182,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa for domain in yunohost.domain.domain_list(auth)['domains']: status = _get_status(domain) - if status["CAtype"] != "Self-signed": + if status["CA_type"]["code"] != "self-signed": continue domain_list.append(domain) @@ -197,7 +196,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa # Is it self-signed ? status = _get_status(domain) - if not force and status["CAtype"] != "Self-signed": + if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps @@ -240,7 +239,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if status["CAtype"] != "Let's Encrypt": + if status["CA_type"]["code"] != "lets-encrypt": continue # Does it expires soon ? @@ -265,7 +264,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? - if status["CAtype"] != "Let's Encrypt": + if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps @@ -444,9 +443,9 @@ def _fetch_and_enable_new_certificate(domain): os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - status_summary_code = _get_status(domain)["summaryCode"] + status_summary = _get_status(domain)["summary"] - if status_summary_code < 20: + if status_summary["code"] != "great": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") @@ -498,47 +497,72 @@ def _get_status(domain): days_remaining = (valid_up_to - datetime.now()).days if cert_issuer == _name_self_CA(): - CA_type = "Self-signed" + CA_type = { + "code": "self-signed", + "verbose": "Self-signed", + } elif cert_issuer.startswith("Let's Encrypt"): - CA_type = "Let's Encrypt" + CA_type = { + "code": "lets-encrypt", + "verbose": "Let's Encrypt", + } elif cert_issuer.startswith("Fake LE"): - CA_type = "Fake Let's Encrypt" + CA_type = { + "code": "fake-lets-encrypt", + "verbose": "Fake Let's Encrypt", + } else: - CA_type = "Other / Unknown" + CA_type = { + "code": "other-unknown", + "verbose": "Other / Unknown", + } - # Unknown by default - status_summary_code = 0 - - # Critical if days_remaining <= 0: - status_summary_code = -30 + status_summary = { + "code": "critical", + "verbose": "CRITICAL", + } - # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif CA_type == "Self-signed" or CA_type == "Fake Let's Encrypt": - status_summary_code = -20 + elif CA_type["code"] in ("self-signed","fake-lets-encrypt"): + status_summary = { + "code": "warning", + "verbose": "WARNING", + } - # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) elif days_remaining < validity_limit: - status_summary_code = -10 + status_summary = { + "code": "attention", + "verbose": "About to expire", + } - # CA not known, but still a valid certificate, so okay ! - elif CA_type == "Other / Unknown": - status_summary_code = 10 + elif CA_type["code"] == "other-unknown": + status_summary = { + "code": "good", + "verbose": "Good", + } - # Let's Encrypt, great ! - elif CA_type == "Let's Encrypt": - status_summary_code = 20 + elif CA_type["code"] == "lets-encrypt": + status_summary = { + "code": "great", + "verbose": "Great!", + } + + else: + status_summary = { + "code": "unknown", + "verbose": "Unknown?", + } return { "domain": domain, "subject": cert_subject, - "CAname": cert_issuer, - "CAtype": CA_type, + "CA_name": cert_issuer, + "CA_type": CA_type, "validity": days_remaining, - "summaryCode": status_summary_code + "summary": status_summary, } ############################################################################### @@ -629,28 +653,6 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True -def _summary_code_to_string(code): - if code <= -30: - return "CRITICAL" - - if code <= -20: - return "WARNING" - - if code <= -10: - return "Attention" - - if code <= 0: - return "Unknown?" - - if code <= 10: - return "Good" - - if code <= 20: - return "Great!" - - return "Unknown?" - - def _name_self_CA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN From 0e7552263d6fe5d4b00f32339656fcc1da8671a2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:34:32 +0100 Subject: [PATCH 0054/1066] [mod] I only need to reload nginx, not restart it --- src/yunohost/certificate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 680149506..91dd3fc2a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -450,9 +450,11 @@ def _fetch_and_enable_new_certificate(domain): logger.info("Restarting services...") - for service in ("nginx", "postfix", "dovecot", "metronome"): + for service in ("postfix", "dovecot", "metronome"): _run_service_command("restart", service) + _run_service_command("reload", "nginx") + def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request From 11d785a22105c2937410d92f40ccff3e4d3c51d2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:05:37 +0100 Subject: [PATCH 0055/1066] [mod] remove useless import --- src/yunohost/certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 91dd3fc2a..826b70a89 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,7 +33,6 @@ import requests from OpenSSL import crypto from datetime import datetime -from tabulate import tabulate from acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError From f1188782e29ea001f435224bf728afcdef9208c0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:08:34 +0100 Subject: [PATCH 0056/1066] [mod] top level constants should be upper case (pep8) --- src/yunohost/certificate.py | 74 ++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 826b70a89..0b583f805 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -46,23 +46,23 @@ from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') -cert_folder = "/etc/yunohost/certs/" -tmp_folder = "/tmp/acme-challenge-private/" -webroot_folder = "/tmp/acme-challenge-public/" +CERT_FOLDER = "/etc/yunohost/certs/" +TMP_FOLDER = "/tmp/acme-challenge-private/" +WEBROOT_FOLDER = "/tmp/acme-challenge-public/" -selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" -account_key_file = "/etc/yunohost/letsencrypt_account.pem" +SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" +ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" -key_size = 2048 +KEY_SIZE = 2048 -validity_limit = 15 # days +VALIDITY_LIMIT = 15 # days # For tests -#certification_authority = "https://acme-staging.api.letsencrypt.org" +#CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" # For prod -certification_authority = "https://acme-v01.api.letsencrypt.org" +CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" -intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" +INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" ############################################################################### # Front-end stuff # @@ -129,7 +129,7 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = os.path.join(cert_folder, domain) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) @@ -172,7 +172,7 @@ def certificate_install_selfsigned(domain_list, force=False): def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): - if not os.path.exists(account_key_file): + if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed @@ -242,7 +242,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal continue # Does it expires soon ? - if force or status["validity"] <= validity_limit: + if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) if len(domain_list) == 0: @@ -259,7 +259,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal status = _get_status(domain) # Does it expires soon ? - if not force or status["validity"] <= validity_limit: + if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? @@ -350,7 +350,7 @@ location '/.well-known/acme-challenge' default_type "text/plain"; alias %s; } - ''' % webroot_folder + ''' % WEBROOT_FOLDER # Write the conf if os.path.exists(nginx_conf_file): @@ -374,36 +374,36 @@ def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not os.path.exists(webroot_folder): - os.makedirs(webroot_folder) + if not os.path.exists(WEBROOT_FOLDER): + os.makedirs(WEBROOT_FOLDER) - if not os.path.exists(tmp_folder): - os.makedirs(tmp_folder) + if not os.path.exists(TMP_FOLDER): + os.makedirs(TMP_FOLDER) - _set_permissions(webroot_folder, "root", "www-data", 0650) - _set_permissions(tmp_folder, "root", "root", 0640) + _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) + _set_permissions(TMP_FOLDER, "root", "root", 0640) # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) - domain_key_file = "%s/%s.pem" % (tmp_folder, domain) + domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) - _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) + _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = "%s/%s.csr" % (tmp_folder, domain) + domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - signed_certificate = sign_certificate(account_key_file, + signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, domain_csr_file, - webroot_folder, + WEBROOT_FOLDER, log=logger, - CA=certification_authority) + CA=CERTIFICATION_AUTHORITY) - intermediate_certificate = requests.get(intermediate_certificate_url).text + intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text # Now save the key and signed certificate logger.info("Saving the key and signed certificate...") @@ -411,7 +411,7 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s.%s" % (cert_folder, domain, date_tag) + new_cert_folder = "%s/%s.%s" % (CERT_FOLDER, domain, date_tag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -431,7 +431,7 @@ def _fetch_and_enable_new_certificate(domain): logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = os.path.join(cert_folder, domain) + live_link = os.path.join(CERT_FOLDER, domain) if not os.path.islink(live_link): shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) @@ -480,7 +480,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - cert_file = os.path.join(cert_folder, domain, "crt.pem") + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): return {} @@ -533,7 +533,7 @@ def _get_status(domain): "verbose": "WARNING", } - elif days_remaining < validity_limit: + elif days_remaining < VALIDITY_LIMIT: status_summary = { "code": "attention", "verbose": "About to expire", @@ -573,13 +573,13 @@ def _get_status(domain): def _generate_account_key(): logger.info("Generating account key ...") - _generate_key(account_key_file) - _set_permissions(account_key_file, "root", "root", 0400) + _generate_key(ACCOUNT_KEY_FILE) + _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400) def _generate_key(destination_path): k = crypto.PKey() - k.generate_key(crypto.TYPE_RSA, key_size) + k.generate_key(crypto.TYPE_RSA, KEY_SIZE) with open(destination_path, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) @@ -596,7 +596,7 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain " + domain) - cert_folder_domain = os.path.join(cert_folder, domain) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) @@ -655,7 +655,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) return cert.get_subject().CN From 718011c0ee01137d51fea265ce0b0eb1c435d7af Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:48:52 +0100 Subject: [PATCH 0057/1066] [mod] use logger string concatenation api --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0b583f805..3e80c48f5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -473,7 +473,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Save the request in tmp folder csr_file = output_folder + domain + ".csr" - logger.info("Saving to " + csr_file + " .") + logger.info("Saving to %s.", csr_file) with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) From bbbac248b6c0f1ce231e89b6454b439609e3d012 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:50:36 +0100 Subject: [PATCH 0058/1066] [mod] use logger string concatenation api --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3e80c48f5..5d643062f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -594,7 +594,7 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): - logger.info("Backuping existing certificate for domain " + domain) + logger.info("Backuping existing certificate for domain %s", domain) cert_folder_domain = os.path.join(CERT_FOLDER, domain) From 59500c3acc8d6c357ab84ffe684a9a101b9a7e89 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 Nov 2016 08:37:47 -0400 Subject: [PATCH 0059/1066] Adding acme-tiny as a dependency in vendor folder --- src/yunohost/certificate.py | 2 +- src/yunohost/vendor/__init__.py | 0 src/yunohost/vendor/acme_tiny/__init__.py | 0 src/yunohost/vendor/acme_tiny/acme_tiny.py | 198 +++++++++++++++++++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/vendor/__init__.py create mode 100644 src/yunohost/vendor/acme_tiny/__init__.py create mode 100644 src/yunohost/vendor/acme_tiny/acme_tiny.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5d643062f..636a5e72c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,7 +33,7 @@ import requests from OpenSSL import crypto from datetime import datetime -from acme_tiny import get_crt as sign_certificate +from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/vendor/__init__.py b/src/yunohost/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/__init__.py b/src/yunohost/vendor/acme_tiny/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py new file mode 100644 index 000000000..0f4cb431f --- /dev/null +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging +try: + from urllib.request import urlopen # Python 3 +except ImportError: + from urllib2 import urlopen # Python 2 + +#DEFAULT_CA = "https://acme-staging.api.letsencrypt.org" +DEFAULT_CA = "https://acme-v01.api.letsencrypt.org" + +LOGGER = logging.getLogger(__name__) +LOGGER.addHandler(logging.StreamHandler()) +LOGGER.setLevel(logging.INFO) + +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): + # helper function base64 encode for jose spec + def _b64(b): + return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + + # parse account key to get public key + log.info("Parsing account key...") + proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + header = { + "alg": "RS256", + "jwk": { + "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + } + accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + + # helper function make signed requests + def _send_signed_request(url, payload): + payload64 = _b64(json.dumps(payload).encode('utf8')) + protected = copy.deepcopy(header) + protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce'] + protected64 = _b64(json.dumps(protected).encode('utf8')) + proc = subprocess.Popen(["openssl", "dgst", "-sha256", "-sign", account_key], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8')) + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + data = json.dumps({ + "header": header, "protected": protected64, + "payload": payload64, "signature": _b64(out), + }) + try: + resp = urlopen(url, data.encode('utf8')) + return resp.getcode(), resp.read() + except IOError as e: + return getattr(e, "code", None), getattr(e, "read", e.__str__)() + + # find domains + log.info("Parsing CSR...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("Error loading {0}: {1}".format(csr, err)) + domains = set([]) + common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) + if common_name is not None: + domains.add(common_name.group(1)) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.startswith("DNS:"): + domains.add(san[4:]) + + # get the certificate domains and expiration + log.info("Registering account...") + code, result = _send_signed_request(CA + "/acme/new-reg", { + "resource": "new-reg", + "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", + }) + if code == 201: + log.info("Registered!") + elif code == 409: + log.info("Already registered!") + else: + raise ValueError("Error registering: {0} {1}".format(code, result)) + + # verify each domain + for domain in domains: + log.info("Verifying {0}...".format(domain)) + + # get new challenge + code, result = _send_signed_request(CA + "/acme/new-authz", { + "resource": "new-authz", + "identifier": {"type": "dns", "value": domain}, + }) + if code != 201: + raise ValueError("Error requesting challenges: {0} {1}".format(code, result)) + + # make the challenge file + challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] if c['type'] == "http-01"][0] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = "{0}.{1}".format(token, thumbprint) + wellknown_path = os.path.join(acme_dir, token) + with open(wellknown_path, "w") as wellknown_file: + wellknown_file.write(keyauthorization) + + # check that the file is in place + wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) + try: + resp = urlopen(wellknown_url) + resp_data = resp.read().decode('utf8').strip() + assert resp_data == keyauthorization + except (IOError, AssertionError): + os.remove(wellknown_path) + raise ValueError("Wrote file to {0}, but couldn't download {1}".format( + wellknown_path, wellknown_url)) + + # notify challenge are met + code, result = _send_signed_request(challenge['uri'], { + "resource": "challenge", + "keyAuthorization": keyauthorization, + }) + if code != 202: + raise ValueError("Error triggering challenge: {0} {1}".format(code, result)) + + # wait for challenge to be verified + while True: + try: + resp = urlopen(challenge['uri']) + challenge_status = json.loads(resp.read().decode('utf8')) + except IOError as e: + raise ValueError("Error checking challenge: {0} {1}".format( + e.code, json.loads(e.read().decode('utf8')))) + if challenge_status['status'] == "pending": + time.sleep(2) + elif challenge_status['status'] == "valid": + log.info("{0} verified!".format(domain)) + os.remove(wellknown_path) + break + else: + raise ValueError("{0} challenge did not pass: {1}".format( + domain, challenge_status)) + + # get the new certificate + log.info("Signing certificate...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + csr_der, err = proc.communicate() + code, result = _send_signed_request(CA + "/acme/new-cert", { + "resource": "new-cert", + "csr": _b64(csr_der), + }) + if code != 201: + raise ValueError("Error signing certificate: {0} {1}".format(code, result)) + + # return signed certificate! + log.info("Certificate signed!") + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))) + +def main(argv): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent("""\ + This script automates the process of getting a signed TLS certificate from + Let's Encrypt using the ACME protocol. It will need to be run on your server + and have access to your private account key, so PLEASE READ THROUGH IT! It's + only ~200 lines, so it won't take long. + + ===Example Usage=== + python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed.crt + =================== + + ===Example Crontab Renewal (once per month)=== + 0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed.crt 2>> /var/log/acme_tiny.log + ============================================== + """) + ) + parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key") + parser.add_argument("--csr", required=True, help="path to your certificate signing request") + parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory") + parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors") + parser.add_argument("--ca", default=DEFAULT_CA, help="certificate authority, default is Let's Encrypt") + + args = parser.parse_args(argv) + LOGGER.setLevel(args.quiet or LOGGER.level) + signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca) + sys.stdout.write(signed_crt) + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) From f3461b36364a3613e5d50648ce52c355f5d466fa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 12:24:59 -0500 Subject: [PATCH 0060/1066] Fixing typo in arguments to match actionmap --- src/yunohost/domain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 1a296ee97..aadd9086d 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -257,16 +257,16 @@ def domain_dns_conf(domain, ttl=None): return result -def domain_cert_status(auth, domainList, full=False): - return yunohost.certificate.certificate_status(auth, domainList, full) +def domain_cert_status(auth, domain_list, full=False): + return yunohost.certificate.certificate_status(auth, domain_list, full) -def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): - return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) +def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False): + return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed) -def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): - return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) +def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False): + return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email) def get_public_ip(protocol=4): From 56cd9610f353c030e6a3c62fcd34879ce907c9e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 14:02:59 -0500 Subject: [PATCH 0061/1066] Improving CLI display of cert-status --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 636a5e72c..78c0c2ed5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -100,7 +100,7 @@ def certificate_status(auth, domain_list, full=False): lines.append(status) - return lines + return { "certificates" : lines } def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 90e63edcfe5af42b6655a582157962f02c7592bf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 14:19:28 -0500 Subject: [PATCH 0062/1066] Improving/cleaning CLI display of cert-status --- src/yunohost/certificate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 78c0c2ed5..d2a67495a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -89,7 +89,7 @@ def certificate_status(auth, domain_list, full=False): if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - lines = [] + certificates = {} for domain in domain_list: status = _get_status(domain) @@ -97,10 +97,13 @@ def certificate_status(auth, domain_list, full=False): if not full: del status["subject"] del status["CA_name"] + status["CA_type"] = status["CA_type"]["verbose"] + status["summary"] = status["summary"]["verbose"] - lines.append(status) + del status["domain"] + certificates[domain] = status - return { "certificates" : lines } + return { "certificates" : certificates } def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 4ddc3aac369cb20ba672553748e04eb218c33b34 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 12:09:02 -0500 Subject: [PATCH 0063/1066] Display a warning message when letsencrypt is installed, suggesting commands to migrate --- locales/en.json | 3 ++- src/yunohost/certificate.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index bdfd1b14a..1a93b72ff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,6 @@ "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" + "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", + "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d2a67495a..f9f5784a0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -40,7 +40,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.app import app_ssowatconf +from yunohost.app import app_ssowatconf, app_list from yunohost.service import _run_service_command @@ -78,6 +78,9 @@ def certificate_status(auth, domain_list, full=False): full -- Display more info about the certificates """ + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + # If no domains given, consider all yunohost domains if domain_list == []: domain_list = yunohost.domain.domain_list(auth)['domains'] @@ -107,6 +110,7 @@ def certificate_status(auth, domain_list, full=False): def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): + """ Install a Let's Encrypt certificate for given domains (all by default) @@ -117,6 +121,11 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ + + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + + if self_signed: certificate_install_selfsigned(domain_list, force) else: @@ -234,6 +243,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal email -- Emails root if some renewing failed """ + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + # If no domains given, consider all yunohost domains with Let's Encrypt # certificates if domain_list == []: @@ -299,6 +311,29 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Back-end stuff # ############################################################################### +def _check_old_letsencrypt_app(): + + installedAppIds = [ app["id"] for app in yunohost.app.app_list(installed=True)["apps"] ] + if ("letsencrypt" not in installedAppIds) : + return + + logger.warning(" ") + logger.warning("Yunohost detected that the 'letsencrypt' app is installed, ") + logger.warning("which conflits with the new certificate management features") + logger.warning("directly integrated in Yunohost. If you wish to use these ") + logger.warning("new features, please run the following commands to migrate ") + logger.warning("your installation :") + logger.warning(" ") + logger.warning(" yunohost app remove letsencrypt") + logger.warning(" yunohost domain cert-install") + logger.warning(" ") + logger.warning("N.B. : this will attempt to re-install certificates for ") + logger.warning("all domains with a Let's Encrypt certificate or self-signed") + logger.warning("certificate.") + logger.warning(" ") + + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + def _install_cron(): cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" From cbc71f2530853f146cdebbd33498574869be1476 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 12:55:01 -0500 Subject: [PATCH 0064/1066] Adding a check for the presence of the ssowat header when checking domain is accessible through http --- src/yunohost/certificate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f9f5784a0..19758c33d 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -685,7 +685,10 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - requests.head("http://" + ip, headers={"Host": domain}) + r = requests.head("http://" + ip, headers={"Host": domain}) + # Check we got the ssowat header in the response + if ("x-sso-wat" not in r.headers.keys()) : + return False except Exception: return False From 6bfe1c80833423b0530f368fddcc05ccdf304629 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 20:59:55 -0500 Subject: [PATCH 0065/1066] Check that the DNS A record matches the global IP now using dnspython and FDN's DNS --- locales/en.json | 6 ++---- src/yunohost/certificate.py | 31 +++++++++---------------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/locales/en.json b/locales/en.json index 1a93b72ff..627c11b52 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,10 +245,8 @@ "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", - "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}), reason: {reason:s}. Use --no-checks to disable checks.", - "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", - "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", - "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", + "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 19758c33d..02ea73af8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,6 +31,8 @@ import grp import smtplib import requests +import dns.resolver + from OpenSSL import crypto from datetime import datetime from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate @@ -656,29 +658,14 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - result = requests.get("https://dns-api.org/A/" + domain) - except Exception as exception: - import traceback - traceback.print_exc(file=sys.stdout) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) + resolver = dns.resolver.Resolver() + # These are FDN's DNS + resolver.nameservers = [ "80.67.169.12", "80.67.169.40" ] + answers = resolver.query(domain, "A") + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) - try: - dns_ip = result.json() - except Exception as exception: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - if len(dns_ip) == 0: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - dns_ip = dns_ip[0] - - if dns_ip.get("error") == "NXDOMAIN": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - - if "value" not in dns_ip: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - dns_ip = dns_ip["value"] + dns_ip = answers[0] return dns_ip == public_ip From e2e1fce44e3f2ec79f2fbd31bde71e10b6230900 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 21:09:06 -0500 Subject: [PATCH 0066/1066] Fixing previous commit --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 02ea73af8..e648e5b13 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -665,7 +665,7 @@ def _dns_ip_match_public_ip(public_ip, domain): except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) - dns_ip = answers[0] + dns_ip = str(answers[0]) return dns_ip == public_ip From a57ebfc4e65897c0405fe21c434e5f4086c5d004 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 21:11:36 -0500 Subject: [PATCH 0067/1066] Use 3072 bits keys --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e648e5b13..3e999c2d1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -55,7 +55,7 @@ WEBROOT_FOLDER = "/tmp/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" -KEY_SIZE = 2048 +KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days From 109cbf764147dba0c6dd1041d776cad8c74f20a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:22:13 -0500 Subject: [PATCH 0068/1066] Backuping existing certificate (if any) also for self-signed generation --- locales/en.json | 1 + src/yunohost/certificate.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 627c11b52..2dec12706 100644 --- a/locales/en.json +++ b/locales/en.json @@ -248,6 +248,7 @@ "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", + "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3e999c2d1..430f6d4d2 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -143,10 +143,18 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) - if not os.path.exists(cert_folder_domain): - os.makedirs(cert_folder_domain) + # Backup existing certificate / folder + if os.path.exists(cert_folder_domain) : + if not os.path.islink(cert_folder_domain): + _backup_current_cert(domain) + shutil.rmtree(cert_folder_domain) + else : + os.remove(cert_folder_domain) + + os.makedirs(cert_folder_domain) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' @@ -184,6 +192,16 @@ def certificate_install_selfsigned(domain_list, force=False): _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) + # Check new status indicate a recently created self-signed certificate, + status = _get_status(domain) + + if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: + logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + else : + logger.error("Installation of self-signed certificate installation for %s failed !", domain) + logger.error(str(e)) + + def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): @@ -474,7 +492,7 @@ def _fetch_and_enable_new_certificate(domain): live_link = os.path.join(CERT_FOLDER, domain) if not os.path.islink(live_link): - shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + shutil.rmtree(live_link) # Hopefully that's not too dangerous (directory should have been backuped before calling this command) elif os.path.lexists(live_link): os.remove(live_link) From e1539297a5d7c23024402e68ff560f57f98dd28c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:25:15 -0500 Subject: [PATCH 0069/1066] Ignore messy stderr from openssl commands during self-signed cert generation --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 430f6d4d2..56f6773bd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -167,9 +167,9 @@ def certificate_install_selfsigned(domain_list, force=False): # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' + 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' % (cert_folder_domain, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' + 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' % (cert_folder_domain, ssl_dir, ssl_dir), ] From 80ebaa6895dd9025fa8b6d6bf19179e93b747456 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:34:19 -0500 Subject: [PATCH 0070/1066] Have a subdirectory for cert backups, to not flood /etc/yunohost/certs/ --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 56f6773bd..0788f9e14 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -657,7 +657,7 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(CERT_FOLDER, domain) date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) + backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag) shutil.copytree(cert_folder_domain, backup_folder) From 4e9a2c050de2b89c9d10016c09cc5c4975cc1bfd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 23:29:10 -0500 Subject: [PATCH 0071/1066] Cleaning / reorganizing the way certificates are stored and enabled --- src/yunohost/certificate.py | 91 +++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0788f9e14..00ab3dbb8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -144,53 +144,48 @@ def certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = os.path.join(CERT_FOLDER, domain) + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) - # Backup existing certificate / folder - if os.path.exists(cert_folder_domain) : - if not os.path.islink(cert_folder_domain): - _backup_current_cert(domain) - shutil.rmtree(cert_folder_domain) - else : - os.remove(cert_folder_domain) - - os.makedirs(cert_folder_domain) + os.makedirs(new_cert_folder) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() - shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(cert_folder_domain, "openssl.cnf")) + shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(new_cert_folder, "openssl.cnf")) # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, new_cert_folder), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' - % (cert_folder_domain, ssl_dir, ssl_dir), + % (new_cert_folder, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' - % (cert_folder_domain, ssl_dir, ssl_dir), + % (new_cert_folder, ssl_dir, ssl_dir), ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) - os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(cert_folder_domain, "ca.pem")) - shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(cert_folder_domain, "key.pem")) - shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(cert_folder_domain, "crt.pem")) + os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(new_cert_folder, "ca.pem")) + shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(new_cert_folder, "key.pem")) + shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(new_cert_folder, "crt.pem")) # append ca.pem at the end of crt.pem - with open(os.path.join(cert_folder_domain, "ca.pem"), "r") as ca_pem: - with open(os.path.join(cert_folder_domain, "crt.pem"), "a") as crt_pem: + with open(os.path.join(new_cert_folder, "ca.pem"), "r") as ca_pem: + with open(os.path.join(new_cert_folder, "crt.pem"), "a") as crt_pem: crt_pem.write("\n") crt_pem.write(ca_pem.read()) - _set_permissions(cert_folder_domain, "root", "root", 0755) - _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) + _set_permissions(new_cert_folder, "root", "root", 0755) + _set_permissions(os.path.join(new_cert_folder, "key.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(new_cert_folder, "crt.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(new_cert_folder, "openssl.cnf"), "root", "root", 0600) + + _enable_certificate(domain, new_cert_folder) # Check new status indicate a recently created self-signed certificate, status = _get_status(domain) @@ -239,7 +234,6 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa if not no_checks: _check_domain_is_correctly_configured(domain) - _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) _install_cron() @@ -308,7 +302,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: _check_domain_is_correctly_configured(domain) - _backup_current_cert(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -469,7 +462,7 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s.%s" % (CERT_FOLDER, domain, date_tag) + new_cert_folder = "%s/%s-history/%s-letsencrypt" % (CERT_FOLDER, domain, date_tag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -486,18 +479,7 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(domain_cert_file, "root", "metronome", 0640) - logger.info("Enabling the new certificate...") - - # Replace (if necessary) the link or folder for live cert - live_link = os.path.join(CERT_FOLDER, domain) - - if not os.path.islink(live_link): - shutil.rmtree(live_link) # Hopefully that's not too dangerous (directory should have been backuped before calling this command) - - elif os.path.lexists(live_link): - os.remove(live_link) - - os.symlink(new_cert_folder, live_link) + _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good status_summary = _get_status(domain)["summary"] @@ -505,13 +487,6 @@ def _fetch_and_enable_new_certificate(domain): if status_summary["code"] != "great": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) - logger.info("Restarting services...") - - for service in ("postfix", "dovecot", "metronome"): - _run_service_command("restart", service) - - _run_service_command("reload", "nginx") - def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request @@ -651,6 +626,32 @@ def _set_permissions(path, user, group, permissions): os.chmod(path, permissions) +def _enable_certificate(domain, new_cert_folder) : + logger.info("Enabling the certificate for domain %s ...", domain) + + live_link = os.path.join(CERT_FOLDER, domain) + + # If a live link (or folder) already exists + if os.path.exists(live_link) : + # If it's not a link ... expect if to be a folder + if not os.path.islink(live_link): + # Backup it and remove it + _backup_current_cert(domain) + shutil.rmtree(live_link) + # Else if it's a link, simply delete it + elif os.path.lexists(live_link): + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + logger.info("Restarting services...") + + for service in ("postfix", "dovecot", "metronome"): + _run_service_command("restart", service) + + _run_service_command("reload", "nginx") + + def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain %s", domain) From 937cccf8131aa48158722dcb58dd0182174b342c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 9 Nov 2016 08:38:52 +0100 Subject: [PATCH 0072/1066] [mod] remove useless import --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 00ab3dbb8..02ccbd380 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,7 +42,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.app import app_ssowatconf, app_list +from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command From 9e5b2743db03e94c8c6cb45d6693823425e0127e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 9 Nov 2016 08:38:58 +0100 Subject: [PATCH 0073/1066] [mod] pep8 --- src/yunohost/certificate.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 02ccbd380..81a459482 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -108,11 +108,11 @@ def certificate_status(auth, domain_list, full=False): del status["domain"] certificates[domain] = status - return { "certificates" : certificates } + return {"certificates" : certificates} def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): - + """ Install a Let's Encrypt certificate for given domains (all by default) @@ -126,7 +126,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si # Check if old letsencrypt_ynh is installed _check_old_letsencrypt_app() - + if self_signed: certificate_install_selfsigned(domain_list, force) @@ -325,9 +325,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal ############################################################################### def _check_old_letsencrypt_app(): + installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] - installedAppIds = [ app["id"] for app in yunohost.app.app_list(installed=True)["apps"] ] - if ("letsencrypt" not in installedAppIds) : + if "letsencrypt" not in installedAppIds: return logger.warning(" ") @@ -344,9 +344,10 @@ def _check_old_letsencrypt_app(): logger.warning("all domains with a Let's Encrypt certificate or self-signed") logger.warning("certificate.") logger.warning(" ") - + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + def _install_cron(): cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" @@ -679,9 +680,9 @@ def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() # These are FDN's DNS - resolver.nameservers = [ "80.67.169.12", "80.67.169.40" ] + resolver.nameservers = ["80.67.169.12", "80.67.169.40"] answers = resolver.query(domain, "A") - except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) dns_ip = str(answers[0]) @@ -693,7 +694,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): try: r = requests.head("http://" + ip, headers={"Host": domain}) # Check we got the ssowat header in the response - if ("x-sso-wat" not in r.headers.keys()) : + if "x-sso-wat" not in r.headers.keys(): return False except Exception: return False From a85c79ef320190d7f80136002798eb9adbfa8192 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Nov 2016 19:41:23 -0500 Subject: [PATCH 0074/1066] Refactored the self-signed cert generation, some steps were overly complicated for no reason --- src/yunohost/certificate.py | 75 +++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81a459482..d03d7d55b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -30,6 +30,7 @@ import pwd import grp import smtplib import requests +import subprocess import dns.resolver @@ -143,58 +144,74 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - + # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) - os.makedirs(new_cert_folder) - - # Get serial + original_ca_file = '/etc/ssl/certs/ca-yunohost_crt.pem' ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - with open(os.path.join(ssl_dir, 'serial'), 'r') as f: - serial = f.readline().rstrip() + conf_template = os.path.join(ssl_dir, "openssl.cnf") - shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(new_cert_folder, "openssl.cnf")) + csr_file = os.path.join(ssl_dir, "certs", "yunohost_csr.pem") + conf_file = os.path.join(new_cert_folder, "openssl.cnf") + key_file = os.path.join(new_cert_folder, "key.pem") + crt_file = os.path.join(new_cert_folder, "crt.pem") + ca_file = os.path.join(new_cert_folder, "ca.pem") + + # Create output folder for new certificate stuff + os.makedirs(new_cert_folder) + + # Create our conf file, based on template, replacing the occurences of + # "yunohost.org" with the given domain + with open(conf_file, "w") as f : + with open(conf_template, "r") as template : + for line in template : + f.write(line.replace("yunohost.org", domain)) - # FIXME : should refactor this to avoid so many os.system() calls... - # We should be able to do all this using OpenSSL.crypto and os/shutil - command_list = [ - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, new_cert_folder), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' - % (new_cert_folder, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' - % (new_cert_folder, ssl_dir, ssl_dir), - ] + # Use OpenSSL command line to create a certificate signing request, + # and self-sign the cert + commands = [] + commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + % (conf_file, csr_file, key_file)) + commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" + % (conf_file, csr_file, crt_file)) - for command in command_list: - if os.system(command) != 0: + for command in commands : + p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + if p.returncode != 0: + logger.warning(out) raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + else : + logger.info(out) - os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(new_cert_folder, "ca.pem")) - shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(new_cert_folder, "key.pem")) - shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(new_cert_folder, "crt.pem")) + # Link the CA cert (not sure it's actually needed in practice though, + # since we append it at the end of crt.pem. For instance for Let's + # Encrypt certs, we only need the crt.pem and key.pem) + os.symlink(original_ca_file, ca_file) - # append ca.pem at the end of crt.pem - with open(os.path.join(new_cert_folder, "ca.pem"), "r") as ca_pem: - with open(os.path.join(new_cert_folder, "crt.pem"), "a") as crt_pem: + # Append ca.pem at the end of crt.pem + with open(ca_file, "r") as ca_pem: + with open(crt_file, "a") as crt_pem: crt_pem.write("\n") crt_pem.write(ca_pem.read()) + # Set appropriate permissions _set_permissions(new_cert_folder, "root", "root", 0755) - _set_permissions(os.path.join(new_cert_folder, "key.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(new_cert_folder, "crt.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(new_cert_folder, "openssl.cnf"), "root", "root", 0600) + _set_permissions(key_file, "root", "metronome", 0640) + _set_permissions(crt_file, "root", "metronome", 0640) + _set_permissions(conf_file, "root", "root", 0600) + # Actually enable the certificate we created _enable_certificate(domain, new_cert_folder) - # Check new status indicate a recently created self-signed certificate, + # Check new status indicate a recently created self-signed certificate status = _get_status(domain) if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) else : logger.error("Installation of self-signed certificate installation for %s failed !", domain) - logger.error(str(e)) From 11c626881a7ac0dcdf27d587899ba0662221d1a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Nov 2016 20:19:56 -0500 Subject: [PATCH 0075/1066] Adding other DNS resolvers from FFDN --- src/yunohost/certificate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d03d7d55b..8feb9a771 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -696,8 +696,16 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() - # These are FDN's DNS - resolver.nameservers = ["80.67.169.12", "80.67.169.40"] + resolver.nameservers = [] + # FFDN DNS resolvers + # See https://www.ffdn.org/wiki/doku.php?id=formations:dns + resolver.nameservers.append("80.67.169.12") # FDN + resolver.nameservers.append("80.67.169.40") # + resolver.nameservers.append("89.234.141.66") # ARN + resolver.nameservers.append("141.255.128.100") # Aquilenet + resolver.nameservers.append("141.255.128.101") # + resolver.nameservers.append("89.234.186.18") # Grifon + resolver.nameservers.append("80.67.188.188") # LDN answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) From cf3e28786cf829bc042226283399699195e21d79 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 00:34:20 +0200 Subject: [PATCH 0076/1066] [mod] remove useless line --- src/yunohost/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index caa38e95b..0a7c92947 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -108,7 +108,6 @@ def app_fetchlist(url=None, name=None): # Rename fetched temp list os.rename('%s.tmp' % list_file, list_file) - os.system("touch /etc/cron.d/yunohost-applist-%s" % name) os.system("echo '00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1' >/etc/cron.d/yunohost-applist-%s" % (url, name, name)) logger.success(m18n.n('appslist_fetched')) From c4cecfcea5f51f1f9fb410358386eb5a6782cdb2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 00:35:23 +0200 Subject: [PATCH 0077/1066] [mod] use python instead of os.system --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0a7c92947..291d8c77a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -108,7 +108,7 @@ def app_fetchlist(url=None, name=None): # Rename fetched temp list os.rename('%s.tmp' % list_file, list_file) - os.system("echo '00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1' >/etc/cron.d/yunohost-applist-%s" % (url, name, name)) + open("/etc/cron.d/yunohost-applist-%s" % name, "w").write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) logger.success(m18n.n('appslist_fetched')) From d9081bddef1b2129ad42b05b28a26cc7680f7d51 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 00:41:32 +0200 Subject: [PATCH 0078/1066] [mod] directly use python to retreive json list --- src/yunohost/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 291d8c77a..3b2a6d24f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -36,6 +36,7 @@ import urlparse import errno import subprocess from collections import OrderedDict +from urllib import urlretrieve from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -100,14 +101,13 @@ def app_fetchlist(url=None, name=None): raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) - list_file = '%s/%s.json' % (repo_path, name) - if os.system('wget "%s" -O "%s.tmp"' % (url, list_file)) != 0: - os.remove('%s.tmp' % list_file) + try: + urlretrieve(url, '%s/%s.json' % (repo_path, name)) + except Exception as e: + # I don't know how to put e into the MoulinetteError stuff + print e raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error')) - # Rename fetched temp list - os.rename('%s.tmp' % list_file, list_file) - open("/etc/cron.d/yunohost-applist-%s" % name, "w").write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) logger.success(m18n.n('appslist_fetched')) From 97128d7d636836068ad6353f331d051121023136 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 00:43:17 +0200 Subject: [PATCH 0079/1066] [mod] exception should only be used for exceptional situations and not when buildin functions allow you to do the expected stuff --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3b2a6d24f..98beca078 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -90,8 +90,8 @@ def app_fetchlist(url=None, name=None): """ # Create app path if not exists - try: os.listdir(repo_path) - except OSError: os.makedirs(repo_path) + if not os.path.exists(repo_path): + os.makedirs(repo_path) if url is None: url = 'https://app.yunohost.org/official.json' From 2aab7bdf1bcc6f025c7c5bf618d0402439abd0f4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 00:44:25 +0200 Subject: [PATCH 0080/1066] [mod] simplify code --- src/yunohost/app.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 98beca078..f6f8699a4 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -96,10 +96,9 @@ def app_fetchlist(url=None, name=None): if url is None: url = 'https://app.yunohost.org/official.json' name = 'yunohost' - else: - if name is None: - raise MoulinetteError(errno.EINVAL, - m18n.n('custom_appslist_name_required')) + elif name is None: + raise MoulinetteError(errno.EINVAL, + m18n.n('custom_appslist_name_required')) try: urlretrieve(url, '%s/%s.json' % (repo_path, name)) From 99f0f761a5e2737b55f9f8b6ce6094b5fd7fb1ca Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Jun 2016 13:21:58 +0200 Subject: [PATCH 0081/1066] [mod] include execption into appslist_retrieve_error message --- locales/en.json | 2 +- src/yunohost/app.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index e939b26fa..5dd46485b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -30,7 +30,7 @@ "app_upgraded": "{app:s} has been upgraded", "appslist_fetched": "The app list has been fetched", "appslist_removed": "The app list has been removed", - "appslist_retrieve_error": "Unable to retrieve the remote app list", + "appslist_retrieve_error": "Unable to retrieve the remote app list: {error}", "appslist_unknown": "Unknown app list", "ask_current_admin_password": "Current administration password", "ask_email": "Email address", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f6f8699a4..211009a04 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -103,9 +103,7 @@ def app_fetchlist(url=None, name=None): try: urlretrieve(url, '%s/%s.json' % (repo_path, name)) except Exception as e: - # I don't know how to put e into the MoulinetteError stuff - print e - raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error')) + raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error'), error=str(e)) open("/etc/cron.d/yunohost-applist-%s" % name, "w").write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) From bba92e4d4104a0ca5e81ad0c0582e5eac98bd6df Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Nov 2016 20:36:58 -0500 Subject: [PATCH 0082/1066] Small tweaks for the web interface --- data/actionsmap/yunohost.yml | 8 ++++---- src/yunohost/certificate.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8f08a98b8..3729e3aa3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -308,7 +308,7 @@ domain: ### certificate_status() cert-status: action_help: List status of current certificates (all by default). - api: GET /certs/status/ + api: GET /domains/cert-status/ configuration: authenticate: all authenticator: ldap-anonymous @@ -323,7 +323,7 @@ domain: ### certificate_install() cert-install: action_help: Install Let's Encrypt certificates for given domains (all by default). - api: POST /certs/enable/ + api: POST /domains/cert-install/ configuration: authenticate: all authenticator: ldap-anonymous @@ -344,7 +344,7 @@ domain: ### certificate_renew() cert-renew: action_help: Renew the Let's Encrypt certificates for given domains (all by default). - api: POST /certs/renew/ + api: POST /domains/cert-renew/ configuration: authenticate: all authenticator: ldap-anonymous @@ -361,7 +361,7 @@ domain: --no-checks: help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true - + ### domain_info() # info: # action_help: Get domain informations diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8feb9a771..8b3db0283 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -103,6 +103,7 @@ def certificate_status(auth, domain_list, full=False): if not full: del status["subject"] del status["CA_name"] + del status["ACME_eligible"] status["CA_type"] = status["CA_type"]["verbose"] status["summary"] = status["summary"]["verbose"] @@ -157,10 +158,10 @@ def certificate_install_selfsigned(domain_list, force=False): key_file = os.path.join(new_cert_folder, "key.pem") crt_file = os.path.join(new_cert_folder, "crt.pem") ca_file = os.path.join(new_cert_folder, "ca.pem") - + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) - + # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain with open(conf_file, "w") as f : @@ -168,10 +169,10 @@ def certificate_install_selfsigned(domain_list, force=False): for line in template : f.write(line.replace("yunohost.org", domain)) - # Use OpenSSL command line to create a certificate signing request, + # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert commands = [] - commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" % (conf_file, csr_file, key_file)) commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" % (conf_file, csr_file, crt_file)) @@ -249,7 +250,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa try: if not no_checks: - _check_domain_is_correctly_configured(domain) + _check_domain_is_ready_for_ACME(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) @@ -318,7 +319,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: - _check_domain_is_correctly_configured(domain) + _check_domain_is_ready_for_ACME(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -608,6 +609,12 @@ def _get_status(domain): "verbose": "Unknown?", } + try : + _check_domain_is_ready_for_ACME(domain) + ACME_eligible = True + except : + ACME_eligible = False + return { "domain": domain, "subject": cert_subject, @@ -615,6 +622,7 @@ def _get_status(domain): "CA_type": CA_type, "validity": days_remaining, "summary": status_summary, + "ACME_eligible": ACME_eligible } ############################################################################### @@ -681,7 +689,7 @@ def _backup_current_cert(domain): shutil.copytree(cert_folder_domain, backup_folder) -def _check_domain_is_correctly_configured(domain): +def _check_domain_is_ready_for_ACME(domain): public_ip = yunohost.domain.get_public_ip() # Check if IP from DNS matches public IP From 5de006f18df96398658809fa6ad5b565f8f83bef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Nov 2016 10:58:57 -0500 Subject: [PATCH 0083/1066] Follow up of @julienmalik comments - misc typo/cosmetic fixes --- data/actionsmap/yunohost.yml | 6 +++--- locales/de.json | 2 +- locales/en.json | 8 ++++---- locales/es.json | 2 +- locales/fr.json | 2 +- locales/nl.json | 2 +- locales/pt.json | 2 +- src/yunohost/certificate.py | 26 ++++++++++++++++---------- src/yunohost/domain.py | 2 +- 9 files changed, 29 insertions(+), 23 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 3729e3aa3..18f470a61 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -335,7 +335,7 @@ domain: help: Install even if current certificate is not self-signed action: store_true --no-checks: - help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to install. (Not recommended) + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) action: store_true --self-signed: help: Install self-signed certificate instead of Let's Encrypt @@ -353,13 +353,13 @@ domain: help: Domains for which to renew the certificates nargs: "*" --force: - help: Ignore the validity treshold (30 days) + help: Ignore the validity threshold (30 days) action: store_true --email: help: Send an email to root with logs if some renewing fails action: store_true --no-checks: - help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true ### domain_info() diff --git a/locales/de.json b/locales/de.json index e57315caa..1331c56b4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -57,7 +57,7 @@ "custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben", "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus", - "certmanager_domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", + "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domain erfolgreich erzeugt", "domain_creation_failed": "Konnte Domain nicht erzeugen", "domain_deleted": "Domain erfolgreich gelöscht", diff --git a/locales/en.json b/locales/en.json index 2dec12706..b3723810e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -237,13 +237,13 @@ "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", - "certmanager_domain_cert_gen_failed": "Unable to generate certificate", + "domain_cert_gen_failed": "Unable to generate certificate", "certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)", "certmanager_domain_unknown": "Unknown domain {domain:s}", - "certmanager_domain_cert_not_selfsigned" : "The certificate of domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", + "certmanager_domain_cert_not_selfsigned" : "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", - "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", - "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", + "certmanager_attempt_to_renew_nonLE_cert" : "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", + "certmanager_attempt_to_renew_valid_cert" : "The certificate for domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", diff --git a/locales/es.json b/locales/es.json index fdd04d10f..549cbe29a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -72,7 +72,7 @@ "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", - "certmanager_domain_cert_gen_failed": "No se pudo crear el certificado", + "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", "domain_deleted": "El dominio ha sido eliminado", diff --git a/locales/fr.json b/locales/fr.json index 6691b2f28..7898de57f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -73,7 +73,7 @@ "diagnostic_monitor_system_error": "Impossible de superviser le système : {error}", "diagnostic_no_apps": "Aucune application installée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »", - "certmanager_domain_cert_gen_failed": "Impossible de générer le certificat", + "domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", "domain_creation_failed": "Impossible de créer le domaine", "domain_deleted": "Le domaine a été supprimé", diff --git a/locales/nl.json b/locales/nl.json index 57b05e309..c2bfed31e 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -37,7 +37,7 @@ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken", "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst", "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'", - "certmanager_domain_cert_gen_failed": "Kan certificaat niet genereren", + "domain_cert_gen_failed": "Kan certificaat niet genereren", "domain_created": "Domein succesvol aangemaakt", "domain_creation_failed": "Kan domein niet aanmaken", "domain_deleted": "Domein succesvol verwijderd", diff --git a/locales/pt.json b/locales/pt.json index b9c9e4bce..d3796d2e9 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -36,7 +36,7 @@ "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}", "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", - "certmanager_domain_cert_gen_failed": "Não foi possível gerar o certificado", + "domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", "domain_creation_failed": "Não foi possível criar o domínio", "domain_deleted": "Domínio removido com êxito", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8b3db0283..961cf18fb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -82,6 +82,8 @@ def certificate_status(auth, domain_list, full=False): """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() # If no domains given, consider all yunohost domains @@ -127,16 +129,18 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() if self_signed: - certificate_install_selfsigned(domain_list, force) + _certificate_install_selfsigned(domain_list, force) else: - certificate_install_letsencrypt(auth, domain_list, force, no_checks) + _certificate_install_letsencrypt(auth, domain_list, force, no_checks) -def certificate_install_selfsigned(domain_list, force=False): +def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: # Check we ain't trying to overwrite a good cert ! @@ -182,7 +186,7 @@ def certificate_install_selfsigned(domain_list, force=False): out, _ = p.communicate() if p.returncode != 0: logger.warning(out) - raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + raise MoulinetteError(errno.EIO, m18n.n('domain_cert_gen_failed')) else : logger.info(out) @@ -216,7 +220,7 @@ def certificate_install_selfsigned(domain_list, force=False): -def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): +def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -276,6 +280,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() # If no domains given, consider all yunohost domains with Let's Encrypt @@ -283,12 +289,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Does it has a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert ? status = _get_status(domain) if status["CA_type"]["code"] != "lets-encrypt": continue - # Does it expires soon ? + # Does it expire soon ? if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) @@ -305,11 +311,11 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal status = _get_status(domain) - # Does it expires soon ? + # Does it expire soon ? if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it has a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert ? if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) @@ -383,7 +389,7 @@ def _email_renewing_failed(domain, exception_message, stack): logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") text = """ -At attempt for renewing the certificate for domain %s failed with the following +An attempt for renewing the certificate for domain %s failed with the following error : %s diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index aadd9086d..7d34aff53 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -114,7 +114,7 @@ def domain_add(auth, domain, dyndns=False): m18n.n('domain_dyndns_root_unknown')) try: - yunohost.certificate.certificate_install_selfsigned([domain], False) + yunohost.certificate._certificate_install_selfsigned([domain], False) try: auth.validate_uniqueness({'virtualdomain': domain}) From c57e343d45bc0f1b0b8f40947c857e8e0a50b415 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Nov 2016 11:24:55 -0500 Subject: [PATCH 0084/1066] Looking for ssowat header in https (workaround for when app is installed on root domain) --- src/yunohost/certificate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 961cf18fb..cca582ed7 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -731,8 +731,11 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - r = requests.head("http://" + ip, headers={"Host": domain}) - # Check we got the ssowat header in the response + # Check HTTP reachability + requests.head("http://" + ip, headers={"Host": domain}) + + # Check we got the ssowat header (in HTTPS) + r = requests.head("https://" + ip, headers={"Host": domain}, verify=False) if "x-sso-wat" not in r.headers.keys(): return False except Exception: From dc731c3af56736e97e4713456f5239aeba66ed40 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:13:39 -0500 Subject: [PATCH 0085/1066] Using a single generic skipped regex for acme challenge in ssowat conf --- src/yunohost/app.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 83fc8614c..a658a0c34 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1031,22 +1031,8 @@ def app_ssowatconf(auth): for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) - # Authorize ACME challenge url if a domain seems configured for it... - for domain in domains: - - # Check ACME challenge file is present in nginx conf - nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain - - if not os.path.isfile(nginx_acme_challenge_conf_file): - continue - - # Check the file contains the ACME challenge uri - if not '/.well-known/acme-challenge' in open(nginx_acme_challenge_conf_file).read(): - continue - - # If so, then authorize the ACME challenge uri to unprotected regex - unprotected_regex.append(domain + "/%.well%-known/acme%-challenge/.*$") - + # Authorize ACME challenge url + skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$") conf_dict = { 'portal_domain': main_domain, From fb29bb879b860de7c214ccacac9d164b647d5773 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:23:14 -0500 Subject: [PATCH 0086/1066] Removing check for ssowat header when testing HTTP reachability --- src/yunohost/certificate.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index cca582ed7..22346a460 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -731,13 +731,7 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - # Check HTTP reachability requests.head("http://" + ip, headers={"Host": domain}) - - # Check we got the ssowat header (in HTTPS) - r = requests.head("https://" + ip, headers={"Host": domain}, verify=False) - if "x-sso-wat" not in r.headers.keys(): - return False except Exception: return False From be061522e6818a14d4d0d23280292b45a593cc93 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:32:33 -0500 Subject: [PATCH 0087/1066] Moving full letsencrypt app conflict warning to locales/en.json --- locales/en.json | 2 +- src/yunohost/certificate.py | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/locales/en.json b/locales/en.json index b3723810e..10dc41edb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,5 @@ "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." + "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 22346a460..7a3ded247 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -354,21 +354,6 @@ def _check_old_letsencrypt_app(): if "letsencrypt" not in installedAppIds: return - logger.warning(" ") - logger.warning("Yunohost detected that the 'letsencrypt' app is installed, ") - logger.warning("which conflits with the new certificate management features") - logger.warning("directly integrated in Yunohost. If you wish to use these ") - logger.warning("new features, please run the following commands to migrate ") - logger.warning("your installation :") - logger.warning(" ") - logger.warning(" yunohost app remove letsencrypt") - logger.warning(" yunohost domain cert-install") - logger.warning(" ") - logger.warning("N.B. : this will attempt to re-install certificates for ") - logger.warning("all domains with a Let's Encrypt certificate or self-signed") - logger.warning("certificate.") - logger.warning(" ") - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) From 0132cf037f05955055b22b9d91909b9b8c3a8f87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:38:33 -0500 Subject: [PATCH 0088/1066] Moving DNS resolvers IP to constant var at beginning of file --- src/yunohost/certificate.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 7a3ded247..ec733f657 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -67,6 +67,18 @@ CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" +DNS_RESOLVERS = [ + # FFDN DNS resolvers + # See https://www.ffdn.org/wiki/doku.php?id=formations:dns + "80.67.169.12", # FDN + "80.67.169.40", # + "89.234.141.66", # ARN + "141.255.128.100", # Aquilenet + "141.255.128.101", # + "89.234.186.18", # Grifon + "80.67.188.188" # LDN +] + ############################################################################### # Front-end stuff # ############################################################################### @@ -695,16 +707,7 @@ def _check_domain_is_ready_for_ACME(domain): def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() - resolver.nameservers = [] - # FFDN DNS resolvers - # See https://www.ffdn.org/wiki/doku.php?id=formations:dns - resolver.nameservers.append("80.67.169.12") # FDN - resolver.nameservers.append("80.67.169.40") # - resolver.nameservers.append("89.234.141.66") # ARN - resolver.nameservers.append("141.255.128.100") # Aquilenet - resolver.nameservers.append("141.255.128.101") # - resolver.nameservers.append("89.234.186.18") # Grifon - resolver.nameservers.append("80.67.188.188") # LDN + resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) From a6353703bd9a6bb3a12edb72ad052d97608b841a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 22:24:54 -0500 Subject: [PATCH 0089/1066] Catching exceptions from acme-tiny --- locales/en.json | 4 +++- src/yunohost/certificate.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/locales/en.json b/locales/en.json index 10dc41edb..85b25efe9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,7 @@ "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate." + "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", + "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", + "certmanager_cert_signing_failed" : "Signing the new certificate failed." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ec733f657..99e6bd014 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,7 +31,6 @@ import grp import smtplib import requests import subprocess - import dns.resolver from OpenSSL import crypto @@ -470,11 +469,20 @@ def _fetch_and_enable_new_certificate(domain): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, - domain_csr_file, - WEBROOT_FOLDER, - log=logger, - CA=CERTIFICATION_AUTHORITY) + try: + signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, + domain_csr_file, + WEBROOT_FOLDER, + log=logger, + CA=CERTIFICATION_AUTHORITY) + except ValueError as e: + if ("urn:acme:error:rateLimited" in str(e)) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) + else : + raise + except Exception as e: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + logger.error(str(e)) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -612,10 +620,10 @@ def _get_status(domain): "verbose": "Unknown?", } - try : + try: _check_domain_is_ready_for_ACME(domain) ACME_eligible = True - except : + except: ACME_eligible = False return { From ed16cd7f5aed448cd76a3da99f82f75732ec2f18 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 23:44:23 -0500 Subject: [PATCH 0090/1066] Adding an option to use the staging Let's Encrypt CA, sort of a dry-run --- data/actionsmap/yunohost.yml | 7 ++++++ src/yunohost/certificate.py | 47 +++++++++++++++++++++++++----------- src/yunohost/domain.py | 8 +++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 18f470a61..c1126ece0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -340,6 +340,9 @@ domain: --self-signed: help: Install self-signed certificate instead of Let's Encrypt action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true ### certificate_renew() cert-renew: @@ -361,6 +364,10 @@ domain: --no-checks: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true + ### domain_info() # info: diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 99e6bd014..3db94e8b8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -60,9 +60,9 @@ KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days # For tests -#CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" +STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" # For prod -CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" +PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" @@ -126,7 +126,7 @@ def certificate_status(auth, domain_list, full=False): return {"certificates" : certificates} -def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): +def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -148,7 +148,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(auth, domain_list, force, no_checks) + _certificate_install_letsencrypt(auth, domain_list, force, no_checks, staging) def _certificate_install_selfsigned(domain_list, force=False): @@ -231,7 +231,7 @@ def _certificate_install_selfsigned(domain_list, force=False): -def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): +def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -258,6 +258,9 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + if (staging): + logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + # Actual install steps for domain in domain_list: @@ -268,7 +271,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F _check_domain_is_ready_for_ACME(domain) _configure_for_acme_challenge(auth, domain) - _fetch_and_enable_new_certificate(domain) + _fetch_and_enable_new_certificate(domain, staging) _install_cron() logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) @@ -278,7 +281,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.error(str(e)) -def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False): +def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -330,6 +333,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + if (staging): + logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + # Actual renew steps for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) @@ -337,7 +343,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: _check_domain_is_ready_for_ACME(domain) - _fetch_and_enable_new_certificate(domain) + _fetch_and_enable_new_certificate(domain, staging) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -442,7 +448,7 @@ location '/.well-known/acme-challenge' app_ssowatconf(auth) -def _fetch_and_enable_new_certificate(domain): +def _fetch_and_enable_new_certificate(domain, staging=False): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") @@ -469,20 +475,25 @@ def _fetch_and_enable_new_certificate(domain): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) + if (staging): + certification_authority = STAGING_CERTIFICATION_AUTHORITY + else: + certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY + try: signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, domain_csr_file, WEBROOT_FOLDER, log=logger, - CA=CERTIFICATION_AUTHORITY) + CA=certification_authority) except ValueError as e: - if ("urn:acme:error:rateLimited" in str(e)) : + if ("urn:acme:error:rateLimited" in str(e)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) - else : + else: raise except Exception as e: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -492,7 +503,12 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s-history/%s-letsencrypt" % (CERT_FOLDER, domain, date_tag) + if (staging): + folder_flag = "staging" + else: + folder_flag = "letsencrypt" + + new_cert_folder = "%s/%s-history/%s-%s" % (CERT_FOLDER, domain, date_tag, folder_flag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -509,6 +525,9 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(domain_cert_file, "root", "metronome", 0640) + if (staging): + return + _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 7d34aff53..415585087 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -261,12 +261,12 @@ def domain_cert_status(auth, domain_list, full=False): return yunohost.certificate.certificate_status(auth, domain_list, full) -def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False): - return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed) +def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): + return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed, staging) -def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False): - return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email) +def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): + return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) def get_public_ip(protocol=4): From e66a7085202a33b9f4688a28a994b02974a28ee3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Nov 2016 11:46:52 -0500 Subject: [PATCH 0091/1066] Misc tweaks on exceptions --- locales/en.json | 3 ++- src/yunohost/certificate.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index 85b25efe9..637671a2d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -253,5 +253,6 @@ "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", - "certmanager_cert_signing_failed" : "Signing the new certificate failed." + "certmanager_cert_signing_failed" : "Signing the new certificate failed.", + "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3db94e8b8..d892e6f21 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -155,10 +155,11 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: # Check we ain't trying to overwrite a good cert ! - status = _get_status(domain) + if (not force) : + status = _get_status(domain) - if status and status["summary"]["code"] in ('good', 'great') and not force: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + if status["summary"]["code"] in ('good', 'great') : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") @@ -490,7 +491,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): if ("urn:acme:error:rateLimited" in str(e)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) else: - raise + logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) except Exception as e: logger.error(str(e)) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) @@ -565,7 +567,7 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - return {} + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_cert_file', domain=domain, file=cert_file)) try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) From dfbfc0cfc7ab35e8a1188b647c7f8c3a3915e62a Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 24 Nov 2016 02:55:59 +0100 Subject: [PATCH 0092/1066] [fix] Can't get mailbos used space if dovecot is down --- locales/en.json | 1 + src/yunohost/user.py | 46 +++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/locales/en.json b/locales/en.json index e939b26fa..a87a459b7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -118,6 +118,7 @@ "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", + "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", "monitor_disabled": "The server monitoring has been disabled", diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ec7dd539c..348a23d32 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -92,7 +92,7 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): def user_create(auth, username, firstname, lastname, mail, password, - mailbox_quota=0): + mailbox_quota="0"): """ Create user @@ -398,20 +398,36 @@ def user_info(auth, username): result_dict['mail-forward'] = user['maildrop'][1:] if 'mailuserquota' in user: - if user['mailuserquota'][0] != '0': - cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] - userquota = subprocess.check_output(cmd,stderr=subprocess.STDOUT, - shell=True) - quotavalue = re.findall(r'\d+', userquota) - result = '%s (%s%s)' % ( _convertSize(eval(quotavalue[0])), - quotavalue[2], '%') - result_dict['mailbox-quota'] = { - 'limit' : user['mailuserquota'][0], - 'use' : result - } - else: - result_dict['mailbox-quota'] = m18n.n('unlimit') - + userquota = user['mailuserquota'][0] + if isinstance( userquota, int ): + userquota = str(userquota) + + # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) + is_limited = not re.match('0[bkMGT]?', userquota) + storage_use = '?' + cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] + try: + cmd_result = subprocess.check_output(cmd,stderr=subprocess.STDOUT, + shell=True) + # Exemple of return value for cmd: + # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 + # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" + has_value=re.search(r'Value=(\d+)', cmd_result) + if has_value: + storage_use = int(has_value.group(1)) + storage_use = _convertSize(storage_use) + if is_limited: + has_percent=re.search(r'%=(\d+)', cmd_result) + if has_percent: + percentage = int(has_percent.group(1)) + storage_use += ' (%s%s)' % (percentage, '%') + except subprocess.CalledProcessError: + logger.warning(m18n.n('mailbox_used_space_dovecot_down')) + result_dict['mailbox-quota'] = { + 'limit' : userquota if is_limited else m18n.n('unlimit'), + 'use' : storage_use + } + if result: return result_dict else: From 195c675c590b5b260883f5ad90e0958e5e9f3ec7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Nov 2016 21:36:34 -0500 Subject: [PATCH 0093/1066] More exception handling, this time for previous acme challenge conf already existing in nginx --- locales/en.json | 3 ++- src/yunohost/certificate.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index 637671a2d..efeb66e69 100644 --- a/locales/en.json +++ b/locales/en.json @@ -254,5 +254,6 @@ "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", "certmanager_cert_signing_failed" : "Signing the new certificate failed.", - "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})" + "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d892e6f21..d9110b79b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -32,6 +32,7 @@ import smtplib import requests import subprocess import dns.resolver +import glob from OpenSSL import crypto from datetime import datetime @@ -152,15 +153,9 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si def _certificate_install_selfsigned(domain_list, force=False): + for domain in domain_list: - # Check we ain't trying to overwrite a good cert ! - if (not force) : - status = _get_status(domain) - - if status["summary"]["code"] in ('good', 'great') : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) @@ -175,6 +170,14 @@ def _certificate_install_selfsigned(domain_list, force=False): crt_file = os.path.join(new_cert_folder, "crt.pem") ca_file = os.path.join(new_cert_folder, "ca.pem") + # Check we ain't trying to overwrite a good cert ! + current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + if (not force) and (os.path.isfile(current_cert_file)): + status = _get_status(domain) + + if status["summary"]["code"] in ('good', 'great') : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -261,7 +264,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if (staging): logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") - + # Actual install steps for domain in domain_list: @@ -336,7 +339,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if (staging): logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") - + # Actual renew steps for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) @@ -421,7 +424,9 @@ Subject: %s def _configure_for_acme_challenge(auth, domain): - nginx_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain + + nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain + nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder nginx_configuration = ''' location '/.well-known/acme-challenge' @@ -431,6 +436,15 @@ location '/.well-known/acme-challenge' } ''' % WEBROOT_FOLDER + # Check there isn't a conflicting file for the acme-challenge well-known uri + for path in glob.glob('%s/*.conf' % nginx_conf_folder): + if (path == nginx_conf_file) : + continue + with open(path) as f: + contents = f.read() + if ('/.well-known/acme-challenge' in contents) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_conflicting_nginx_file', filepath=path)) + # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") @@ -564,6 +578,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): From 1b20899f0efeb81f945f28fce83f77e5d0afb5f0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 24 Nov 2016 10:40:01 -0500 Subject: [PATCH 0094/1066] Daily renew cron job instead of weekly --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d9110b79b..81f2f0e2b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -379,7 +379,7 @@ def _check_old_letsencrypt_app(): def _install_cron(): - cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" + cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") From ddcc57eb9db20e55fcb2bc91ba6b70054dd3182f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 24 Nov 2016 10:59:25 -0500 Subject: [PATCH 0095/1066] pep8 --- src/yunohost/certificate.py | 144 ++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81f2f0e2b..b9fcb9b3e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -73,8 +73,8 @@ DNS_RESOLVERS = [ "80.67.169.12", # FDN "80.67.169.40", # "89.234.141.66", # ARN - "141.255.128.100", # Aquilenet - "141.255.128.101", # + "141.255.128.100", # Aquilenet + "141.255.128.101", "89.234.186.18", # Grifon "80.67.188.188" # LDN ] @@ -107,7 +107,8 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: # Is it in Yunohost domain list ? if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) certificates = {} @@ -124,11 +125,10 @@ def certificate_status(auth, domain_list, full=False): del status["domain"] certificates[domain] = status - return {"certificates" : certificates} + return {"certificates": certificates} def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): - """ Install a Let's Encrypt certificate for given domains (all by default) @@ -145,11 +145,11 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si # not used anymore _check_old_letsencrypt_app() - if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(auth, domain_list, force, no_checks, staging) + _certificate_install_letsencrypt( + auth, domain_list, force, no_checks, staging) def _certificate_install_selfsigned(domain_list, force=False): @@ -158,7 +158,8 @@ def _certificate_install_selfsigned(domain_list, force=False): # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) + new_cert_folder = "%s/%s-history/%s-selfsigned" % ( + CERT_FOLDER, domain, date_tag) original_ca_file = '/etc/ssl/certs/ca-yunohost_crt.pem' ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' @@ -175,17 +176,18 @@ def _certificate_install_selfsigned(domain_list, force=False): if (not force) and (os.path.isfile(current_cert_file)): status = _get_status(domain) - if status["summary"]["code"] in ('good', 'great') : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + if status["summary"]["code"] in ('good', 'great'): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_replace_valid_cert', domain=domain)) # Create output folder for new certificate stuff os.makedirs(new_cert_folder) # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain - with open(conf_file, "w") as f : - with open(conf_template, "r") as template : - for line in template : + with open(conf_file, "w") as f: + with open(conf_template, "r") as template: + for line in template: f.write(line.replace("yunohost.org", domain)) # Use OpenSSL command line to create a certificate signing request, @@ -196,13 +198,15 @@ def _certificate_install_selfsigned(domain_list, force=False): commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" % (conf_file, csr_file, crt_file)) - for command in commands : - p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for command in commands: + p = subprocess.Popen( + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out, _ = p.communicate() if p.returncode != 0: logger.warning(out) - raise MoulinetteError(errno.EIO, m18n.n('domain_cert_gen_failed')) - else : + raise MoulinetteError( + errno.EIO, m18n.n('domain_cert_gen_failed')) + else: logger.info(out) # Link the CA cert (not sure it's actually needed in practice though, @@ -229,10 +233,11 @@ def _certificate_install_selfsigned(domain_list, force=False): status = _get_status(domain) if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: - logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) - else : - logger.error("Installation of self-signed certificate installation for %s failed !", domain) - + logger.success( + m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + else: + logger.error( + "Installation of self-signed certificate installation for %s failed !", domain) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -255,20 +260,24 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F for domain in domain_list: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) # Is it self-signed ? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_cert_not_selfsigned', domain=domain)) if (staging): - logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") # Actual install steps for domain in domain_list: - logger.info("Now attempting install of certificate for domain %s!", domain) + logger.info( + "Now attempting install of certificate for domain %s!", domain) try: if not no_checks: @@ -278,7 +287,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F _fetch_and_enable_new_certificate(domain, staging) _install_cron() - logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) + logger.success( + m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: logger.error("Certificate installation for %s failed !", domain) @@ -325,31 +335,37 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) status = _get_status(domain) # Does it expire soon ? if not force or status["validity"] <= VALIDITY_LIMIT: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it have a Let's Encrypt cert ? if status["CA_type"]["code"] != "lets-encrypt": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) if (staging): - logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") # Actual renew steps for domain in domain_list: - logger.info("Now attempting renewing of certificate for domain %s !", domain) + logger.info( + "Now attempting renewing of certificate for domain %s !", domain) try: if not no_checks: _check_domain_is_ready_for_ACME(domain) _fetch_and_enable_new_certificate(domain, staging) - logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) + logger.success( + m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: import traceback @@ -375,7 +391,8 @@ def _check_old_letsencrypt_app(): if "letsencrypt" not in installedAppIds: return - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_old_letsencrypt_app_detected')) def _install_cron(): @@ -436,21 +453,25 @@ location '/.well-known/acme-challenge' } ''' % WEBROOT_FOLDER - # Check there isn't a conflicting file for the acme-challenge well-known uri + # Check there isn't a conflicting file for the acme-challenge well-known + # uri for path in glob.glob('%s/*.conf' % nginx_conf_folder): - if (path == nginx_conf_file) : + if (path == nginx_conf_file): continue with open(path) as f: contents = f.read() - if ('/.well-known/acme-challenge' in contents) : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_conflicting_nginx_file', filepath=path)) + if ('/.well-known/acme-challenge' in contents): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_conflicting_nginx_file', filepath=path)) # Write the conf if os.path.exists(nginx_conf_file): - logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") + logger.info( + "Nginx configuration file for ACME challenge already exists for domain, skipping.") return - logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) + logger.info( + "Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -477,7 +498,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(TMP_FOLDER, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) + logger.info( + "Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) @@ -503,13 +525,16 @@ def _fetch_and_enable_new_certificate(domain, staging=False): CA=certification_authority) except ValueError as e: if ("urn:acme:error:rateLimited" in str(e)): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_hit_rate_limit', domain=domain)) else: logger.error(str(e)) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) except Exception as e: logger.error(str(e)) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -524,7 +549,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): else: folder_flag = "letsencrypt" - new_cert_folder = "%s/%s-history/%s-%s" % (CERT_FOLDER, domain, date_tag, folder_flag) + new_cert_folder = "%s/%s-history/%s-%s" % ( + CERT_FOLDER, domain, date_tag, folder_flag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -550,7 +576,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): status_summary = _get_status(domain)["summary"] if status_summary["code"] != "great": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_certificate_fetching_or_enabling_failed', domain=domain)) def _prepare_certificate_signing_request(domain, key_file, output_folder): @@ -582,14 +609,17 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_cert_file', domain=domain, file=cert_file)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_no_cert_file', domain=domain, file=cert_file)) try: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) + cert = crypto.load_certificate( + crypto.FILETYPE_PEM, open(cert_file).read()) except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN @@ -626,7 +656,7 @@ def _get_status(domain): "verbose": "CRITICAL", } - elif CA_type["code"] in ("self-signed","fake-lets-encrypt"): + elif CA_type["code"] in ("self-signed", "fake-lets-encrypt"): status_summary = { "code": "warning", "verbose": "WARNING", @@ -699,13 +729,13 @@ def _set_permissions(path, user, group, permissions): os.chmod(path, permissions) -def _enable_certificate(domain, new_cert_folder) : +def _enable_certificate(domain, new_cert_folder): logger.info("Enabling the certificate for domain %s ...", domain) live_link = os.path.join(CERT_FOLDER, domain) # If a live link (or folder) already exists - if os.path.exists(live_link) : + if os.path.exists(live_link): # If it's not a link ... expect if to be a folder if not os.path.islink(live_link): # Backup it and remove it @@ -741,11 +771,13 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP ? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_http_not_working', domain=domain)) def _dns_ip_match_public_ip(public_ip, domain): @@ -754,7 +786,8 @@ def _dns_ip_match_public_ip(public_ip, domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_error_no_A_record', domain=domain)) dns_ip = str(answers[0]) @@ -771,7 +804,8 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) + cert = crypto.load_certificate( + crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) return cert.get_subject().CN From 952040183e4111374fdf0e3b754425a815632745 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 27 Nov 2016 01:16:22 +0100 Subject: [PATCH 0096/1066] [mod] give instructions on how to solve the conf.json.persistant parsing error --- locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index dc801a908..bbb33e937 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,8 +209,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}", - "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. To fix this you need to fix the json syntax error in the SSOwat persistent configuration file which is located there on your filesystem: /etc/ssowat/conf.json.persistent", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. To fix this you need to fix the json syntax error in the SSOwat persistent configuration file which is located there on your filesystem: /etc/ssowat/conf.json.persistent", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", From 504baefd87a45edaf59ccb91da4f4c502f7019bc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 02:06:45 +0100 Subject: [PATCH 0097/1066] [fix] avoid random bug on post-install due to nscd cache\n\nThanks to the work of Alex Aubin who've managed to discover why this was happening and propose a solution. --- src/yunohost/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f78e32363..7d7d8d0cc 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -32,6 +32,7 @@ import requests import json import errno import logging +import subprocess from collections import OrderedDict import apt @@ -82,6 +83,7 @@ def tools_ldapinit(auth): } auth.update('cn=admin', admin_dict) + subprocess.call(['nscd', '-i', 'passwd']) logger.success(m18n.n('ldap_initialized')) From f956fa7161bb855d165e2ab6b9b013c15cc6e5b2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Nov 2016 02:35:19 +0100 Subject: [PATCH 0098/1066] [enh] Adding check that user is actually created + minor refactor of ldap/auth init --- locales/en.json | 1 + src/yunohost/tools.py | 31 +++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index e939b26fa..5bc3b7bdb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -114,6 +114,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it.", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it.", "ldap_initialized": "LDAP has been initialized", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin. Aborting.", "license_undefined": "undefined", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 7d7d8d0cc..671a7304c 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -33,6 +33,7 @@ import json import errno import logging import subprocess +import pwd from collections import OrderedDict import apt @@ -53,12 +54,20 @@ apps_setting_path= '/etc/yunohost/apps/' logger = getActionLogger('yunohost.tools') -def tools_ldapinit(auth): +def tools_ldapinit(): """ YunoHost LDAP initialization """ + + # Instantiate LDAP Authenticator + auth = init_authenticator(('ldap', 'default'), + {'uri': "ldap://localhost:389", + 'base_dn': "dc=yunohost,dc=org", + 'user_rdn': "cn=admin" }) + auth.authenticate('yunohost') + with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f: ldap_map = yaml.load(f) @@ -83,10 +92,19 @@ def tools_ldapinit(auth): } auth.update('cn=admin', admin_dict) + + # Force nscd to refresh cache to take admin creation into account subprocess.call(['nscd', '-i', 'passwd']) - logger.success(m18n.n('ldap_initialized')) + # Check admin actually exists now + try: + pwd.getpwnam("admin") + except KeyError: + raise MoulinetteError(errno.EINVAL, + m18n.n('ldap_init_failed_to_create_admin')) + logger.success(m18n.n('ldap_initialized')) + return auth def tools_adminpw(auth, new_password): """ @@ -193,16 +211,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.info(m18n.n('yunohost_installing')) - # Instantiate LDAP Authenticator - auth = init_authenticator(('ldap', 'default'), - {'uri': "ldap://localhost:389", - 'base_dn': "dc=yunohost,dc=org", - 'user_rdn': "cn=admin" }) - auth.authenticate('yunohost') - # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook - tools_ldapinit(auth) + auth = tools_ldapinit() # Create required folders folders_to_create = [ From 7f046f088003ed9effe56ad90abac833f35fefc9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:32:07 +0100 Subject: [PATCH 0099/1066] [mod] no space before a ? in english --- src/yunohost/certificate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b9fcb9b3e..3660698ea 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -105,7 +105,7 @@ def certificate_status(auth, domain_list, full=False): else: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] for domain in domain_list: - # Is it in Yunohost domain list ? + # Is it in Yunohost domain list? if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) @@ -263,13 +263,13 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) - # Is it self-signed ? + # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_cert_not_selfsigned', domain=domain)) - if (staging): + if staging: logger.warning( "Please note that you used the --staging option, and that no new certificate will actually be enabled !") @@ -317,12 +317,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Does it have a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert? status = _get_status(domain) if status["CA_type"]["code"] != "lets-encrypt": continue - # Does it expire soon ? + # Does it expire soon? if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) @@ -333,19 +333,19 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal else: for domain in domain_list: - # Is it in Yunohost dmomain list ? + # Is it in Yunohost dmomain list? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) status = _get_status(domain) - # Does it expire soon ? + # Does it expire soon? if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it have a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert? if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) @@ -774,7 +774,7 @@ def _check_domain_is_ready_for_ACME(domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) - # Check if domain seems to be accessible through HTTP ? + # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_http_not_working', domain=domain)) From 5b73ab448f33194ced9a8b5e3afcb74490f05186 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:32:24 +0100 Subject: [PATCH 0100/1066] [mod] create list in one step --- src/yunohost/certificate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3660698ea..0133a3387 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -192,11 +192,12 @@ def _certificate_install_selfsigned(domain_list, force=False): # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert - commands = [] - commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" - % (conf_file, csr_file, key_file)) - commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" - % (conf_file, csr_file, crt_file)) + commands = [ + "openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + % (conf_file, csr_file, key_file), + "openssl ca -config %s -days 3650 -in %s -out %s -batch" + % (conf_file, csr_file, crt_file), + ] for command in commands: p = subprocess.Popen( From 86eb9a2405324cab5d9a84335d52aab21ca46580 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:34:12 +0100 Subject: [PATCH 0101/1066] [fix] avoid reverse order log display on web admin --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0133a3387..eec261f20 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -292,8 +292,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - logger.error("Certificate installation for %s failed !", domain) - logger.error(str(e)) + logger.error("Certificate installation for %s failed !\nException: %s", domain, e) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): From 2f4f2546127cf497b1f2511437c98f6a9acd5f65 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:22 +0100 Subject: [PATCH 0102/1066] [mod] pep8 --- src/yunohost/certificate.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index eec261f20..b9e37dc79 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -173,7 +173,7 @@ def _certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") - if (not force) and (os.path.isfile(current_cert_file)): + if not force and os.path.isfile(current_cert_file): status = _get_status(domain) if status["summary"]["code"] in ('good', 'great'): @@ -350,7 +350,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) - if (staging): + if staging: logger.warning( "Please note that you used the --staging option, and that no new certificate will actually be enabled !") @@ -456,11 +456,14 @@ location '/.well-known/acme-challenge' # Check there isn't a conflicting file for the acme-challenge well-known # uri for path in glob.glob('%s/*.conf' % nginx_conf_folder): - if (path == nginx_conf_file): + + if path == nginx_conf_file: continue + with open(path) as f: contents = f.read() - if ('/.well-known/acme-challenge' in contents): + + if '/.well-known/acme-challenge' in contents: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_conflicting_nginx_file', filepath=path)) @@ -512,7 +515,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - if (staging): + if staging: certification_authority = STAGING_CERTIFICATION_AUTHORITY else: certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY @@ -524,7 +527,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): log=logger, CA=certification_authority) except ValueError as e: - if ("urn:acme:error:rateLimited" in str(e)): + if "urn:acme:error:rateLimited" in str(e): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_hit_rate_limit', domain=domain)) else: @@ -544,7 +547,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - if (staging): + if staging: folder_flag = "staging" else: folder_flag = "letsencrypt" @@ -567,7 +570,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(domain_cert_file, "root", "metronome", 0640) - if (staging): + if staging: return _enable_certificate(domain, new_cert_folder) From 4a9b89d12e0ff22379e74bb7dfa85cd4b441de69 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:36 +0100 Subject: [PATCH 0103/1066] [mod] merge double with --- src/yunohost/certificate.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b9e37dc79..fb982980b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -185,10 +185,9 @@ def _certificate_install_selfsigned(domain_list, force=False): # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain - with open(conf_file, "w") as f: - with open(conf_template, "r") as template: - for line in template: - f.write(line.replace("yunohost.org", domain)) + with open(conf_file, "w") as f, open(conf_template, "r") as template: + for line in template: + f.write(line.replace("yunohost.org", domain)) # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert @@ -216,10 +215,9 @@ def _certificate_install_selfsigned(domain_list, force=False): os.symlink(original_ca_file, ca_file) # Append ca.pem at the end of crt.pem - with open(ca_file, "r") as ca_pem: - with open(crt_file, "a") as crt_pem: - crt_pem.write("\n") - crt_pem.write(ca_pem.read()) + with open(ca_file, "r") as ca_pem, open(crt_file, "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) # Set appropriate permissions _set_permissions(new_cert_folder, "root", "root", 0755) From f0c29147dd65c5f819f3ade4e5238549cc9c0fe4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:42 +0100 Subject: [PATCH 0104/1066] [mod] style --- src/yunohost/certificate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index fb982980b..f8a927e08 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -201,7 +201,9 @@ def _certificate_install_selfsigned(domain_list, force=False): for command in commands: p = subprocess.Popen( command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + if p.returncode != 0: logger.warning(out) raise MoulinetteError( @@ -532,8 +534,10 @@ def _fetch_and_enable_new_certificate(domain, staging=False): logger.error(str(e)) raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) + except Exception as e: logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) @@ -552,6 +556,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): new_cert_folder = "%s/%s-history/%s-%s" % ( CERT_FOLDER, domain, date_tag, folder_flag) + os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) From a7289c447adab0ab170e684b4866d322994b2778 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:54:13 +0100 Subject: [PATCH 0105/1066] [fix] typo --- data/actionsmap/yunohost.yml | 2 +- src/yunohost/app.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 56b28cf38..e5af4ee6b 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -437,7 +437,7 @@ app: full: --args help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") -n: - full: --no-remove-on-fail + full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation ### app_remove() TODO: Write help diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f85e9f191..b70b6388a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -440,7 +440,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) -def app_install(auth, app, label=None, args=None, no_remove_on_fail=False): +def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -448,7 +448,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_fail=False): app -- Name, local path or git URL of the app to install label -- Custom name for the app args -- Serialize arguments for app installation - no_remove_on_fail -- Debug option to avoid removing the app on a failed installation + no_remove_on_failure -- Debug option to avoid removing the app on a failed installation """ from yunohost.hook import hook_add, hook_remove, hook_exec @@ -542,7 +542,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_fail=False): logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - if not no_remove_on_fail: + if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} env_dict_remove["YNH_APP_ID"] = app_id From ee5a42eaed64957d8b1319dd4a821aa75793c1cd Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:00:15 +0100 Subject: [PATCH 0106/1066] Remove helper ynh_mkdir_tmp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le helper `ynh_mkdir_tmp` est inutile. Il existe la commande `mktemp -d` pour créer des dossiers temporaires. --- data/helpers.d/filesystem | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 8a2bd9aff..41b570ad1 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,15 +62,3 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } - -# Create a directory under /tmp -# -# usage: ynh_mkdir_tmp -# | ret: the created directory path -ynh_mkdir_tmp() { - TMPDIR="/tmp/$(ynh_string_random 6)" - while [ -d $TMPDIR ]; do - TMPDIR="/tmp/$(ynh_string_random 6)" - done - mkdir -p "$TMPDIR" && echo "$TMPDIR" -} From 261a304f37ea845481b1539e1383de56f4f75d7b Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:19:19 +0100 Subject: [PATCH 0107/1066] Update filesystem --- data/helpers.d/filesystem | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 41b570ad1..d53a75a38 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,3 +62,13 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } + +# Create a directory under /tmp +# +# Deprecated helper +# +# usage: ynh_mkdir_tmp +# | ret: the created directory path +ynh_mkdir_tmp() { + mktemp -d +} From 2bfbada570d9d194ddec39718474e780107d0fa9 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 29 Nov 2016 15:27:51 +0100 Subject: [PATCH 0108/1066] [enh] Add warning about deprecated ynh_mkdir_tmp helper --- data/helpers.d/filesystem | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index d53a75a38..3fc1b0eb8 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -70,5 +70,6 @@ ynh_bind_or_cp() { # usage: ynh_mkdir_tmp # | ret: the created directory path ynh_mkdir_tmp() { + echo "This helper is deprecated, you should use 'mktemp -d' instead." >&2 mktemp -d } From 8bf8534a9a68c9ca324559c1aede7a939d495a52 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Nov 2016 00:02:52 -0500 Subject: [PATCH 0109/1066] [fix] Need to create archives_path even for custom output directory --- src/yunohost/backup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index dd7c73852..7d2e9d445 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -120,8 +120,10 @@ def backup_create(name=None, description=None, output_directory=None, env_var['CAN_BIND'] = 0 else: output_directory = archives_path - if not os.path.isdir(archives_path): - os.mkdir(archives_path, 0750) + + # Create archives directory if it does not exists + if not os.path.isdir(archives_path): + os.mkdir(archives_path, 0750) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) From 6171fbdb0302cf524f1e17517b27f2a16294306b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Nov 2016 00:04:17 -0500 Subject: [PATCH 0110/1066] Keep track of backups with custom directory using symlinks --- locales/en.json | 1 + src/yunohost/backup.py | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index efeb66e69..c50f3d3de 100644 --- a/locales/en.json +++ b/locales/en.json @@ -43,6 +43,7 @@ "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", + "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", "backup_archive_hook_not_exec": "Hook '{hook:s}' not executed in this backup", "backup_archive_name_exists": "The backup's archive name already exists", "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 7d2e9d445..0e991e8fc 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -290,7 +290,7 @@ def backup_create(name=None, description=None, output_directory=None, raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) - # Add files to the arvhice + # Add files to the archive try: tar.add(tmp_dir, arcname='') tar.close() @@ -300,10 +300,22 @@ def backup_create(name=None, description=None, output_directory=None, raise MoulinetteError(errno.EIO, m18n.n('backup_creation_failed')) + # FIXME : it looks weird that the "move info file" is not enabled if + # user activated "no_compress" ... or does it really means + # "dont_keep_track_of_this_backup_in_history" ? + # Move info file shutil.move(tmp_dir + '/info.json', '{:s}/{:s}.info.json'.format(archives_path, name)) + # If backuped to a non-default location, keep a symlink of the archive + # to that location + if output_directory != archives_path: + link = "%s/%s.tar.gz" % (archives_path, name) + os.symlink(archive_file, link) + + + # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() @@ -604,10 +616,21 @@ def backup_info(name, with_details=False, human_readable=False): """ archive_file = '%s/%s.tar.gz' % (archives_path, name) - if not os.path.isfile(archive_file): + + # Check file exist (even if it's a broken symlink) + if not os.path.lexists(archive_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=name)) + # If symlink, retrieve the real path + if os.path.islink(archive_file) : + archive_file = os.path.realpath(archive_file) + + # Raise exception if link is broken (e.g. on unmounted external storage) + if (not os.path.exists(archive_file)) : + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_broken_link', path=archive_file)) + info_file = "%s/%s.info.json" % (archives_path, name) try: with open(info_file) as f: From 2cd18d878615997e59ea39cedddfa0386e3f64e6 Mon Sep 17 00:00:00 2001 From: Zamentur aka ljf Date: Wed, 30 Nov 2016 18:26:40 +0100 Subject: [PATCH 0111/1066] [fix] Support git ynh app with submodules #533 (#174) --- src/yunohost/app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0be58f71c..019bac899 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1258,8 +1258,13 @@ def _fetch_app_from_git(app): url = url[:tree_index] branch = app[tree_index+6:] try: + # We use currently git 2.1 so we can't use --shallow-submodules + # option. When git will be in 2.9 (with the new debian version) + # we will be able to use it. Without this option all the history + # of the submodules repo is downloaded. subprocess.check_call([ - 'git', 'clone', '--depth=1', url, extracted_app_folder]) + 'git', 'clone', '--depth=1', '--recursive', url, + extracted_app_folder]) subprocess.check_call([ 'git', 'reset', '--hard', branch ], cwd=extracted_app_folder) From 777c4833fdf6d2b7ec19d26f69dd99a0e2dc4741 Mon Sep 17 00:00:00 2001 From: opi Date: Thu, 1 Dec 2016 21:33:50 +0100 Subject: [PATCH 0112/1066] Update changelog for 2.5.0 release --- debian/changelog | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/debian/changelog b/debian/changelog index 94ddd58ec..7467b4ff2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,43 @@ +yunohost (2.5.0) testing; urgency=low + + * Certificate management integration (e.g. Let's Encrypt certificate install) + * [fix] Support git ynh app with submodules #533 (#174) + * [enh] display file path on file_not_exist error + * [mod] move a part of os.system calls to native shutil/os + * [fix] Can't restore app on a root domain + + Miscellaneous + + * Update backup.py + * [mod] autopep8 + * [mod] trailing spaces + * [mod] pep8 + * [mod] remove useless imports + * [mod] more pythonic and explicit tests with more verbose errors + * [fix] correctly handle all cases + * [mod] simplier condition + * [fix] uses https + * [mod] uses logger string concatenation api + * [mod] small opti, getting domain list can be slow + * [mod] pylint + * [mod] os.path.join + * [mod] remove useless assign + * [enh] include tracebak into error email + * [mod] remove the summary code concept and switch to code/verbose duet instead + * [mod] I only need to reload nginx, not restart it + * [mod] top level constants should be upper case (pep8) + * Check that the DNS A record matches the global IP now using dnspython and FDN's DNS + * Refactored the self-signed cert generation, some steps were overly complicated for no reason + * Using a single generic skipped regex for acme challenge in ssowat conf + * Adding an option to use the staging Let's Encrypt CA, sort of a dry-run + * [enh] Complete readme (#183) + * [fix] avoid reverse order log display on web admin + + Thanks to all contributors: Aleks, Bram, JimboJoe, ljf, M5oul + Kudos to Aleks for leading the Let's Encrypt integration to YunoHost core \o/ + + -- opi Thu, 01 Dec 2016 21:22:19 +0100 + yunohost (2.4.2) stable; urgency=low [ Laurent Peuch ] From f6188405bc2914583500e4b912397117367248fc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Dec 2016 23:09:02 -0500 Subject: [PATCH 0113/1066] [fix] Fix the way name of self-CA is determined --- locales/en.json | 3 ++- src/yunohost/certificate.py | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/locales/en.json b/locales/en.json index efeb66e69..806def22a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -255,5 +255,6 @@ "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", "certmanager_cert_signing_failed" : "Signing the new certificate failed.", "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first." + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first.", + "certmanager_unable_to_determine_self_CA_name": "Unable to determine name of self-signing authority." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f8a927e08..db99da733 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -56,6 +56,8 @@ WEBROOT_FOLDER = "/tmp/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" +SSL_DIR = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' + KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days @@ -161,11 +163,9 @@ def _certificate_install_selfsigned(domain_list, force=False): new_cert_folder = "%s/%s-history/%s-selfsigned" % ( CERT_FOLDER, domain, date_tag) - original_ca_file = '/etc/ssl/certs/ca-yunohost_crt.pem' - ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - conf_template = os.path.join(ssl_dir, "openssl.cnf") + conf_template = os.path.join(SSL_DIR, "openssl.cnf") - csr_file = os.path.join(ssl_dir, "certs", "yunohost_csr.pem") + csr_file = os.path.join(SSL_DIR, "certs", "yunohost_csr.pem") conf_file = os.path.join(new_cert_folder, "openssl.cnf") key_file = os.path.join(new_cert_folder, "key.pem") crt_file = os.path.join(new_cert_folder, "crt.pem") @@ -214,7 +214,7 @@ def _certificate_install_selfsigned(domain_list, force=False): # Link the CA cert (not sure it's actually needed in practice though, # since we append it at the end of crt.pem. For instance for Let's # Encrypt certs, we only need the crt.pem and key.pem) - os.symlink(original_ca_file, ca_file) + os.symlink(SELF_CA_FILE, ca_file) # Append ca.pem at the end of crt.pem with open(ca_file, "r") as ca_pem, open(crt_file, "a") as crt_pem: @@ -810,9 +810,20 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): - cert = crypto.load_certificate( - crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) - return cert.get_subject().CN + ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") + + try : + with open("%s/openssl.ca.cnf" % SSL_DIR) as f: + lines = f.readlines() + + for line in lines: + if (line.startswith("commonName_default")): + return line.split()[2] + except : + pass + + logger.warning(m18n.n('certmanager_unable_to_determine_self_CA_name')) + return "" def _tail(n, file_path): From 4eaefe5145a41453a6109c81972c31d52bbe43d8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 2 Dec 2016 14:19:20 +0100 Subject: [PATCH 0114/1066] [mod] use unused variable --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index db99da733..c8f2598a2 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -813,7 +813,7 @@ def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") try : - with open("%s/openssl.ca.cnf" % SSL_DIR) as f: + with open(ca_conf) as f: lines = f.readlines() for line in lines: From 005d624f2fd3bb9133158a2fd1dd2992eb0dd6cc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 2 Dec 2016 14:19:28 +0100 Subject: [PATCH 0115/1066] [mod] pep8 --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c8f2598a2..74e93314c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -817,9 +817,9 @@ def _name_self_CA(): lines = f.readlines() for line in lines: - if (line.startswith("commonName_default")): + if line.startswith("commonName_default"): return line.split()[2] - except : + except: pass logger.warning(m18n.n('certmanager_unable_to_determine_self_CA_name')) From 595f897261371d4d76a4725aacab59ea634a93cf Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 11:34:46 +0100 Subject: [PATCH 0116/1066] [fix] implement opi's feedback --- locales/en.json | 2 +- src/yunohost/tools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 5bc3b7bdb..acbd0ed98 100644 --- a/locales/en.json +++ b/locales/en.json @@ -114,7 +114,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it.", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it.", "ldap_initialized": "LDAP has been initialized", - "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin. Aborting.", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user.", "license_undefined": "undefined", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 671a7304c..e92729129 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -100,8 +100,8 @@ def tools_ldapinit(): try: pwd.getpwnam("admin") except KeyError: - raise MoulinetteError(errno.EINVAL, - m18n.n('ldap_init_failed_to_create_admin')) + logger.error(m18n.n('ldap_init_failed_to_create_admin')) + raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed')) logger.success(m18n.n('ldap_initialized')) return auth From 7ee6ab2fbacf42e19cb2abb5d1b14e6fd0885c9b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 11:38:37 +0100 Subject: [PATCH 0117/1066] [mod] implement opi's feedback --- locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index bbb33e937..700df8db4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,8 +209,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. To fix this you need to fix the json syntax error in the SSOwat persistent configuration file which is located there on your filesystem: /etc/ssowat/conf.json.persistent", - "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. To fix this you need to fix the json syntax error in the SSOwat persistent configuration file which is located there on your filesystem: /etc/ssowat/conf.json.persistent", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax.", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax.", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", From 5fee9c13534190888ae7c863bfad1854fa4bd1a3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 11:48:29 +0100 Subject: [PATCH 0118/1066] [mod] remove unused imports --- src/yunohost/tools.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e92729129..93d50e2a8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -24,10 +24,7 @@ Specific tools """ import os -import sys import yaml -import re -import getpass import requests import json import errno @@ -44,9 +41,9 @@ from moulinette.utils.log import getActionLogger from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list from yunohost.domain import domain_add, domain_list, get_public_ip from yunohost.dyndns import dyndns_subscribe -from yunohost.firewall import firewall_upnp, firewall_reload +from yunohost.firewall import firewall_upnp from yunohost.service import service_status, service_regen_conf, service_log -from yunohost.monitor import monitor_disk, monitor_network, monitor_system +from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version apps_setting_path= '/etc/yunohost/apps/' From e348c6214ac3b95e4c993f1700d7970a3c93cba1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 11:52:24 +0100 Subject: [PATCH 0119/1066] [mod] pep8 --- src/yunohost/tools.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 93d50e2a8..ce0ced1ab 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -69,12 +69,16 @@ def tools_ldapinit(): ldap_map = yaml.load(f) for rdn, attr_dict in ldap_map['parents'].items(): - try: auth.add(rdn, attr_dict) - except: pass + try: + auth.add(rdn, attr_dict) + except: + pass for rdn, attr_dict in ldap_map['children'].items(): - try: auth.add(rdn, attr_dict) - except: pass + try: + auth.add(rdn, attr_dict) + except: + pass admin_dict = { 'cn': 'admin', @@ -170,7 +174,8 @@ def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): try: with open('/etc/yunohost/installed', 'r') as f: service_regen_conf() - except IOError: pass + except IOError: + pass logger.success(m18n.n('maindomain_changed')) @@ -199,6 +204,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): else: dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) + if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: dyndns = True @@ -222,8 +228,10 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ] for folder in folders_to_create: - try: os.listdir(folder) - except OSError: os.makedirs(folder) + try: + os.listdir(folder) + except OSError: + os.makedirs(folder) # Change folders permissions os.system('chmod 755 /home/yunohost.app') @@ -308,6 +316,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): logger.info(m18n.n('updating_apt_cache')) if not cache.update(): raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) + logger.info(m18n.n('done')) cache.open(None) @@ -355,7 +364,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): if len(apps) == 0 and len(packages) == 0: logger.info(m18n.n('packages_no_upgrade')) - return { 'packages': packages, 'apps': apps } + return {'packages': packages, 'apps': apps} def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): @@ -388,6 +397,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): critical_upgrades.add(pkg.name) # Temporarily keep package ... pkg.mark_keep() + # ... and set a hourly cron up to upgrade critical packages if critical_upgrades: logger.info(m18n.n('packages_upgrade_critical_later', @@ -397,6 +407,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) + try: # Apply APT changes # TODO: Logs output for the API @@ -424,7 +435,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): # Return API logs if it is an API call if is_api: - return { "log": service_log('yunohost-api', number="100").values()[0] } + return {"log": service_log('yunohost-api', number="100").values()[0]} def tools_diagnosis(auth, private=False): @@ -483,6 +494,7 @@ def tools_diagnosis(auth, private=False): # Services status services = service_status() diagnosis['services'] = {} + for service in services: diagnosis['services'][service] = "%s (%s)" % (services[service]['status'], services[service]['loaded']) From 110d481cd12ffa05407eaa58ae3af24b2b289e0e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 12:00:55 +0100 Subject: [PATCH 0120/1066] [mod] remove unused imports --- src/yunohost/app.py | 2 -- src/yunohost/backup.py | 1 - src/yunohost/hook.py | 3 --- src/yunohost/monitor.py | 2 +- src/yunohost/service.py | 2 +- src/yunohost/user.py | 1 - 6 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 019bac899..a30690018 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -24,10 +24,8 @@ Manage apps """ import os -import sys import json import shutil -import stat import yaml import time import re diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index dd7c73852..f2a48735c 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -25,7 +25,6 @@ """ import os import re -import sys import json import errno import time diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 2d46cfcd5..db7cd9504 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -24,11 +24,8 @@ Manage hooks """ import os -import sys import re -import json import errno -import subprocess from glob import iglob from moulinette.core import MoulinetteError diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index d0fe224e9..315fb3a08 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -35,7 +35,7 @@ import errno import os import dns.resolver import cPickle as pickle -from datetime import datetime, timedelta +from datetime import datetime from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ab26dd2bc..b1b776326 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -36,7 +36,7 @@ from difflib import unified_diff from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem -from yunohost.hook import hook_list, hook_callback +from yunohost.hook import hook_callback base_conf_path = '/home/yunohost.conf' diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ec7dd539c..f5ac25ea8 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -30,7 +30,6 @@ import string import json import errno import subprocess -import math import re from moulinette.core import MoulinetteError From f43750b4d6f69c08bd1772510b6c788dd7a4a465 Mon Sep 17 00:00:00 2001 From: julienmalik Date: Sun, 4 Dec 2016 11:55:06 +0100 Subject: [PATCH 0121/1066] [fix] Fix up indentation in actionmap Reported by beudbeud on the chan --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b410159a8..26543721e 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -500,7 +500,7 @@ app: full: --args help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") -n: - full: --no-remove-on-failure + full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation ### app_remove() TODO: Write help From c2065f0a7222f9d00111e4ed47a35b3b15d2f33f Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 4 Dec 2016 15:32:34 +0100 Subject: [PATCH 0122/1066] [fix] Improve dnssec key generation on low entropy devices See https://bugs.launchpad.net/ubuntu/+source/bind9/+bug/963368 --- src/yunohost/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 878bc577e..b32cde0da 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -95,7 +95,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None logger.info(m18n.n('dyndns_key_generating')) os.system('cd /etc/yunohost/dyndns && ' \ - 'dnssec-keygen -a hmac-md5 -b 128 -n USER %s' % domain) + 'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -n USER %s' % domain) os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0] From 397aeb2498425d07e9ca60d56e536fe07500bdc9 Mon Sep 17 00:00:00 2001 From: opi Date: Mon, 5 Dec 2016 10:42:38 +0100 Subject: [PATCH 0123/1066] [enh] Add haveged as dependency Increase entropy on virtual servers. Speed up key generation by openssl and dnssec See https://github.com/YunoHost/yunohost/pull/201 & https://bugs.launchpad.net/ubuntu/+source/bind9/+bug/963368 --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index 80f562e76..ec44a06b1 100644 --- a/debian/control +++ b/debian/control @@ -25,6 +25,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsmasq, openssl, avahi-daemon , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools + , haveged Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper From 87bdc5a5ce46cc7c9c38c90444c558f2ca76c32f Mon Sep 17 00:00:00 2001 From: opi Date: Mon, 5 Dec 2016 10:45:55 +0100 Subject: [PATCH 0124/1066] [fix] Add missing dependency to nscd package #656 See https://dev.yunohost.org/issues/656 nscd is called during user_create and user_delete, but this package is only in Recommends for libnss-ldapd. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 80f562e76..5fb816c61 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsutils, bind9utils, unzip, git, curl, cron , ca-certificates, netcat-openbsd, iproute , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd - , slapd, ldap-utils, sudo-ldap, libnss-ldapd + , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban From 8bd130dc86eaf35239bdd362697f7d9f63abffa9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 5 Dec 2016 18:35:47 +0100 Subject: [PATCH 0125/1066] [mod] remove unused variable (pyflake) --- src/yunohost/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3702eb1b4..153a13575 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -995,7 +995,6 @@ def app_ssowatconf(auth): redirected_regex = { main_domain +'/yunohost[\/]?$': 'https://'+ main_domain +'/yunohost/sso/' } redirected_urls ={} - apps = {} try: apps_list = app_list()['apps'] except: From 731ff6f906bea5ed9543ed49d0180e8360dc7fbd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Nov 2016 19:36:57 -0500 Subject: [PATCH 0126/1066] [fix] Refactoring tools_maindomain and disabling removal of main domain to avoid breaking things --- data/actionsmap/yunohost.yml | 7 ++-- locales/en.json | 3 +- src/yunohost/domain.py | 14 ++++++++ src/yunohost/tools.py | 62 ++++++++++++++++-------------------- 4 files changed, 46 insertions(+), 40 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 26543721e..91c5b2aad 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1266,7 +1266,7 @@ tools: ### tools_maindomain() maindomain: - action_help: Main domain change tool + action_help: Check the current main domain, or change it api: - GET /domains/main - PUT /domains/main @@ -1274,12 +1274,9 @@ tools: authenticate: all lock: false arguments: - -o: - full: --old-domain - extra: - pattern: *pattern_domain -n: full: --new-domain + help: Change the current main domain extra: pattern: *pattern_domain diff --git a/locales/en.json b/locales/en.json index e44f87778..6d0ec34bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -258,5 +258,6 @@ "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", "certmanager_cert_signing_failed" : "Signing the new certificate failed.", "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first." + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first.", + "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first" } diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 415585087..9c1563423 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -113,6 +113,7 @@ def domain_add(auth, domain, dyndns=False): raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_root_unknown')) + try: yunohost.certificate._certificate_install_selfsigned([domain], False) @@ -158,6 +159,10 @@ def domain_remove(auth, domain, force=False): if not force and domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + # Check domain is not the main domain + if (domain == _get_maindomain()) : + raise MoulinetteError(errno.EINVAL, m18n.n('domain_cannot_remove_main')) + # Check if apps are installed on the domain for app in os.listdir('/etc/yunohost/apps/'): with open('/etc/yunohost/apps/' + app +'/settings.yml') as f: @@ -284,3 +289,12 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + +def _get_maindomain(): + with open('/etc/yunohost/current_host', 'r') as f: + maindomain = f.readline().rstrip() + return maindomain + +def _set_maindomain(domain): + with open('/etc/yunohost/current_host', 'w') as f: + f.write(domain) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 475bf9b7f..51acc410d 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -39,7 +39,7 @@ import apt.progress from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list -from yunohost.domain import domain_add, domain_list, get_public_ip +from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import dyndns_subscribe from yunohost.firewall import firewall_upnp from yunohost.service import service_status, service_regen_conf, service_log @@ -127,50 +127,43 @@ def tools_adminpw(auth, new_password): def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): """ - Main domain change tool + Main domain consultaton or change tool Keyword argument: - new_domain - old_domain + new_domain -- The new domain to be set as the main domain """ - if not old_domain: - with open('/etc/yunohost/current_host', 'r') as f: - old_domain = f.readline().rstrip() - - if not new_domain: - return { 'current_main_domain': old_domain } + # If no new domain specified, we return the current main domain if not new_domain: - raise MoulinetteError(errno.EINVAL, m18n.n('new_domain_required')) + return { 'current_main_domain': _get_maindomain() } + + # Check domain exists if new_domain not in domain_list(auth)['domains']: - domain_add(auth, new_domain) + raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - os.system('rm /etc/ssl/private/yunohost_key.pem') - os.system('rm /etc/ssl/certs/yunohost_crt.pem') + # Apply changes to ssl certs + ssl_key = "/etc/ssl/private/yunohost_key.pem" + ssl_crt = "/etc/ssl/private/yunohost_crt.pem" + new_ssl_key = "/etc/yunohost/certs/%s/key.pem" % new_domain + new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain - command_list = [ - 'ln -s /etc/yunohost/certs/%s/key.pem /etc/ssl/private/yunohost_key.pem' % new_domain, - 'ln -s /etc/yunohost/certs/%s/crt.pem /etc/ssl/certs/yunohost_crt.pem' % new_domain, - 'echo %s > /etc/yunohost/current_host' % new_domain, - ] + try: - for command in command_list: - if os.system(command) != 0: - raise MoulinetteError(errno.EPERM, - m18n.n('maindomain_change_failed')) + if os.path.exists(ssl_key) or os.path.lexists(ssl_key) : + os.remove(ssl_key) + if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt) : + os.remove(ssl_crt) - if dyndns and len(new_domain.split('.')) >= 3: - try: - r = requests.get('https://dyndns.yunohost.org/domains') - except requests.ConnectionError: - pass - else: - dyndomains = json.loads(r.text) - dyndomain = '.'.join(new_domain.split('.')[1:]) - if dyndomain in dyndomains: - dyndns_subscribe(domain=new_domain) + os.symlink(new_ssl_key, ssl_key) + os.symlink(new_ssl_crt, ssl_crt) + _set_maindomain(new_domain) + except Exception as e: + print e + raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) + + # Regen configurations try: with open('/etc/yunohost/installed', 'r') as f: service_regen_conf() @@ -285,7 +278,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): m18n.n('yunohost_ca_creation_failed')) # New domain config - tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain, dyndns=dyndns) + domain_add(auth, new_domain, dyndns) + tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain) # Generate SSOwat configuration file app_ssowatconf(auth) From 597da8be465309356b7b16f9766ba8f3449d924c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 25 Nov 2016 03:03:41 +0100 Subject: [PATCH 0127/1066] Fixing previous commit after postinstall tests --- src/yunohost/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 51acc410d..8e1603d83 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -125,7 +125,7 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -def tools_maindomain(auth, old_domain=None, new_domain=None, dyndns=False): +def tools_maindomain(auth, new_domain=None): """ Main domain consultaton or change tool @@ -278,8 +278,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): m18n.n('yunohost_ca_creation_failed')) # New domain config - domain_add(auth, new_domain, dyndns) - tools_maindomain(auth, old_domain='yunohost.org', new_domain=domain) + domain_add(auth, domain, dyndns) + tools_maindomain(auth, domain) # Generate SSOwat configuration file app_ssowatconf(auth) From 9c798e5d62c0230823c3b1a317974ce32a79ebd6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Dec 2016 23:40:37 -0500 Subject: [PATCH 0128/1066] [fix] Fixing message --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8e1603d83..d8b5f7f7e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -127,7 +127,7 @@ def tools_adminpw(auth, new_password): def tools_maindomain(auth, new_domain=None): """ - Main domain consultaton or change tool + Check the current main domain, or change it Keyword argument: new_domain -- The new domain to be set as the main domain From c468a6975446a839ad1e3c12acbf82494959fae8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 2 Dec 2016 23:23:06 +0100 Subject: [PATCH 0129/1066] [mod] pep8 --- src/yunohost/domain.py | 3 ++- src/yunohost/tools.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 9c1563423..1aeab33ef 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -160,7 +160,7 @@ def domain_remove(auth, domain, force=False): raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) # Check domain is not the main domain - if (domain == _get_maindomain()) : + if domain == _get_maindomain(): raise MoulinetteError(errno.EINVAL, m18n.n('domain_cannot_remove_main')) # Check if apps are installed on the domain @@ -290,6 +290,7 @@ def get_public_ip(protocol=4): raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + def _get_maindomain(): with open('/etc/yunohost/current_host', 'r') as f: maindomain = f.readline().rstrip() diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index d8b5f7f7e..2c1c55d77 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -136,7 +136,7 @@ def tools_maindomain(auth, new_domain=None): # If no new domain specified, we return the current main domain if not new_domain: - return { 'current_main_domain': _get_maindomain() } + return {'current_main_domain': _get_maindomain()} # Check domain exists if new_domain not in domain_list(auth)['domains']: @@ -149,7 +149,6 @@ def tools_maindomain(auth, new_domain=None): new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain try: - if os.path.exists(ssl_key) or os.path.lexists(ssl_key) : os.remove(ssl_key) if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt) : From c763b74fea5db85a92daf8c5c5e11598b42abc90 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 2 Dec 2016 23:23:16 +0100 Subject: [PATCH 0130/1066] [mod] use logger instead of print, a bit better bot not perfect --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 2c1c55d77..bd3bb9696 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -159,7 +159,7 @@ def tools_maindomain(auth, new_domain=None): _set_maindomain(new_domain) except Exception as e: - print e + logger.warning("%s" % e, exc_info=1) raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) # Regen configurations From 801ceca32b26d07be9a7e976518538bf4419c6c2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 2 Dec 2016 23:50:22 +0100 Subject: [PATCH 0131/1066] [mod] pep8 --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index bd3bb9696..7cc221683 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -149,9 +149,9 @@ def tools_maindomain(auth, new_domain=None): new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_domain try: - if os.path.exists(ssl_key) or os.path.lexists(ssl_key) : + if os.path.exists(ssl_key) or os.path.lexists(ssl_key): os.remove(ssl_key) - if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt) : + if os.path.exists(ssl_crt) or os.path.lexists(ssl_crt): os.remove(ssl_crt) os.symlink(new_ssl_key, ssl_key) From d85657a5c60afc8c9387e830d8250dd0e950dd18 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 7 Dec 2016 21:54:13 +0100 Subject: [PATCH 0132/1066] [mod] style --- src/yunohost/user.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 348a23d32..6c73c5ba0 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -399,30 +399,38 @@ def user_info(auth, username): if 'mailuserquota' in user: userquota = user['mailuserquota'][0] - if isinstance( userquota, int ): + + if isinstance(userquota, int): userquota = str(userquota) # Test if userquota is '0' or '0M' ( quota pattern is ^(\d+[bkMGT])|0$ ) is_limited = not re.match('0[bkMGT]?', userquota) storage_use = '?' + cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] + try: - cmd_result = subprocess.check_output(cmd,stderr=subprocess.STDOUT, + cmd_result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) # Exemple of return value for cmd: # """Quota name=User quota Type=STORAGE Value=0 Limit=- %=0 # Quota name=User quota Type=MESSAGE Value=0 Limit=- %=0""" - has_value=re.search(r'Value=(\d+)', cmd_result) + has_value = re.search(r'Value=(\d+)', cmd_result) + if has_value: storage_use = int(has_value.group(1)) storage_use = _convertSize(storage_use) + if is_limited: - has_percent=re.search(r'%=(\d+)', cmd_result) + has_percent = re.search(r'%=(\d+)', cmd_result) + if has_percent: percentage = int(has_percent.group(1)) - storage_use += ' (%s%s)' % (percentage, '%') + storage_use += ' (%s%%)' % percentage + except subprocess.CalledProcessError: logger.warning(m18n.n('mailbox_used_space_dovecot_down')) + result_dict['mailbox-quota'] = { 'limit' : userquota if is_limited else m18n.n('unlimit'), 'use' : storage_use From 6aa64a071ffd4cf16447ee1ed9f492a5906315ea Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 7 Dec 2016 16:14:01 -0500 Subject: [PATCH 0133/1066] Improving exception handling --- locales/en.json | 3 ++- src/yunohost/certificate.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/locales/en.json b/locales/en.json index 806def22a..7de00f395 100644 --- a/locales/en.json +++ b/locales/en.json @@ -256,5 +256,6 @@ "certmanager_cert_signing_failed" : "Signing the new certificate failed.", "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first.", - "certmanager_unable_to_determine_self_CA_name": "Unable to determine name of self-signing authority." + "certmanager_self_ca_conf_file_not_found" : "Configuration file not found for self-signing CA ({file:s})", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority in {file:s}." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 74e93314c..8209160a4 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -812,17 +812,18 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") - try : - with open(ca_conf) as f: - lines = f.readlines() + if not os.path.exists(ca_conf) : + logger.warning(m18n.n('certmanager_self_ca_conf_file_not_found', file=ca_conf)) + return "" - for line in lines: - if line.startswith("commonName_default"): - return line.split()[2] - except: - pass + with open(ca_conf) as f: + lines = f.readlines() - logger.warning(m18n.n('certmanager_unable_to_determine_self_CA_name')) + for line in lines: + if line.startswith("commonName_default"): + return line.split()[2] + + logger.warning(m18n.n('certmanager_unable_to_parse_self_CA_name', file=ca_conf)) return "" From 1b6d8b64637bbebf630139315c5674e5733b57ad Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 9 Dec 2016 00:50:28 +0100 Subject: [PATCH 0134/1066] [mod] pep8 --- src/yunohost/backup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 0e991e8fc..4aec59598 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -623,15 +623,16 @@ def backup_info(name, with_details=False, human_readable=False): m18n.n('backup_archive_name_unknown', name=name)) # If symlink, retrieve the real path - if os.path.islink(archive_file) : + if os.path.islink(archive_file): archive_file = os.path.realpath(archive_file) # Raise exception if link is broken (e.g. on unmounted external storage) - if (not os.path.exists(archive_file)) : + if not os.path.exists(archive_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_broken_link', path=archive_file)) info_file = "%s/%s.info.json" % (archives_path, name) + try: with open(info_file) as f: # Retrieve backup info From 0d6cf499a0b858e7009245fd11a9210188047c13 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 9 Dec 2016 07:34:31 +0100 Subject: [PATCH 0135/1066] [fix] Bracket in passwd from ynh_string_random With bracket we get string like "3KFEc[xm4[udnZ6pDJ8LvP0n" Without "El0nMO3thacDwrjeRzgKnb30" --- data/helpers.d/string | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index a2bf0d463..1a848d239 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -6,6 +6,6 @@ # | arg: length - the string length to generate (default: 24) ynh_string_random() { dd if=/dev/urandom bs=1 count=200 2> /dev/null \ - | tr -c -d '[A-Za-z0-9]' \ + | tr -c -d 'A-Za-z0-9' \ | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' } From ba1357fb949c9a24fc25faf0ecf56695a837ac4d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 11 Dec 2016 14:15:39 +0100 Subject: [PATCH 0136/1066] [mod] uniformise strings following jibec comment --- locales/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 587dd34ae..d996a5c7a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -257,9 +257,9 @@ "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", "certmanager_cert_signing_failed" : "Signing the new certificate failed.", - "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", + "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file: {file:s})", "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first.", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", - "certmanager_self_ca_conf_file_not_found" : "Configuration file not found for self-signing CA ({file:s})", - "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority in {file:s}." + "certmanager_self_ca_conf_file_not_found" : "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } From c9bd7e86332c12dcb15ab7b44212343f1bbc4d96 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 11 Dec 2016 14:18:58 +0100 Subject: [PATCH 0137/1066] [mod] english typography/json format --- locales/en.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locales/en.json b/locales/en.json index d996a5c7a..977ea26c1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -241,25 +241,25 @@ "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", "domain_cert_gen_failed": "Unable to generate certificate", - "certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", "certmanager_domain_unknown": "Unknown domain {domain:s}", - "certmanager_domain_cert_not_selfsigned" : "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", - "certmanager_attempt_to_renew_nonLE_cert" : "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", - "certmanager_attempt_to_renew_valid_cert" : "The certificate for domain {domain:s} is not about to expire ! Use --force to bypass", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", - "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", - "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", - "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", - "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", - "certmanager_cert_signing_failed" : "Signing the new certificate failed.", - "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first.", + "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", + "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", + "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", + "certmanager_hit_rate_limit":"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", + "certmanager_cert_signing_failed": "Signing the new certificate failed.", + "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first.", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", - "certmanager_self_ca_conf_file_not_found" : "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } From fb8394a68e5800000fecb758591e260c89eb3721 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 11 Dec 2016 14:21:20 +0100 Subject: [PATCH 0138/1066] [mod] remove '.' at the end of a string --- locales/en.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/locales/en.json b/locales/en.json index 977ea26c1..b7ecf2325 100644 --- a/locales/en.json +++ b/locales/en.json @@ -57,7 +57,7 @@ "backup_hook_unknown": "Backup hook '{hook:s}' unknown", "backup_invalid_archive": "Invalid backup archive", "backup_nothings_done": "There is nothing to save", - "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders.", + "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", "backup_running_app_script": "Running backup script of app '{app:s}'...", @@ -79,11 +79,11 @@ "domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "Domain already exists", - "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal.", + "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", "domain_unknown": "Unknown domain", "domain_zone_exists": "DNS zone file already exists", "domain_zone_not_found": "DNS zone file not found for domain {:s}", - "done": "Done.", + "done": "Done", "downloading": "Downloading...", "dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", @@ -102,7 +102,7 @@ "field_invalid": "Invalid field '{:s}'", "firewall_reload_failed": "Unable to reload the firewall", "firewall_reloaded": "The firewall has been reloaded", - "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", + "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log", "format_datetime_short": "%m/%d/%Y %I:%M %p", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", @@ -110,10 +110,10 @@ "hook_name_unknown": "Unknown hook name '{name:s}'", "installation_complete": "Installation complete", "installation_failed": "Installation failed", - "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it.", - "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it.", + "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", + "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_initialized": "LDAP has been initialized", - "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user.", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "license_undefined": "undefined", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", @@ -209,8 +209,8 @@ "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax.", - "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax.", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", "system_upgraded": "The system has been upgraded", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", @@ -239,7 +239,7 @@ "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", "domain_cert_gen_failed": "Unable to generate certificate", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", "certmanager_domain_unknown": "Unknown domain {domain:s}", @@ -247,18 +247,18 @@ "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", - "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", - "certmanager_hit_rate_limit":"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", - "certmanager_cert_signing_failed": "Signing the new certificate failed.", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_hit_rate_limit":"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_cert_signing_failed": "Signing the new certificate failed", "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first.", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" From 3464415a77a3feb4e8a934fe6ec15aceec4c2b94 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 11 Dec 2016 15:34:33 +0100 Subject: [PATCH 0139/1066] Update changelog for 2.5.1 release --- debian/changelog | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/debian/changelog b/debian/changelog index 7467b4ff2..34d883ca1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,22 @@ +yunohost (2.5.1) testing; urgency=low + + * [fix] Raise error on malformed SSOwat persistent conf. + * [enh] Catch SSOwat persistent configuration write error. + * [fix] Write SSOwat configuration file only if needed. + * [enh] Display full exception error message. + * [enh] cli option to avoid removing an application on installation failure + * [mod] give instructions on how to solve the conf.json.persistant parsing error + * [fix] avoid random bug on post-install due to nscd cache + * [enh] Adding check that user is actually created + minor refactor of ldap/auth init + * [fix] Fix the way name of self-CA is determined + * [fix] Add missing dependency to nscd package #656 + * [fix] Refactoring tools_maindomain and disabling removal of main domain to avoid breaking things + * [fix] Bracket in passwd from ynh_string_random + + Thanks to all contributors: Aleks, Bram, ju, jibec, ljf, M5oul, opi + + -- Laurent Peuch Sun, 11 Dec 2016 15:26:21 +0100 + yunohost (2.5.0) testing; urgency=low * Certificate management integration (e.g. Let's Encrypt certificate install) From 4897c39edfcdb2e30e8855fbc17b31966c33cb8c Mon Sep 17 00:00:00 2001 From: Moul Date: Sun, 11 Dec 2016 18:14:42 +0100 Subject: [PATCH 0140/1066] =?UTF-8?q?[fix]=20[#662](https://dev.yunohost.o?= =?UTF-8?q?rg/issues/662):=C2=A0missing=20'python-openssl'=20dependency=20?= =?UTF-8?q?for=20Let's=20Encrypt=20integration.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 5fb816c61..480cde40b 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Package: yunohost Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.3.5.1) - , python-psutil, python-requests, python-dnspython + , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc , glances , dnsutils, bind9utils, unzip, git, curl, cron From 71672d371e1626955afd9fdb796b23dc77fd2a30 Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 11 Dec 2016 22:58:56 +0100 Subject: [PATCH 0141/1066] [fix] --no-remove-on-failure for app install should behave as a flag. --- data/actionsmap/yunohost.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 91c5b2aad..25eb6cf6d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -502,6 +502,7 @@ app: -n: full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation + action: store_true ### app_remove() TODO: Write help remove: From f6648f360cf7d7088cba04f4fac996ce10be1789 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 5 Dec 2016 18:44:31 +0100 Subject: [PATCH 0142/1066] [fix] don't remove trailing char if it's not a slash --- src/yunohost/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 153a13575..95146ad64 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1011,19 +1011,19 @@ def app_ssowatconf(auth): for item in _get_setting(app_settings, 'skipped_uris'): if item[-1:] == '/': item = item[:-1] - skipped_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'skipped_regex'): skipped_regex.append(item) for item in _get_setting(app_settings, 'unprotected_uris'): if item[-1:] == '/': item = item[:-1] - unprotected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'unprotected_regex'): unprotected_regex.append(item) for item in _get_setting(app_settings, 'protected_uris'): if item[-1:] == '/': item = item[:-1] - protected_urls.append(app_settings['domain'] + app_settings['path'][:-1] + item) + protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) for item in _get_setting(app_settings, 'protected_regex'): protected_regex.append(item) if 'redirected_urls' in app_settings: From 0e2cae4f1b473b6734fda28066b80ce38c884c7c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 11 Dec 2016 17:59:39 -0500 Subject: [PATCH 0143/1066] Use service_status to check if dovecot is running --- src/yunohost/user.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 6c73c5ba0..65cfddae2 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -35,6 +35,7 @@ import re from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from yunohost.service import service_status logger = getActionLogger('yunohost.user') @@ -407,9 +408,10 @@ def user_info(auth, username): is_limited = not re.match('0[bkMGT]?', userquota) storage_use = '?' - cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] - - try: + if (service_status("dovecot")["status"] != "running"): + logger.warning(m18n.n('mailbox_used_space_dovecot_down')) + else: + cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] cmd_result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True) # Exemple of return value for cmd: @@ -428,9 +430,6 @@ def user_info(auth, username): percentage = int(has_percent.group(1)) storage_use += ' (%s%%)' % percentage - except subprocess.CalledProcessError: - logger.warning(m18n.n('mailbox_used_space_dovecot_down')) - result_dict['mailbox-quota'] = { 'limit' : userquota if is_limited else m18n.n('unlimit'), 'use' : storage_use From 6f313d943bb7ce47598b9c61336f3bc34d967966 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 12 Dec 2016 00:14:22 +0100 Subject: [PATCH 0144/1066] [mod] pep8 --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 65cfddae2..08ab4c649 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -408,7 +408,7 @@ def user_info(auth, username): is_limited = not re.match('0[bkMGT]?', userquota) storage_use = '?' - if (service_status("dovecot")["status"] != "running"): + if service_status("dovecot")["status"] != "running": logger.warning(m18n.n('mailbox_used_space_dovecot_down')) else: cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] From 656bb809d137fb5e4613d1ad767a9af7db28b0f3 Mon Sep 17 00:00:00 2001 From: Moul Date: Mon, 12 Dec 2016 00:22:50 +0100 Subject: [PATCH 0145/1066] [mod] certificate: pep8. --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8209160a4..04d755692 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -812,7 +812,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") - if not os.path.exists(ca_conf) : + if not os.path.exists(ca_conf): logger.warning(m18n.n('certmanager_self_ca_conf_file_not_found', file=ca_conf)) return "" From 64af6a1dbf6ad61bd7c5e19c49fe9721a53f5aa8 Mon Sep 17 00:00:00 2001 From: likeitneverwentaway Date: Mon, 12 Dec 2016 09:03:50 +0100 Subject: [PATCH 0146/1066] [enh] List available domains when installing an app by CLI. --- locales/en.json | 1 + src/yunohost/app.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index dfb8ffa4f..a8a55dd26 100644 --- a/locales/en.json +++ b/locales/en.json @@ -84,6 +84,7 @@ "domain_zone_exists": "DNS zone file already exists", "domain_zone_not_found": "DNS zone file not found for domain {:s}", "done": "Done", + "domains_available": "Available domains:", "downloading": "Downloading...", "dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 95146ad64..85693a0dd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1497,7 +1497,7 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - from yunohost.domain import domain_list + from yunohost.domain import domain_list, _get_maindomain from yunohost.user import user_info args_dict = OrderedDict() @@ -1537,6 +1537,13 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): # Check for a password argument is_password = True if arg_type == 'password' else False + if arg_type == 'domain': + arg_default = _get_maindomain() + ask_string += ' (default: {0})'.format(arg_default) + msignals.display(m18n.n('domains_available')) + for domain in domain_list(auth)['domains']: + msignals.display("- {}".format(domain)) + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: From a8a682ac7e2ac4bacafccc98f83e3b1c046c1726 Mon Sep 17 00:00:00 2001 From: Moul Date: Mon, 12 Dec 2016 19:18:03 +0100 Subject: [PATCH 0147/1066] [enh] add empty file for esperanto to enable it on weblate --- locales/eo.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/eo.json diff --git a/locales/eo.json b/locales/eo.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/eo.json @@ -0,0 +1 @@ +{} From 7717c58de4c8cda615165d84a12a30dd73eea58a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 25 Nov 2016 00:15:03 +0100 Subject: [PATCH 0148/1066] Revert "[fix] Can't restore app on a root domain" --- src/yunohost/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 95146ad64..b6657539e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -917,6 +917,10 @@ def app_checkurl(auth, url, app=None): raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) if domain in apps_map: + # Domain already has apps on sub path + if path == '/': + raise MoulinetteError(errno.EPERM, + m18n.n('app_location_install_failed')) # Loop through apps for p, a in apps_map[domain].items(): # Skip requested app checking @@ -926,7 +930,7 @@ def app_checkurl(auth, url, app=None): if path == p: raise MoulinetteError(errno.EINVAL, m18n.n('app_location_already_used')) - elif path.startswith(p) or p.startswith(path): + elif path.startswith(p): raise MoulinetteError(errno.EPERM, m18n.n('app_location_install_failed')) From 67cd9d9b22390579c3fa84439856d2a02c86879c Mon Sep 17 00:00:00 2001 From: Philip Gatzka Date: Fri, 29 Jul 2016 11:05:25 +0200 Subject: [PATCH 0149/1066] [i18n] Translated using Weblate (German) Currently translated at 63.5% (152 of 239 strings) --- locales/de.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 1331c56b4..0fff3e0b4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -209,5 +209,14 @@ "yunohost_ca_creation_failed": "Zertifikatsstelle konnte nicht erstellt werden", "yunohost_configured": "YunoHost wurde erfolgreich konfiguriert", "yunohost_installing": "YunoHost wird installiert...", - "yunohost_not_installed": "Die YunoHost ist unvollständig. Bitte 'yunohost tools postinstall' ausführen." + "yunohost_not_installed": "Die YunoHost ist unvollständig. Bitte 'yunohost tools postinstall' ausführen.", + "app_not_properly_removed": "{app:s} wurde nicht ordnungsgemäß entfernt", + "service_regenconf_failed": "Konnte die Konfiguration für folgende Dienste nicht neu erzeugen: {services}", + "not_enough_disk_space": "Zu wenig Speicherplatz unter '{path:s}' verfügbar", + "backup_creation_failed": "Erzeugung des Backups fehlgeschlagen", + "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell", + "package_not_installed": "Paket '{pkgname}' ist nicht installiert", + "pattern_positive_number": "Muss eine positive Zahl sein", + "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", + "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf" } From 8c1c5f6ce85db2fc40888ff76f8f64c512191d71 Mon Sep 17 00:00:00 2001 From: Anmol Date: Fri, 29 Jul 2016 13:58:09 +0200 Subject: [PATCH 0150/1066] [i18n] Translated using Weblate (Hindi) Currently translated at 4.1% (10 of 239 strings) --- locales/hi.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index 0967ef424..e202d8378 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -1 +1,12 @@ -{} +{ + "action_invalid": "अवैध कार्रवाई '{action:s}'", + "admin_password": "व्यवस्थापक पासवर्ड", + "admin_password_change_failed": "पासवर्ड बदलने में असमर्थ", + "admin_password_changed": "व्यवस्थापक पासवर्ड बदल दिया गया है", + "app_already_installed": "'{app:s}' पहले से ही इंस्टाल्ड है", + "app_argument_choice_invalid": "गलत तर्क का चयन किया गया '{name:s}' , तर्क इन विकल्पों में से होने चाहिए {choices:s}", + "app_argument_invalid": "तर्क के लिए अमान्य मान '{name:s}': {error:s}", + "app_argument_required": "तर्क '{name:s}' की आवश्यकता है", + "app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ", + "app_id_invalid": "अवैध एप्लिकेशन id" +} From 5a12996017ba087402510cf49b18111a379d2e9a Mon Sep 17 00:00:00 2001 From: Anmol Date: Tue, 2 Aug 2016 22:31:47 +0200 Subject: [PATCH 0151/1066] [i18n] Translated using Weblate (Hindi) Currently translated at 33.0% (79 of 239 strings) --- locales/hi.json | 71 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index e202d8378..2ed328282 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -8,5 +8,74 @@ "app_argument_invalid": "तर्क के लिए अमान्य मान '{name:s}': {error:s}", "app_argument_required": "तर्क '{name:s}' की आवश्यकता है", "app_extraction_failed": "इन्सटाल्ड फ़ाइलों को निकालने में असमर्थ", - "app_id_invalid": "अवैध एप्लिकेशन id" + "app_id_invalid": "अवैध एप्लिकेशन id", + "app_incompatible": "यह एप्लिकेशन युनोहोस्ट की इस वर्जन के लिए नहीं है", + "app_install_files_invalid": "फाइलों की अमान्य स्थापना", + "app_location_already_used": "इस लोकेशन पे पहले से ही कोई एप्लीकेशन इन्सटाल्ड है", + "app_location_install_failed": "इस लोकेशन पे एप्लीकेशन इंस्टाल करने में असमर्थ", + "app_manifest_invalid": "एप्लीकेशन का मैनिफेस्ट अमान्य", + "app_no_upgrade": "कोई भी एप्लीकेशन को अपडेट की जरूरत नहीं", + "app_not_correctly_installed": "{app:s} ठीक ढंग से इनस्टॉल नहीं हुई", + "app_not_installed": "{app:s} इनस्टॉल नहीं हुई", + "app_not_properly_removed": "{app:s} ठीक ढंग से नहीं अनइन्सटॉल की गई", + "app_package_need_update": "इस एप्लीकेशन पैकेज को युनोहोस्ट के नए बदलावों/गाइडलिनेज़ के कारण उपडटेशन की जरूरत", + "app_removed": "{app:s} को अनइन्सटॉल कर दिया गया", + "app_requirements_checking": "जरूरी पैकेजेज़ की जाँच हो रही है ....", + "app_requirements_failed": "आवश्यकताओं को पूरा करने में असमर्थ: {error}", + "app_requirements_unmeet": "आवश्यकताए पूरी नहीं हो सकी, पैकेज {pkgname}({version})यह होना चाहिए {spec}", + "app_sources_fetch_failed": "सोर्स फाइल्स प्राप्त करने में असमर्थ", + "app_unknown": "अनजान एप्लीकेशन", + "app_unsupported_remote_type": "एप्लीकेशन के लिए उन्सुपपोर्टेड रिमोट टाइप इस्तेमाल किया गया", + "app_upgrade_failed": "{app:s} अपडेट करने में असमर्थ", + "app_upgraded": "{app:s} अपडेट हो गयी हैं", + "appslist_fetched": "एप्लीकेशन की सूचि अपडेट हो गयी", + "appslist_removed": "एप्लीकेशन की सूचि निकल दी गयी है", + "appslist_retrieve_error": "दूरस्थ एप्लिकेशन सूची प्राप्त करने में असमर्थ", + "appslist_unknown": "अनजान एप्लिकेशन सूची", + "ask_current_admin_password": "वर्तमान व्यवस्थापक पासवर्ड", + "ask_email": "ईमेल का पता", + "ask_firstname": "नाम", + "ask_lastname": "अंतिम नाम", + "ask_list_to_remove": "सूचि जिसको हटाना है", + "ask_main_domain": "मुख्य डोमेन", + "ask_new_admin_password": "नया व्यवस्थापक पासवर्ड", + "ask_password": "पासवर्ड", + "backup_action_required": "आप को सेव करने के लिए कुछ लिखना होगा", + "backup_app_failed": "एप्लीकेशन का बैकअप करने में असमर्थ '{app:s}'", + "backup_archive_app_not_found": "'{app:s}' बैकअप आरचिव में नहीं मिला", + "backup_archive_hook_not_exec": "हुक '{hook:s}' इस बैकअप में एक्सेक्युट नहीं किया गया", + "backup_archive_name_exists": "इस बैकअप आरचिव का नाम पहले से ही मौजूद है", + "backup_archive_name_unknown": "'{name:s}' इस नाम की लोकल बैकअप आरचिव मौजूद नहीं", + "backup_archive_open_failed": "बैकअप आरचिव को खोलने में असमर्थ", + "backup_cleaning_failed": "टेम्पोरेरी बैकअप डायरेक्टरी को उड़ने में असमर्थ", + "backup_created": "बैकअप सफलतापूर्वक किया गया", + "backup_creating_archive": "बैकअप आरचिव बनाई जा रही है ...", + "backup_creation_failed": "बैकअप बनाने में विफल", + "backup_delete_error": "'{path:s}' डिलीट करने में असमर्थ", + "backup_deleted": "इस बैकअप को डिलीट दिया गया है", + "backup_extracting_archive": "बैकअप आरचिव को एक्सट्रेक्ट किया जा रहा है ...", + "backup_hook_unknown": "'{hook:s}' यह बैकअप हुक नहीं मिला", + "backup_invalid_archive": "अवैध बैकअप आरचिव", + "backup_nothings_done": "सेव करने के लिए कुछ नहीं", + "backup_output_directory_forbidden": "निषिद्ध आउटपुट डायरेक्टरी। निम्न दिए गए डायरेक्टरी में बैकअप नहीं बन सकता /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var और /home/yunohost.backup/archives के सब-फोल्डर।", + "backup_output_directory_not_empty": "आउटपुट डायरेक्टरी खाली नहीं है", + "backup_output_directory_required": "बैकअप करने के लिए आउट पुट डायरेक्टरी की आवश्यकता है", + "backup_running_app_script": "'{app:s}' एप्लीकेशन की बैकअप स्क्रिप्ट चल रही है...", + "backup_running_hooks": "बैकअप हुक्स चल रहे है...", + "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app:s}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है", + "custom_appslist_name_required": "आप को अपनी कस्टम एप्लीकेशन के लिए नाम देने की आवश्यकता है", + "diagnostic_debian_version_error": "डेबियन वर्जन प्राप्त करने में असफलता {error}", + "diagnostic_kernel_version_error": "कर्नेल वर्जन प्राप्त नहीं की जा पा रही : {error}", + "diagnostic_monitor_disk_error": "डिस्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_monitor_network_error": "नेटवर्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_monitor_system_error": "सिस्टम की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnostic_no_apps": "कोई एप्लीकेशन इन्सटाल्ड नहीं है", + "dnsmasq_isnt_installed": "dnsmasq इन्सटाल्ड नहीं लगता,इनस्टॉल करने के लिए किप्या ये कमांड चलाये 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ", + "domain_created": "डोमेन बनाया गया", + "domain_creation_failed": "डोमेन बनाने में असमर्थ", + "domain_deleted": "डोमेन डिलीट कर दिया गया है", + "domain_deletion_failed": "डोमेन डिलीट करने में असमर्थ", + "domain_dyndns_already_subscribed": "DynDNS डोमेन पहले ही सब्स्क्राइड है", + "domain_dyndns_invalid": "DynDNS के साथ इनवैलिड डोमिन इस्तेमाल किया गया" } From d22f0012355d42e1bd22a503ab5cbd5499302975 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Tue, 29 Nov 2016 22:25:36 +0100 Subject: [PATCH 0152/1066] [i18n] Translated using Weblate (French) Currently translated at 99.6% (256 of 257 strings) --- locales/fr.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 7898de57f..6017e3737 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -248,5 +248,22 @@ "yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification", "yunohost_configured": "YunoHost a été configuré", "yunohost_installing": "Installation de YunoHost...", - "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »." + "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall ».", + "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner)", + "certmanager_domain_unknown": "Domaine inconnu {domain:s}", + "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain:s} a échoué…", + "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas fourni par Let’s Encrypt. Impossible de le renouveler automatiquement !", + "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point d’expirer ! Utilisez --force pour contourner", + "certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} n’est pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes.", + "certmanager_error_no_A_record": "Aucun enregistrement DNS « A » n’a été trouvé pour {domain:s}. De devez faire pointer votre nom de domaine vers votre machine pour être capable d’installer un certificat Let’s Encrypt ! (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrement DNS « A » du domaine {domain:s} est différent de l’adresse IP de ce serveur. Si vous avez modifié récemment votre enregistrement « A », veuillez attendre sa propagation (quelques vérificateur de propagation sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", + "certmanager_cannot_read_cert": "Quelque chose s’est mal passé lors de la tentative d’ouverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), cause : {reason:s}", + "certmanager_cert_install_success_selfsigned": "Installation avec succès d’un certificat auto-signé pour le domaine {domain:s} !", + "certmanager_cert_install_success": "Installation avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", + "certmanager_cert_renew_success": "Renouvellement avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", + "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés.", + "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué.", + "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", + "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable." } From 62a5e5c5df1cb73a0f947bff9d706ee5a61854dd Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Tue, 29 Nov 2016 22:25:36 +0100 Subject: [PATCH 0153/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (257 of 257 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 6017e3737..01f1cb184 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -265,5 +265,6 @@ "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés.", "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué.", "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", - "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable." + "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable.", + "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails." } From d6e76d2891ae11935be8b1bf369dbd2c5c5e3ca8 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Thu, 1 Dec 2016 22:46:40 +0100 Subject: [PATCH 0154/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (258 of 258 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 01f1cb184..f0afe0597 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -266,5 +266,6 @@ "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué.", "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable.", - "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails." + "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails.", + "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin." } From 83e9e10123364044f512382edcd0c4a978c46781 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Mon, 5 Dec 2016 07:29:35 +0100 Subject: [PATCH 0155/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (260 of 260 strings) --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f0afe0597..ca2d7fe50 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -267,5 +267,7 @@ "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable.", "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails.", - "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin." + "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin.", + "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON.", + "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON." } From 6e9f305e8381ce1c0734b7fc4cf4acb5ca07b617 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Thu, 8 Dec 2016 07:09:57 +0100 Subject: [PATCH 0156/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (261 of 261 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index ca2d7fe50..2a05dbd58 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -269,5 +269,6 @@ "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails.", "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin.", "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON.", - "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON." + "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON.", + "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable." } From 41f70f5d56770c44af1cf883308786a1c8ee55e8 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sun, 11 Dec 2016 22:56:27 +0100 Subject: [PATCH 0157/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (263 of 263 strings) --- locales/fr.json | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2a05dbd58..ed299daf1 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -59,7 +59,7 @@ "backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu", "backup_invalid_archive": "Archive de sauvegarde incorrecte", "backup_nothings_done": "Il n'y a rien à sauvegarder", - "backup_output_directory_forbidden": "Dossier de sortie interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives.", + "backup_output_directory_forbidden": "Dossier de destination interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide", "backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde", "backup_running_app_script": "Lancement du script de sauvegarde de l'application « {app:s} »...", @@ -82,11 +82,11 @@ "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine.", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine", "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", - "done": "Terminé.", + "done": "Terminé", "downloading": "Téléchargement...", "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée", "dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour le domaine DynDNS", @@ -105,7 +105,7 @@ "field_invalid": "Champ incorrect : « {:s} »", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Le pare-feu a été rechargé", - "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", + "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal", "format_datetime_short": "%d/%m/%Y %H:%M", "hook_argument_missing": "Argument manquant : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'", @@ -115,8 +115,8 @@ "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", - "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes sûrement dans un conteneur, ou alors votre noyau ne le supporte pas.", - "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", + "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", + "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", "ldap_initialized": "L'annuaire LDAP a été initialisé", "license_undefined": "indéfinie", "mail_alias_remove_failed": "Impossible de supprimer l'adresse courriel supplémentaire « {mail:s} »", @@ -248,27 +248,29 @@ "yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification", "yunohost_configured": "YunoHost a été configuré", "yunohost_installing": "Installation de YunoHost...", - "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall ».", + "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »", "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner)", "certmanager_domain_unknown": "Domaine inconnu {domain:s}", "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force)", "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain:s} a échoué…", "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas fourni par Let’s Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point d’expirer ! Utilisez --force pour contourner", - "certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} n’est pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes.", + "certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} n’est pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes", "certmanager_error_no_A_record": "Aucun enregistrement DNS « A » n’a été trouvé pour {domain:s}. De devez faire pointer votre nom de domaine vers votre machine pour être capable d’installer un certificat Let’s Encrypt ! (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrement DNS « A » du domaine {domain:s} est différent de l’adresse IP de ce serveur. Si vous avez modifié récemment votre enregistrement « A », veuillez attendre sa propagation (quelques vérificateur de propagation sont disponibles en ligne). (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces contrôles)", "certmanager_cannot_read_cert": "Quelque chose s’est mal passé lors de la tentative d’ouverture du certificat actuel pour le domaine {domain:s} (fichier : {file:s}), cause : {reason:s}", "certmanager_cert_install_success_selfsigned": "Installation avec succès d’un certificat auto-signé pour le domaine {domain:s} !", "certmanager_cert_install_success": "Installation avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", "certmanager_cert_renew_success": "Renouvellement avec succès d’un certificat Let’s Encrypt pour le domaine {domain:s} !", - "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés.", - "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué.", + "certmanager_old_letsencrypt_app_detected": "\nYunoHost a détecté que l’application « letsencrypt » est installé, ce qui est en conflit avec les nouvelles fonctionnalités de gestion intégrée de certificats dans YunoHost. Si vous souhaitez utiliser ces nouvelles fonctionnalités intégrées, veuillez lancer les commandes suivantes pour migrer votre installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : cela tentera de réinstaller les certificats de tous les domaines avec un certificat Let's Encrypt ou ceux auto-signés", + "certmanager_cert_signing_failed": "La signature du nouveau certificat a échoué", "certmanager_no_cert_file": "Impossible de lire le fichier de certificat pour le domaine {domain:s} (fichier : {file:s})", - "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable.", - "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails.", - "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin.", - "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON.", - "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON.", - "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable." + "certmanager_conflicting_nginx_file": "Impossible de préparer le domaine pour de défi ACME : le fichier de configuration nginx {filepath:s} est en conflit et doit être retiré au préalable", + "certmanager_hit_rate_limit": "Trop de certificats ont déjà été demandés récemment pour cet ensemble précis de domaines {domain:s}. Veuillez réessayer plus tard. Lisez https://letsencrypt.org/docs/rate-limits/ pour obtenir plus de détails", + "ldap_init_failed_to_create_admin": "L’initialisation de LDAP n’a pas réussi à créer l’utilisateur admin", + "ssowat_persistent_conf_read_error": "Erreur lors de la lecture de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON", + "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON", + "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", + "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})" } From 64a831721323c079ddc9ff86a0fe00515040068a Mon Sep 17 00:00:00 2001 From: Genma Date: Mon, 12 Dec 2016 12:22:44 +0100 Subject: [PATCH 0158/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (264 of 264 strings) --- locales/fr.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index ed299daf1..c7fc1a130 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -111,7 +111,7 @@ "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", - "hook_list_by_invalid": "Propriété pour lister les scripts incorrecte", + "hook_list_by_invalid": "Propriété pour lister les scripts incorrects", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", @@ -272,5 +272,6 @@ "ssowat_persistent_conf_write_error": "Erreur lors de la sauvegarde de la configuration persistante de SSOwat : {error:s}. Modifiez le fichier /etc/ssowat/conf.json.persistent pour réparer la syntaxe JSON", "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", - "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})" + "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la boîte mail." } From a3f2e62c11b0b842bf89dc57f65622de9da482c6 Mon Sep 17 00:00:00 2001 From: Moul Date: Mon, 12 Dec 2016 21:34:07 +0100 Subject: [PATCH 0159/1066] [enh] readme: add translation badge status. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 714fa0980..9aed880ac 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ This repository is the core of YunoHost code. + +Translation status + + ## Issues - [Please report issues on YunoHost bugtracker](https://dev.yunohost.org/projects/yunohost/issues) (no registration needed). From e3b7db71f2fa6ee5a70efcd31e087172988b1bcf Mon Sep 17 00:00:00 2001 From: Juanu Date: Tue, 13 Dec 2016 14:00:04 +0100 Subject: [PATCH 0160/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) --- locales/es.json | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/locales/es.json b/locales/es.json index 549cbe29a..093fb17fa 100644 --- a/locales/es.json +++ b/locales/es.json @@ -58,7 +58,7 @@ "backup_hook_unknown": "Hook de copia de seguridad desconocido '{hook:s}'", "backup_invalid_archive": "La copia de seguridad no es válida", "backup_nothings_done": "No hay nada que guardar", - "backup_output_directory_forbidden": "Directorio de salida no permitido. Las copias de seguridad no pueden ser creadas en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o en los subdirectorios /home/yunohost.backup.", + "backup_output_directory_forbidden": "Directorio de salida no permitido. Las copias de seguridad no pueden ser creadas en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o en los subdirectorios de /home/yunohost.backup/archives", "backup_output_directory_not_empty": "El directorio de salida no está vacío", "backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad", "backup_running_app_script": "Ejecutando la script de copia de seguridad de la aplicación '{app:s}'...", @@ -114,8 +114,8 @@ "hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "installation_complete": "Instalación finalizada", "installation_failed": "No pudo realizar la instalación", - "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción.", - "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción.", + "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción", + "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción", "ldap_initialized": "LDAP iniciado", "license_undefined": "indefinido", "mail_alias_remove_failed": "No se pudo eliminar el alias de correo '{mail:s}'", @@ -240,5 +240,30 @@ "yunohost_ca_creation_failed": "No se pudo crear el certificado de autoridad", "yunohost_configured": "YunoHost ha sido configurado", "yunohost_installing": "Instalando YunoHost...", - "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", + "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", + "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón", + "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s} 1! (Use --force para omitir este mensaje)", + "certmanager_domain_unknown": "Dominio desconocido {domain:s} 1", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Estás seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para {domain:s} 1 falla de alguna manera...", + "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} 1 no está emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", + "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} 1 no está a punto de expirar! Utilice --force para omitir este mensaje", + "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} 1 a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", + "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s} 1. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} 1 es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} 1 (archivo: {file:s} 2), razón: {reason:s} 3", + "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s} 1!", + "certmanager_cert_install_success": "Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s} 1!", + "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s} 1!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar certificados para todos los dominios con un certificado Let's Encrypt o un certificado autofirmado", + "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s} 1. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", + "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", + "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} 1 (archivo: {file:s} 2)", + "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} 1 está en conflicto y debe ser eliminado primero", + "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", + "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s} 1)", + "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)" } From 72789875347a49c8be9ad90791cdac58426636e2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 13 Dec 2016 07:20:51 +0100 Subject: [PATCH 0161/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (264 of 264 strings) --- locales/fr.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index c7fc1a130..35749030c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -119,9 +119,9 @@ "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", "ldap_initialized": "L'annuaire LDAP a été initialisé", "license_undefined": "indéfinie", - "mail_alias_remove_failed": "Impossible de supprimer l'adresse courriel supplémentaire « {mail:s} »", - "mail_domain_unknown": "Le domaine « {domain:s} » de l'adresse courriel est inconnu", - "mail_forward_remove_failed": "Impossible de supprimer l'adresse courriel de transfert « {mail:s} »", + "mail_alias_remove_failed": "Impossible de supprimer l'alias courriel « {mail:s} »", + "mail_domain_unknown": "Le domaine « {domain:s} » du courriel est inconnu", + "mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert « {mail:s} »", "maindomain_change_failed": "Impossible de modifier le domaine principal", "maindomain_changed": "Le domaine principal a été modifié", "monitor_disabled": "La supervision du serveur a été désactivé", @@ -155,7 +155,7 @@ "path_removal_failed": "Impossible de supprimer le chemin {:s}", "pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)", - "pattern_email": "Doit être une adresse courriel valide (ex. : someone@domain.org)", + "pattern_email": "Doit être une adresse courriel valide (ex. : pseudo@domain.org)", "pattern_firstname": "Doit être un prénom valide", "pattern_lastname": "Doit être un nom valide", "pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas", @@ -273,5 +273,5 @@ "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", - "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la boîte mail." + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie" } From cde530735e942fef067c1fb4aa30d1022590b7f8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 13 Dec 2016 15:09:25 +0100 Subject: [PATCH 0162/1066] [enh] add empty file for breton translation --- locales/br.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/br.json diff --git a/locales/br.json b/locales/br.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/br.json @@ -0,0 +1 @@ +{} From 12c66d153096b13abe99e8978eb35cd6c2edc2f6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 3 Dec 2016 00:36:22 +0100 Subject: [PATCH 0163/1066] [enh] remove timeout from cli interface --- bin/yunohost | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/yunohost b/bin/yunohost index b2a0e4b1b..0bf2c004c 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -66,6 +66,10 @@ def _parse_cli_args(): action='store_true', default=False, help="Don't produce any output", ) + parser.add_argument('--timeout', + type=int, default=None, + help="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", + ) parser.add_argument('--admin-password', default=None, dest='password', metavar='PASSWORD', help="The admin password to use to authenticate", @@ -209,6 +213,7 @@ if __name__ == '__main__': ret = moulinette.cli( _retrieve_namespaces(), args, use_cache=opts.use_cache, output_as=opts.output_as, - password=opts.password, parser_kwargs={'top_parser': parser} + password=opts.password, parser_kwargs={'top_parser': parser}, + timeout=opts.timeout, ) sys.exit(ret) From eb1f6a77a9c1800ffd520316dcae097bb155d869 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 15 Dec 2016 16:33:46 +0100 Subject: [PATCH 0164/1066] Revert "Revert "[fix] Can't restore app on a root domain"" This reverts commit 7717c58de4c8cda615165d84a12a30dd73eea58a. --- src/yunohost/app.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index eb22cb955..85693a0dd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -917,10 +917,6 @@ def app_checkurl(auth, url, app=None): raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) if domain in apps_map: - # Domain already has apps on sub path - if path == '/': - raise MoulinetteError(errno.EPERM, - m18n.n('app_location_install_failed')) # Loop through apps for p, a in apps_map[domain].items(): # Skip requested app checking @@ -930,7 +926,7 @@ def app_checkurl(auth, url, app=None): if path == p: raise MoulinetteError(errno.EINVAL, m18n.n('app_location_already_used')) - elif path.startswith(p): + elif path.startswith(p) or p.startswith(path): raise MoulinetteError(errno.EPERM, m18n.n('app_location_install_failed')) From 9be254ecfcc4790405d4769e65872376df3faa71 Mon Sep 17 00:00:00 2001 From: opi Date: Thu, 15 Dec 2016 21:24:40 +0100 Subject: [PATCH 0165/1066] [enh][love] Add CONTRIBUTORS.md --- CONTRIBUTORS.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 CONTRIBUTORS.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..375c1cec6 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,88 @@ +YunoHost core contributors +========================== + +YunoHost is built and maintained by the YunoHost project community. +Everyone is encouraged to submit issues and changes, and to contribute in other ways -- see https://yunohost.org/contribute to find out how. + +-- + +Initial YunoHost core was built by Kload & beudbeud, for YunoHost v2. + +Most of code was written by Kload and jerome, with help of numerous contributors. + +Translation is made by a bunch of lovely people all over the world. + +We would like to thank anyone who ever helped the YunoHost project <3 + + +YunoHost core Contributors +-------------------------- + +- Jérôme Lebleu +- Kload +- Laurent 'Bram' Peuch +- Julien 'ju' Malik +- opi +- Aleks +- Adrien 'beudbeud' Beudin +- M5oul +- Valentin 'zamentur' / 'ljf' Grimaud +- Jocelyn Delalande +- infertux +- Taziden +- ZeHiro +- Josue-T +- nahoj +- a1ex +- JimboJoe +- vetetix +- jellium +- Sebastien 'sebian' Badia +- lmangani +- Julien Vaubourg + + +YunoHost core Translators +------------------------- + +If you want to help translation, please visit https://translate.yunohost.org/projects/yunohost/yunohost/ + + +### Dutch + +- DUBWiSE +- marut + +### English + +- Bugsbane + +### French + +- aoz roon +- Genma +- Jean-Baptiste Holcroft +- Jérôme Lebleu + +### German + +- david.bartke +- Felix Bartels +- Philip Gatzka + +### Hindi + +- Anmol + +### Italian + +- Thomas Bille + +### Portuguese + +- Deleted User + +### Spanish + +- Juanu + From 71df3a6c00ae13ee613a33c1bdbc7809596361bd Mon Sep 17 00:00:00 2001 From: Juanu Date: Tue, 13 Dec 2016 15:01:21 +0100 Subject: [PATCH 0166/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) --- locales/es.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/locales/es.json b/locales/es.json index 093fb17fa..9a7e4314e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,12 +1,12 @@ { - "action_invalid": "Acción no válida '{action:s}'", + "action_invalid": "Acción no válida '{action:s} 1'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se pudo cambiar la contraseña", + "admin_password_change_failed": "No se puede cambiar la contraseña", "admin_password_changed": "La contraseña administrativa ha sido cambiada", - "app_already_installed": "{app:s} ya está instalada", - "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s}', deber una de {choices:s}", - "app_argument_invalid": "Valor no válido para el argumento '{name:s}': {error:s}", - "app_argument_required": "Se requiere el argumento '{name:s}'", + "app_already_installed": "{app:s} 2 ya está instalada", + "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s} 3', deber una de {choices:s} 4", + "app_argument_invalid": "Valor no válido para el argumento '{name:s} 5': {error:s} 6", + "app_argument_required": "Se requiere el argumento '{name:s} 7'", "app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_id_invalid": "Id de la aplicación no válida", "app_incompatible": "La aplicación no es compatible con su versión de YunoHost", @@ -15,9 +15,9 @@ "app_location_install_failed": "No se puede instalar la aplicación en esta localización", "app_manifest_invalid": "El manifiesto de la aplicación no es válido", "app_no_upgrade": "No hay aplicaciones para actualizar", - "app_not_correctly_installed": "La aplicación {app:s} parece estar incorrectamente instalada", - "app_not_installed": "{app:s} no está instalada", - "app_not_properly_removed": "La {app:s} no ha sido desinstalada correctamente", + "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", + "app_not_installed": "{app:s} 9 no está instalada", + "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente", "app_package_need_update": "Es necesario actualizar el paquete de la aplicación debido a los cambios en YunoHost", "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", "app_removed": "{app:s} ha sido eliminada", @@ -71,7 +71,7 @@ "diagnostic_monitor_network_error": "No se puede monitorizar la red: {error}", "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", - "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", + "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecute 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", @@ -84,11 +84,11 @@ "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminarlo.", "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zona del DNS ya existe", - "domain_zone_not_found": "No se ha encontrado el archivo de zona DNS para el dominio [:s]", + "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]", "done": "Hecho.", "downloading": "Descargando...", "dyndns_cron_installed": "La tarea cron para DynDNS ha sido instalada", - "dyndns_cron_remove_failed": "No se pudo eliminar la tarea del cron DynDNS", + "dyndns_cron_remove_failed": "No se pudo eliminar la tarea cron DynDNS", "dyndns_cron_removed": "La tarea cron DynDNS ha sido eliminada", "dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en el DynDNS", "dyndns_ip_updated": "Su dirección IP ha sido actualizada en el DynDNS", @@ -193,13 +193,13 @@ "service_conf_file_updated": "El archivo de configuración '{conf}' ha sido actualizado", "service_conf_up_to_date": "La configuración del servicio '{service}' ya está actualizada", "service_conf_updated": "La configuración ha sido actualizada para el servicio '{service}'", - "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service}'", + "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service} 1'", "service_disable_failed": "No se pudo deshabilitar el servicio '{service:s}'", "service_disabled": "El servicio '{service:s}' ha sido deshabilitado", "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_no_log": "No hay ningún registro para el servicio '{service:s}'", - "service_regenconf_dry_pending_applying": "Comprobando configuración que podría haber sido aplicada al servicio '{service}'...", + "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service} 1'...", "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}", "service_regenconf_pending_applying": "Aplicando la configuración para el servicio '{service}'...", "service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'", @@ -242,13 +242,13 @@ "yunohost_installing": "Instalando YunoHost...", "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", - "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón", + "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón de correo", "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s} 1! (Use --force para omitir este mensaje)", "certmanager_domain_unknown": "Dominio desconocido {domain:s} 1", - "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Estás seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", - "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para {domain:s} 1 falla de alguna manera...", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} 1 ha fallado de alguna manera...", "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} 1 no está emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} 1 no está a punto de expirar! Utilice --force para omitir este mensaje", "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} 1 a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", @@ -258,7 +258,7 @@ "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s} 1!", "certmanager_cert_install_success": "Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s} 1!", "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar certificados para todos los dominios con un certificado Let's Encrypt o un certificado autofirmado", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s} 1. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} 1 (archivo: {file:s} 2)", From 025f7e932e02278402225c967e4795de12efa2a7 Mon Sep 17 00:00:00 2001 From: Juanu Date: Tue, 13 Dec 2016 15:53:25 +0100 Subject: [PATCH 0167/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) --- locales/es.json | 68 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/locales/es.json b/locales/es.json index 9a7e4314e..b3dd4f14e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -113,16 +113,16 @@ "hook_list_by_invalid": "Propiedad no válida para listar por hook", "hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "installation_complete": "Instalación finalizada", - "installation_failed": "No pudo realizar la instalación", + "installation_failed": "No se pudo realizar la instalación", "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción", "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción", - "ldap_initialized": "LDAP iniciado", + "ldap_initialized": "Se ha inicializado LDAP", "license_undefined": "indefinido", "mail_alias_remove_failed": "No se pudo eliminar el alias de correo '{mail:s}'", "mail_domain_unknown": "El dominio de correo '{domain:s}' es desconocido", "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo '{mail:s}'", "maindomain_change_failed": "No se pudo cambiar el dominio principal", - "maindomain_changed": "El dominio principal ha sido cambiado", + "maindomain_changed": "Se ha cambiado el dominio principal", "monitor_disabled": "La monitorización del sistema ha sido deshabilitada", "monitor_enabled": "La monitorización del sistema ha sido habilitada", "monitor_glances_con_failed": "No se pudo conectar al servidor Glances", @@ -130,13 +130,13 @@ "monitor_period_invalid": "Período de tiempo no válido", "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticas", "monitor_stats_no_update": "No hay estadísticas de monitorización para actualizar", - "monitor_stats_period_unavailable": "No hay estadísticas para ese período", + "monitor_stats_period_unavailable": "No hay estadísticas para el período", "mountpoint_unknown": "Punto de montaje desconocido", "mysql_db_creation_failed": "No se pudo crear la base de datos MySQL", "mysql_db_init_failed": "No se pudo iniciar la base de datos MySQL", - "mysql_db_initialized": "La base de datos MySQL ha sido iniciada", + "mysql_db_initialized": "La base de datos MySQL ha sido inicializada", "network_check_mx_ko": "El registro DNS MX no está configurado", - "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado en su red", + "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado por su red", "network_check_smtp_ok": "El puerto de salida del correo electrónico (25, SMTP) no está bloqueado", "new_domain_required": "Debe proporcionar el nuevo dominio principal", "no_appslist_found": "No se ha encontrado ninguna lista de aplicaciones", @@ -145,13 +145,13 @@ "no_restore_script": "No se ha encontrado un script de restauración para la aplicación '{app:s}'", "not_enough_disk_space": "No hay suficiente espacio en '{path:s}'", "package_not_installed": "El paquete '{pkgname}' no está instalado", - "package_unexpected_error": "Un error inesperado procesando el paquete '{pkgname}'", + "package_unexpected_error": "Ha ocurrido un error inesperado procesando el paquete '{pkgname}'", "package_unknown": "Paquete desconocido '{pkgname}'", - "packages_no_upgrade": "No hay paquetes que actualizar", + "packages_no_upgrade": "No hay paquetes para actualizar", "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", - "path_removal_failed": "No se pudo borrar la ruta {:s}", - "pattern_backup_archive_name": "Debe que ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", + "path_removal_failed": "No se pudo eliminar la ruta {:s}", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida (por ejemplo, alguien@dominio.org)", "pattern_firstname": "Debe ser un nombre válido", @@ -180,7 +180,7 @@ "restore_running_hooks": "Ejecutando los hooks de restauración...", "service_add_failed": "No se pudo añadir el servicio '{service:s}'", "service_added": "Servicio '{service:s}' ha sido añadido", - "service_already_started": "El servicio '{service:s}' ya ha sido iniciado", + "service_already_started": "El servicio '{service:s}' ya ha sido inicializado", "service_already_stopped": "El servicio '{service:s}' ya ha sido detenido", "service_cmd_exec_failed": "No se pudo ejecutar el comando '{command:s}'", "service_conf_file_backed_up": "Se ha realizado una copia de seguridad del archivo de configuración '{conf}' en '{backup}'", @@ -199,9 +199,9 @@ "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_no_log": "No hay ningún registro para el servicio '{service:s}'", - "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service} 1'...", + "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service}'...", "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}", - "service_regenconf_pending_applying": "Aplicando la configuración para el servicio '{service}'...", + "service_regenconf_pending_applying": "Aplicando la configuración pendiente para el servicio '{service}'...", "service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'", "service_removed": "El servicio '{service:s}' ha sido desinstalado", "service_start_failed": "No se pudo iniciar el servicio '{service:s}'", @@ -219,7 +219,7 @@ "unit_unknown": "Unidad desconocida '{unit:s}'", "unlimit": "Sin cuota", "unrestore_app": "La aplicación '{app:s}' no será restaurada", - "update_cache_failed": "No se pudo actualizar la caché APT", + "update_cache_failed": "No se pudo actualizar la caché de APT", "updating_apt_cache": "Actualizando lista de paquetes disponibles...", "upgrade_complete": "Actualización finalizada", "upgrading_packages": "Actualizando paquetes...", @@ -243,27 +243,27 @@ "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón de correo", - "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", - "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", - "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s} 1! (Use --force para omitir este mensaje)", - "certmanager_domain_unknown": "Dominio desconocido {domain:s} 1", - "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", - "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} 1 ha fallado de alguna manera...", - "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} 1 no está emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", - "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} 1 no está a punto de expirar! Utilice --force para omitir este mensaje", - "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} 1 a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", - "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s} 1. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} 1 es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", - "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} 1 (archivo: {file:s} 2), razón: {reason:s} 3", - "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s} 1!", - "certmanager_cert_install_success": "Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", - "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s} 1. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", + "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s}! (Use --force para omitir este mensaje)", + "certmanager_domain_unknown": "Dominio desconocido {domain:s}", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} ha fallado de alguna manera...", + "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", + "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} no está a punto de expirar! Utilice --force para omitir este mensaje", + "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", + "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s}. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} (archivo: {file:s}), razón: {reason:s}", + "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s}!", + "certmanager_cert_install_success": "¡Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada, esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", + "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s}. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", - "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} 1 (archivo: {file:s} 2)", - "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} 1 está en conflicto y debe ser eliminado primero", + "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} (archivo: {file:s})", + "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} está en conflicto y debe ser eliminado primero", "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", - "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s} 1)", + "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)" } From bd258958fa50eca24ccab5fd5905c59bfc2b2a05 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 13 Dec 2016 07:20:51 +0100 Subject: [PATCH 0168/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (265 of 265 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 35749030c..cbd619c38 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -273,5 +273,6 @@ "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", - "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie" + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", + "domains_available": "Domaines disponibles :" } From 0a7479cc37a8c11d2e646145f7eb7bff1696ec14 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 16 Dec 2016 00:59:10 +0100 Subject: [PATCH 0169/1066] Update changelog for 2.5.2 release --- debian/changelog | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/debian/changelog b/debian/changelog index 34d883ca1..248c934fd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,42 @@ +yunohost (2.5.2) testing; urgency=low + + LDAP admin user: + * [fix] wait for admin user to be available after a slapd regen-conf, this fix install on slow hardware/vps + + Dovecot/emails: + * [enh] reorder dovecot main configuration so that it is easier to read and extend + * [enh] Allow for dovecot configuration extensions + * [fix] Can't get mailbos used space if dovecot is down + + Backup: + * [fix] Need to create archives_path even for custom output directory + * Keep track of backups with custom directory using symlinks + + Security: + * [fix] Improve dnssec key generation on low entropy devices + * [enh] Add haveged as dependency + + Random broken app installed on slow hardware: + * [enh] List available domains when installing an app by CLI. + + Translation: + * French by Jibec and Genma + * German by Philip Gatzka + * Hindi by Anmol + * Spanish by Juanu + + Other fixes and improvements: + * [enh] remove timeout from cli interface + * [fix] [#662](https://dev.yunohost.org/issues/662): missing 'python-openssl' dependency for Let's Encrypt integration. + * [fix] --no-remove-on-failure for app install should behave as a flag. + * [fix] don't remove trailing char if it's not a slash + + Thanks to all contributors: Aleks, alex, Anmol, Bram, Genma, jibec, ju, + Juanu, ljf, Moul, opi, Philip Gatzka and to the people who are participating + to the beta and giving us feedback <3 + + -- Laurent Peuch Fri, 16 Dec 2016 00:49:08 +0100 + yunohost (2.5.1) testing; urgency=low * [fix] Raise error on malformed SSOwat persistent conf. From 40cb95f33af7f209e839b06df2591c645e132511 Mon Sep 17 00:00:00 2001 From: opi Date: Sat, 17 Dec 2016 15:26:50 +0100 Subject: [PATCH 0170/1066] [enh] Increase fail2ban maxretry on user login, narrow nginx log files We're intended to a general public, after all ;) --- data/templates/fail2ban/jail.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index 59dcf51df..d34763e48 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -581,4 +581,5 @@ enabled = true port = http,https protocol = tcp filter = yunohost -logpath = /var/log/nginx/*.log +logpath = /var/log/nginx*/*error.log +maxretry = 6 From a94733c1f9d9dd8e11b484bafc576800029d28be Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 18 Dec 2016 01:05:03 +0100 Subject: [PATCH 0171/1066] [enh] add yaml syntax check in travis.yml --- .travis.yml | 5 +++++ tests/test_actionmap.py | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .travis.yml create mode 100644 tests/test_actionmap.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..25fe0e5fc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +install: "pip install pytest pyyaml" +python: + - "2.7" +script: "py.test tests" diff --git a/tests/test_actionmap.py b/tests/test_actionmap.py new file mode 100644 index 000000000..08b868839 --- /dev/null +++ b/tests/test_actionmap.py @@ -0,0 +1,4 @@ +import yaml + +def test_yaml_syntax(): + yaml.load(open("data/actionsmap/yunohost.yml")) From aad37409333895051e505c944b2ec05790d2fb46 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 18 Dec 2016 02:01:17 +0100 Subject: [PATCH 0172/1066] [mod] autopep8 on all files that aren't concerned by a PR --- src/yunohost/backup.py | 12 +- src/yunohost/certificate.py | 6 +- src/yunohost/domain.py | 28 ++--- src/yunohost/dyndns.py | 36 +++--- src/yunohost/firewall.py | 39 ++++--- src/yunohost/monitor.py | 30 ++--- src/yunohost/service.py | 31 ++--- src/yunohost/user.py | 126 +++++++++++---------- src/yunohost/utils/packages.py | 1 + src/yunohost/vendor/acme_tiny/acme_tiny.py | 26 ++++- 10 files changed, 181 insertions(+), 154 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index da5115c28..587da31c7 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -47,7 +47,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall -backup_path = '/home/yunohost.backup' +backup_path = '/home/yunohost.backup' archives_path = '%s/archives' % backup_path logger = getActionLogger('yunohost.backup') @@ -313,8 +313,6 @@ def backup_create(name=None, description=None, output_directory=None, link = "%s/%s.tar.gz" % (archives_path, name) os.symlink(archive_file, link) - - # Clean temporary directory if tmp_dir != output_directory: _clean_tmp_dir() @@ -323,7 +321,7 @@ def backup_create(name=None, description=None, output_directory=None, # Return backup info info['name'] = name - return { 'archive': info } + return {'archive': info} def backup_restore(auth, name, hooks=[], ignore_hooks=False, @@ -601,7 +599,7 @@ def backup_list(with_info=False, human_readable=False): d[a] = backup_info(a, human_readable=human_readable) result = d - return { 'archives': result } + return {'archives': result} def backup_info(name, with_details=False, human_readable=False): @@ -645,7 +643,7 @@ def backup_info(name, with_details=False, human_readable=False): size = info.get('size', 0) if not size: tar = tarfile.open(archive_file, "r:gz") - size = reduce(lambda x,y: getattr(x, 'size', x)+getattr(y, 'size', y), + size = reduce(lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y), tar.getmembers()) tar.close() if human_readable: @@ -678,7 +676,7 @@ def backup_delete(name): archive_file = '%s/%s.tar.gz' % (archives_path, name) info_file = "%s/%s.info.json" % (archives_path, name) - for backup_file in [archive_file,info_file]: + for backup_file in [archive_file, info_file]: if not os.path.isfile(backup_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=backup_file)) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 04d755692..01852d2ec 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -193,9 +193,9 @@ def _certificate_install_selfsigned(domain_list, force=False): # and self-sign the cert commands = [ "openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" - % (conf_file, csr_file, key_file), + % (conf_file, csr_file, key_file), "openssl ca -config %s -days 3650 -in %s -out %s -batch" - % (conf_file, csr_file, crt_file), + % (conf_file, csr_file, crt_file), ] for command in commands: @@ -528,7 +528,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): CA=certification_authority) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): - raise MoulinetteError(errno.EINVAL, m18n.n( + raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_hit_rate_limit', domain=domain)) else: logger.error(str(e)) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 1aeab33ef..ded8bb27a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -66,10 +66,10 @@ def domain_list(auth, filter=None, limit=None, offset=None): result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain']) if len(result) > offset and limit > 0: - for domain in result[offset:offset+limit]: + for domain in result[offset:offset + limit]: result_list.append(domain['virtualdomain'][0]) - return { 'domains': result_list } + return {'domains': result_list} def domain_add(auth, domain, dyndns=False): @@ -83,7 +83,7 @@ def domain_add(auth, domain, dyndns=False): """ from yunohost.hook import hook_callback - attr_dict = { 'objectClass' : ['mailDomain', 'top'] } + attr_dict = {'objectClass': ['mailDomain', 'top']} now = datetime.datetime.now() timestamp = str(now.year) + str(now.month) + str(now.day) @@ -103,7 +103,7 @@ def domain_add(auth, domain, dyndns=False): pass else: dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) + dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if os.path.exists('/etc/cron.d/yunohost-dyndns'): raise MoulinetteError(errno.EPERM, @@ -113,7 +113,6 @@ def domain_add(auth, domain, dyndns=False): raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_root_unknown')) - try: yunohost.certificate._certificate_install_selfsigned([domain], False) @@ -122,7 +121,6 @@ def domain_add(auth, domain, dyndns=False): except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) - attr_dict['virtualdomain'] = domain if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): @@ -133,11 +131,14 @@ def domain_add(auth, domain, dyndns=False): service_regen_conf(names=[ 'nginx', 'metronome', 'dnsmasq', 'rmilter']) os.system('yunohost app ssowatconf > /dev/null 2>&1') - except IOError: pass + except IOError: + pass except: # Force domain removal silently - try: domain_remove(auth, domain, True) - except: pass + try: + domain_remove(auth, domain, True) + except: + pass raise hook_callback('post_domain_add', args=[domain]) @@ -165,7 +166,7 @@ def domain_remove(auth, domain, force=False): # Check if apps are installed on the domain for app in os.listdir('/etc/yunohost/apps/'): - with open('/etc/yunohost/apps/' + app +'/settings.yml') as f: + with open('/etc/yunohost/apps/' + app + '/settings.yml') as f: try: app_domain = yaml.load(f)['domain'] except: @@ -224,13 +225,13 @@ def domain_dns_conf(domain, ttl=None): "muc {ttl} IN CNAME @\n" "pubsub {ttl} IN CNAME @\n" "vjud {ttl} IN CNAME @\n" - ).format(ttl=ttl, domain=domain) + ).format(ttl=ttl, domain=domain) # Email result += ('\n' '@ {ttl} IN MX 10 {domain}.\n' '@ {ttl} IN TXT "v=spf1 a mx ip4:{ip4}' - ).format(ttl=ttl, domain=domain, ip4=ip4) + ).format(ttl=ttl, domain=domain, ip4=ip4) if ip6 is not None: result += ' ip6:{ip6}'.format(ip6=ip6) result += ' -all"' @@ -246,7 +247,7 @@ def domain_dns_conf(domain, ttl=None): r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' '(?=.*(;[\s]*|")v=(?P[^";]+))' '(?=.*(;[\s]*|")k=(?P[^";]+))' - '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M|re.S + '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M | re.S ) if dkim: result += '\n{host}. {ttl} IN TXT "v={v}; k={k}; p={p}"'.format( @@ -296,6 +297,7 @@ def _get_maindomain(): maindomain = f.readline().rstrip() return maindomain + def _set_maindomain(domain): with open('/etc/yunohost/current_host', 'w') as f: f.write(domain) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index b32cde0da..7553e417c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -94,7 +94,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None logger.info(m18n.n('dyndns_key_generating')) - os.system('cd /etc/yunohost/dyndns && ' \ + os.system('cd /etc/yunohost/dyndns && ' 'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -n USER %s' % domain) os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') @@ -104,12 +104,14 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: - r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={ 'subdomain': domain }) + r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: - try: error = json.loads(r.text)['error'] - except: error = "Server error" + try: + error = json.loads(r.text)['error'] + except: + error = "Server error" raise MoulinetteError(errno.EPERM, m18n.n('dyndns_registration_failed', error=error)) @@ -204,33 +206,33 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, lines = [ 'server %s' % dyn_host, 'zone %s' % host, - 'update delete %s. A' % domain, - 'update delete %s. AAAA' % domain, - 'update delete %s. MX' % domain, - 'update delete %s. TXT' % domain, + 'update delete %s. A' % domain, + 'update delete %s. AAAA' % domain, + 'update delete %s. MX' % domain, + 'update delete %s. TXT' % domain, 'update delete pubsub.%s. A' % domain, 'update delete pubsub.%s. AAAA' % domain, - 'update delete muc.%s. A' % domain, + 'update delete muc.%s. A' % domain, 'update delete muc.%s. AAAA' % domain, - 'update delete vjud.%s. A' % domain, + 'update delete vjud.%s. A' % domain, 'update delete vjud.%s. AAAA' % domain, 'update delete _xmpp-client._tcp.%s. SRV' % domain, 'update delete _xmpp-server._tcp.%s. SRV' % domain, - 'update add %s. 1800 A %s' % (domain, ipv4), + 'update add %s. 1800 A %s' % (domain, ipv4), 'update add %s. 14400 MX 5 %s.' % (domain, domain), 'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain, - 'update add pubsub.%s. 1800 A %s' % (domain, ipv4), - 'update add muc.%s. 1800 A %s' % (domain, ipv4), - 'update add vjud.%s. 1800 A %s' % (domain, ipv4), + 'update add pubsub.%s. 1800 A %s' % (domain, ipv4), + 'update add muc.%s. 1800 A %s' % (domain, ipv4), + 'update add vjud.%s. 1800 A %s' % (domain, ipv4), 'update add _xmpp-client._tcp.%s. 14400 SRV 0 5 5222 %s.' % (domain, domain), 'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain) ] if ipv6 is not None: lines += [ - 'update add %s. 1800 AAAA %s' % (domain, ipv6), + 'update add %s. 1800 AAAA %s' % (domain, ipv6), 'update add pubsub.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), + 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), + 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), ] lines += [ 'show', diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 1291cf86a..91f484f48 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -67,14 +67,14 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, # Validate protocols protocols = ['TCP', 'UDP'] if protocol != 'Both' and protocol in protocols: - protocols = [protocol,] + protocols = [protocol, ] # Validate IP versions ipvs = ['ipv4', 'ipv6'] if ipv4_only and not ipv6_only: - ipvs = ['ipv4',] + ipvs = ['ipv4', ] elif ipv6_only and not ipv4_only: - ipvs = ['ipv6',] + ipvs = ['ipv6', ] for p in protocols: # Iterate over IP versions to add port @@ -117,18 +117,18 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, # Validate protocols protocols = ['TCP', 'UDP'] if protocol != 'Both' and protocol in protocols: - protocols = [protocol,] + protocols = [protocol, ] # Validate IP versions and UPnP ipvs = ['ipv4', 'ipv6'] upnp = True if ipv4_only and ipv6_only: - upnp = True # automatically disallow UPnP + upnp = True # automatically disallow UPnP elif ipv4_only: - ipvs = ['ipv4',] + ipvs = ['ipv4', ] upnp = upnp_only elif ipv6_only: - ipvs = ['ipv6',] + ipvs = ['ipv6', ] upnp = upnp_only elif upnp_only: ipvs = [] @@ -178,7 +178,7 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False): ports = sorted(set(ports['ipv4']) | set(ports['ipv6'])) # Format returned dict - ret = { "opened_ports": ports } + ret = {"opened_ports": ports} if list_forwarded: # Combine TCP and UDP forwarded ports ret['forwarded_ports'] = sorted( @@ -224,8 +224,8 @@ def firewall_reload(skip_upnp=False): # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv4'][protocol]: - rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT" \ - % (protocol, process.quote(str(port)))) + rules.append("iptables -w -A INPUT -p %s --dport %s -j ACCEPT" + % (protocol, process.quote(str(port)))) rules += [ "iptables -w -A INPUT -i lo -j ACCEPT", "iptables -w -A INPUT -p icmp -j ACCEPT", @@ -253,8 +253,8 @@ def firewall_reload(skip_upnp=False): # Iterate over ports and add rule for protocol in ['TCP', 'UDP']: for port in firewall['ipv6'][protocol]: - rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT" \ - % (protocol, process.quote(str(port)))) + rules.append("ip6tables -w -A INPUT -p %s --dport %s -j ACCEPT" + % (protocol, process.quote(str(port)))) rules += [ "ip6tables -w -A INPUT -i lo -j ACCEPT", "ip6tables -w -A INPUT -p icmpv6 -j ACCEPT", @@ -308,13 +308,14 @@ def firewall_upnp(action='status', no_refresh=False): try: # Remove old cron job os.remove('/etc/cron.d/yunohost-firewall') - except: pass + except: + pass action = 'status' no_refresh = False if action == 'status' and no_refresh: # Only return current state - return { 'enabled': enabled } + return {'enabled': enabled} elif action == 'enable' or (enabled and action == 'status'): # Add cron job with open(upnp_cron_job, 'w+') as f: @@ -330,7 +331,8 @@ def firewall_upnp(action='status', no_refresh=False): try: # Remove cron job os.remove(upnp_cron_job) - except: pass + except: + pass enabled = False if action == 'status': no_refresh = True @@ -364,7 +366,8 @@ def firewall_upnp(action='status', no_refresh=False): if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) - except: pass + except: + pass if not enabled: continue try: @@ -403,7 +406,7 @@ def firewall_upnp(action='status', no_refresh=False): if action == 'enable' and not enabled: raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) - return { 'enabled': enabled } + return {'enabled': enabled} def firewall_stop(): @@ -444,12 +447,14 @@ def _get_ssh_port(default=22): pass return default + def _update_firewall_file(rules): """Make a backup and write new rules to firewall file""" os.system("cp {0} {0}.old".format(firewall_file)) with open(firewall_file, 'w') as f: yaml.safe_dump(rules, f, default_flow_style=False) + def _on_rule_command_error(returncode, cmd, output): """Callback for rules commands error""" # Log error and continue commands execution diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 315fb3a08..18089e328 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -44,8 +44,8 @@ from yunohost.domain import get_public_ip logger = getActionLogger('yunohost.monitor') -glances_uri = 'http://127.0.0.1:61209' -stats_path = '/var/lib/yunohost/stats' +glances_uri = 'http://127.0.0.1:61209' +stats_path = '/var/lib/yunohost/stats' crontab_path = '/etc/cron.d/yunohost-monitor' @@ -87,13 +87,13 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): # Retrieve monitoring for unit(s) for u in units: if u == 'io': - ## Define setter + # Define setter if len(units) > 1: def _set(dn, dvalue): try: result[dn][u] = dvalue except KeyError: - result[dn] = { u: dvalue } + result[dn] = {u: dvalue} else: def _set(dn, dvalue): result[dn] = dvalue @@ -111,13 +111,13 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): for dname in devices_names: _set(dname, 'not-available') elif u == 'filesystem': - ## Define setter + # Define setter if len(units) > 1: def _set(dn, dvalue): try: result[dn][u] = dvalue except KeyError: - result[dn] = { u: dvalue } + result[dn] = {u: dvalue} else: def _set(dn, dvalue): result[dn] = dvalue @@ -183,11 +183,11 @@ def monitor_network(units=None, human_readable=False): smtp_check = m18n.n('network_check_smtp_ko') try: - answers = dns.resolver.query(domain,'MX') + answers = dns.resolver.query(domain, 'MX') mx_check = {} i = 0 for server in answers: - mx_id = 'mx%s' %i + mx_id = 'mx%s' % i mx_check[mx_id] = server i = i + 1 except: @@ -307,7 +307,7 @@ def monitor_update_stats(period): stats = _retrieve_stats(period) if not stats: - stats = { 'disk': {}, 'network': {}, 'system': {}, 'timestamp': [] } + stats = {'disk': {}, 'network': {}, 'system': {}, 'timestamp': []} monitor = None # Get monitoring stats @@ -357,7 +357,7 @@ def monitor_update_stats(period): if 'usage' in stats['network'] and iname in stats['network']['usage']: curr = stats['network']['usage'][iname] net_usage[iname] = _append_to_stats(curr, values, 'time_since_update') - stats['network'] = { 'usage': net_usage, 'infos': monitor['network']['infos'] } + stats['network'] = {'usage': net_usage, 'infos': monitor['network']['infos']} # Append system stats for unit, values in monitor['system'].items(): @@ -421,7 +421,7 @@ def monitor_enable(with_stats=False): rules = ('*/5 * * * * root {cmd} day >> /dev/null\n' '3 * * * * root {cmd} week >> /dev/null\n' '6 */4 * * * root {cmd} month >> /dev/null').format( - cmd='/usr/bin/yunohost --quiet monitor update-stats') + cmd='/usr/bin/yunohost --quiet monitor update-stats') with open(crontab_path, 'w') as f: f.write(rules) @@ -530,7 +530,7 @@ def binary_to_human(n, customary=False): symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') prefix = {} for i, s in enumerate(symbols): - prefix[s] = 1 << (i+1)*10 + prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] @@ -590,7 +590,7 @@ def _save_stats(stats, period, date=None): # Limit stats if date is None: t = stats['timestamp'] - limit = { 'day': 86400, 'week': 604800, 'month': 2419200 } + limit = {'day': 86400, 'week': 604800, 'month': 2419200} if (t[len(t) - 1] - t[0]) > limit[period]: begin = t[len(t) - 1] - limit[period] stats = _filter_stats(stats, begin) @@ -612,7 +612,7 @@ def _monitor_all(period=None, since=None): since -- Timestamp of the stats beginning """ - result = { 'disk': {}, 'network': {}, 'system': {} } + result = {'disk': {}, 'network': {}, 'system': {}} # Real-time stats if period == 'day' and since is None: @@ -697,7 +697,7 @@ def _calculate_stats_mean(stats): s[k] = _mean(v, t, ts) elif isinstance(v, list): try: - nums = [ float(x * t[i]) for i, x in enumerate(v) ] + nums = [float(x * t[i]) for i, x in enumerate(v)] except: pass else: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b1b776326..dcd3dee83 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -60,9 +60,9 @@ def service_add(name, status=None, log=None, runlevel=None): services = _get_services() if not status: - services[name] = { 'status': 'service' } + services[name] = {'status': 'service'} else: - services[name] = { 'status': status } + services[name] = {'status': status} if log is not None: services[name]['log'] = log @@ -202,7 +202,7 @@ def service_status(names=[]): status = None if 'status' not in services[name] or \ - services[name]['status'] == 'service': + services[name]['status'] == 'service': status = 'service %s status' % name else: status = str(services[name]['status']) @@ -211,7 +211,7 @@ def service_status(names=[]): if 'runlevel' in services[name].keys(): runlevel = int(services[name]['runlevel']) - result[name] = { 'status': 'unknown', 'loaded': 'unknown' } + result[name] = {'status': 'unknown', 'loaded': 'unknown'} # Retrieve service status try: @@ -261,7 +261,7 @@ def service_log(name, number=50): for log_path in log_list: if os.path.isdir(log_path): - for log in [ f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log' ]: + for log in [f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log']: result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number)) else: result[log_path] = _tail(log_path, int(number)) @@ -314,13 +314,14 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, common_args = [1 if force else 0, 1 if dry_run else 0] # Execute hooks for pre-regen - pre_args = ['pre',] + common_args + pre_args = ['pre', ] + common_args + def _pre_call(name, priority, path, args): # create the pending conf directory for the service service_pending_path = os.path.join(pending_conf_dir, name) filesystem.mkdir(service_pending_path, 0755, True, uid='admin') # return the arguments to pass to the script - return pre_args + [service_pending_path,] + return pre_args + [service_pending_path, ] pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call) # Update the services name @@ -336,8 +337,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): logger.info(m18n.n( - 'service_regenconf_pending_applying' if not dry_run else \ - 'service_regenconf_dry_pending_applying', + 'service_regenconf_pending_applying' if not dry_run else + 'service_regenconf_dry_pending_applying', service=service)) conf_hashes = _get_conf_hashes(service) @@ -444,8 +445,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, continue elif not failed_regen: logger.success(m18n.n( - 'service_conf_updated' if not dry_run else \ - 'service_conf_would_be_updated', + 'service_conf_updated' if not dry_run else + 'service_conf_would_be_updated', service=service)) if succeed_regen and not dry_run: _update_conf_hashes(service, conf_hashes) @@ -461,14 +462,15 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, return result # Execute hooks for post-regen - post_args = ['post',] + common_args + post_args = ['post', ] + common_args + def _pre_call(name, priority, path, args): # append coma-separated applied changes for the service if name in result and result[name]['applied']: regen_conf_files = ','.join(result[name]['applied'].keys()) else: regen_conf_files = '' - return post_args + [regen_conf_files,] + return post_args + [regen_conf_files, ] hook_callback('conf_regen', names, pre_callback=_pre_call) return result @@ -556,7 +558,8 @@ def _tail(file, n, offset=None): return lines[-to_read:offset and -offset or None] avg_line_length *= 1.3 - except IOError: return [] + except IOError: + return [] def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 7ea9852b7..9de9595f4 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -50,12 +50,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): fields -- fields to fetch """ - user_attrs = { 'uid': 'username', - 'cn': 'fullname', - 'mail': 'mail', - 'maildrop': 'mail-forward', - 'mailuserquota': 'mailbox-quota' } - attrs = [ 'uid' ] + user_attrs = {'uid': 'username', + 'cn': 'fullname', + 'mail': 'mail', + 'maildrop': 'mail-forward', + 'mailuserquota': 'mailbox-quota'} + attrs = ['uid'] users = {} # Set default arguments values @@ -74,12 +74,12 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): raise MoulinetteError(errno.EINVAL, m18n.n('field_invalid', attr)) else: - attrs = [ 'uid', 'cn', 'mail', 'mailuserquota' ] + attrs = ['uid', 'cn', 'mail', 'mailuserquota'] result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs) if len(result) > offset and limit > 0: - for user in result[offset:offset+limit]: + for user in result[offset:offset + limit]: entry = {} for attr, values in user.items(): try: @@ -88,7 +88,7 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): pass uid = entry[user_attrs['uid']] users[uid] = entry - return { 'users' : users } + return {'users': users} def user_create(auth, username, firstname, lastname, mail, password, @@ -112,8 +112,8 @@ def user_create(auth, username, firstname, lastname, mail, password, # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ - 'uid' : username, - 'mail' : mail + 'uid': username, + 'mail': mail }) # Validate uniqueness of username in system users @@ -125,10 +125,10 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists - if mail[mail.find('@')+1:] not in domain_list(auth)['domains']: + if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) # Get random UID/GID uid_check = gid_check = 0 @@ -141,24 +141,24 @@ def user_create(auth, username, firstname, lastname, mail, password, fullname = '%s %s' % (firstname, lastname) rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set,8)) + salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { - 'objectClass' : ['mailAccount', 'inetOrgPerson', 'posixAccount'], - 'givenName' : firstname, - 'sn' : lastname, - 'displayName' : fullname, - 'cn' : fullname, - 'uid' : username, - 'mail' : mail, - 'maildrop' : username, - 'mailuserquota' : mailbox_quota, - 'userPassword' : user_pwd, - 'gidNumber' : uid, - 'uidNumber' : uid, - 'homeDirectory' : '/home/' + username, - 'loginShell' : '/bin/false' + 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'], + 'givenName': firstname, + 'sn': lastname, + 'displayName': fullname, + 'cn': fullname, + 'uid': username, + 'mail': mail, + 'maildrop': username, + 'mailuserquota': mailbox_quota, + 'userPassword': user_pwd, + 'gidNumber': uid, + 'uidNumber': uid, + 'homeDirectory': '/home/' + username, + 'loginShell': '/bin/false' } # If it is the first user, add some aliases @@ -166,12 +166,12 @@ def user_create(auth, username, firstname, lastname, mail, password, with open('/etc/yunohost/current_host') as f: main_domain = f.readline().rstrip() aliases = [ - 'root@'+ main_domain, - 'admin@'+ main_domain, - 'webmaster@'+ main_domain, - 'postmaster@'+ main_domain, + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, ] - attr_dict['mail'] = [ attr_dict['mail'] ] + aliases + attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO try: @@ -192,7 +192,6 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) - if auth.add(rdn, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) @@ -200,7 +199,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] memberlist.append(username) - if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): + if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): try: # Attempt to create user home folder subprocess.check_call( @@ -210,12 +209,12 @@ def user_create(auth, username, firstname, lastname, mail, password, logger.warning(m18n.n('user_home_creation_failed'), exc_info=1) app_ssowatconf(auth) - #TODO: Send a welcome mail to user + # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) hook_callback('post_user_create', args=[username, mail, password, firstname, lastname]) - return { 'fullname' : fullname, 'username' : username, 'mail' : mail } + return {'fullname': fullname, 'username': username, 'mail': mail} raise MoulinetteError(169, m18n.n('user_creation_failed')) @@ -238,9 +237,11 @@ def user_delete(auth, username, purge=False): # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] - try: memberlist.remove(username) - except: pass - if auth.update('cn=sftpusers,ou=groups', { 'memberUid': memberlist }): + try: + memberlist.remove(username) + except: + pass + if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: @@ -286,11 +287,11 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, # Get modifications from arguments if firstname: - new_attr_dict['givenName'] = firstname # TODO: Validate + new_attr_dict['givenName'] = firstname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + user['sn'][0] if lastname: - new_attr_dict['sn'] = lastname # TODO: Validate + new_attr_dict['sn'] = lastname # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = user['givenName'][0] + ' ' + lastname if lastname and firstname: @@ -298,34 +299,34 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if change_password: char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set,8)) + salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt) if mail: - auth.validate_uniqueness({ 'mail': mail }) - if mail[mail.find('@')+1:] not in domains: + auth.validate_uniqueness({'mail': mail}) + if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] if add_mailalias: if not isinstance(add_mailalias, list): - add_mailalias = [ add_mailalias ] + add_mailalias = [add_mailalias] for mail in add_mailalias: - auth.validate_uniqueness({ 'mail': mail }) - if mail[mail.find('@')+1:] not in domains: + auth.validate_uniqueness({'mail': mail}) + if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@')+1:])) + domain=mail[mail.find('@') + 1:])) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] if remove_mailalias: if not isinstance(remove_mailalias, list): - remove_mailalias = [ remove_mailalias ] + remove_mailalias = [remove_mailalias] for mail in remove_mailalias: if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) @@ -336,7 +337,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if add_mailforward: if not isinstance(add_mailforward, list): - add_mailforward = [ add_mailforward ] + add_mailforward = [add_mailforward] for mail in add_mailforward: if mail in user['maildrop'][1:]: continue @@ -345,7 +346,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if remove_mailforward: if not isinstance(remove_mailforward, list): - remove_mailforward = [ remove_mailforward ] + remove_mailforward = [remove_mailforward] for mail in remove_mailforward: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) @@ -358,11 +359,11 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, new_attr_dict['mailuserquota'] = mailbox_quota if auth.update('uid=%s,ou=users' % username, new_attr_dict): - logger.success(m18n.n('user_updated')) - app_ssowatconf(auth) - return user_info(auth, username) + logger.success(m18n.n('user_updated')) + app_ssowatconf(auth) + return user_info(auth, username) else: - raise MoulinetteError(169, m18n.n('user_update_failed')) + raise MoulinetteError(169, m18n.n('user_update_failed')) def user_info(auth, username): @@ -378,9 +379,9 @@ def user_info(auth, username): ] if len(username.split('@')) is 2: - filter = 'mail='+ username + filter = 'mail=' + username else: - filter = 'uid='+ username + filter = 'uid=' + username result = auth.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) @@ -436,8 +437,8 @@ def user_info(auth, username): storage_use += ' (%s%%)' % percentage result_dict['mailbox-quota'] = { - 'limit' : userquota if is_limited else m18n.n('unlimit'), - 'use' : storage_use + 'limit': userquota if is_limited else m18n.n('unlimit'), + 'use': storage_use } if result: @@ -445,8 +446,9 @@ def user_info(auth, username): else: raise MoulinetteError(167, m18n.n('user_info_failed')) + def _convertSize(num, suffix=''): - for unit in ['K','M','G','T','P','E','Z']: + for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 5be2103e5..2372e7442 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -424,6 +424,7 @@ def get_installed_version(*pkgnames, **kwargs): return versions[pkgnames[0]] return versions + def meets_version_specifier(pkgname, specifier): """Check if a package installed version meets specifier""" spec = SpecifierSet(specifier) diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py index 0f4cb431f..d0ba33d1e 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -1,9 +1,21 @@ #!/usr/bin/env python -import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging +import argparse +import subprocess +import json +import os +import sys +import base64 +import binascii +import time +import hashlib +import re +import copy +import textwrap +import logging try: - from urllib.request import urlopen # Python 3 + from urllib.request import urlopen # Python 3 except ImportError: - from urllib2 import urlopen # Python 2 + from urllib2 import urlopen # Python 2 #DEFAULT_CA = "https://acme-staging.api.letsencrypt.org" DEFAULT_CA = "https://acme-v01.api.letsencrypt.org" @@ -12,6 +24,7 @@ LOGGER = logging.getLogger(__name__) LOGGER.addHandler(logging.StreamHandler()) LOGGER.setLevel(logging.INFO) + def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): # helper function base64 encode for jose spec def _b64(b): @@ -26,7 +39,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): raise IOError("OpenSSL Error: {0}".format(err)) pub_hex, pub_exp = re.search( r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", - out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + out.decode('utf8'), re.MULTILINE | re.DOTALL).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp header = { @@ -72,7 +85,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) if common_name is not None: domains.add(common_name.group(1)) - subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE | re.DOTALL) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): @@ -165,6 +178,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))) + def main(argv): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, @@ -194,5 +208,5 @@ def main(argv): signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca) sys.stdout.write(signed_crt) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover main(sys.argv[1:]) From 3f22f15dd392a8c612383fa060defe16a295ed34 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 18 Dec 2016 23:57:56 +0100 Subject: [PATCH 0173/1066] [fix] add timeout to fetchlist's wget --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 85693a0dd..a35ccb7e6 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -99,7 +99,7 @@ def app_fetchlist(url=None, name=None): m18n.n('custom_appslist_name_required')) list_file = '%s/%s.json' % (repo_path, name) - if os.system('wget "%s" -O "%s.tmp"' % (url, list_file)) != 0: + if os.system('wget --timeout=30 "%s" -O "%s.tmp"' % (url, list_file)) != 0: os.remove('%s.tmp' % list_file) raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error')) From e857f4f0b27d71299c498305b24e4b3f7e4571c4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 19 Dec 2016 12:17:27 -0500 Subject: [PATCH 0174/1066] [mod] Cleaner logs for _get_conf_hashes --- src/yunohost/service.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index dcd3dee83..2d9068180 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -639,12 +639,17 @@ def _get_pending_conf(services=[]): def _get_conf_hashes(service): """Get the registered conf hashes for a service""" - try: - return _get_services()[service]['conffiles'] - except: - logger.debug("unable to retrieve conf hashes for %s", - service, exc_info=1) + + d = _get_services() + + if (service not in d.keys()): + logger.debug("Service %s is not in services.yml yet.", service) return {} + elif ('conffiles' not in d[service].keys()): + logger.debug("No configuration files for service %s.", service) + return {} + else: + return d[service]['conffiles'] def _update_conf_hashes(service, hashes): From 5b7536cf1036cecee6fcc187b2d1c3f9b7124093 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 19 Dec 2016 12:18:28 -0500 Subject: [PATCH 0175/1066] Style for Bram :) --- src/yunohost/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 2d9068180..f17edd567 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -642,10 +642,10 @@ def _get_conf_hashes(service): d = _get_services() - if (service not in d.keys()): + if service not in d.keys(): logger.debug("Service %s is not in services.yml yet.", service) return {} - elif ('conffiles' not in d[service].keys()): + elif 'conffiles' not in d[service].keys(): logger.debug("No configuration files for service %s.", service) return {} else: From 0b6ccaf31a8301b50648ec0ba0473d2190384355 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 19 Dec 2016 18:24:01 -0500 Subject: [PATCH 0176/1066] Implementing comments --- src/yunohost/service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f17edd567..01b85dfbe 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -640,16 +640,16 @@ def _get_pending_conf(services=[]): def _get_conf_hashes(service): """Get the registered conf hashes for a service""" - d = _get_services() + services = _get_services() - if service not in d.keys(): + if service not in services: logger.debug("Service %s is not in services.yml yet.", service) return {} - elif 'conffiles' not in d[service].keys(): + elif 'conffiles' not in services[service]: logger.debug("No configuration files for service %s.", service) return {} else: - return d[service]['conffiles'] + return services[service]['conffiles'] def _update_conf_hashes(service, hashes): From 418c9b273614a7000b028a720df954416295525b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 21 Dec 2016 21:20:23 -0500 Subject: [PATCH 0177/1066] Check acme challenge conf exists in nginx when renewing cert --- locales/en.json | 1 + src/yunohost/certificate.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 468be0c1c..dc020645d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -264,5 +264,6 @@ "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 01852d2ec..f1d663eba 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -323,8 +323,16 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal continue # Does it expire soon? - if force or status["validity"] <= VALIDITY_LIMIT: - domain_list.append(domain) + if status["validity"] > VALIDITY_LIMIT and not force: + continue + + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + logger.warning(m18n.n( + 'certmanager_acme_not_configured_for_domain', domain=domain)) + continue + + domain_list.append(domain) if len(domain_list) == 0: logger.info("No certificate needs to be renewed.") @@ -350,6 +358,11 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + # Check ACME challenge configured for given domain + if not _check_acme_challenge_configuration(domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_acme_not_configured_for_domain', domain=domain)) + if staging: logger.warning( "Please note that you used the --staging option, and that no new certificate will actually be enabled !") @@ -362,6 +375,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: _check_domain_is_ready_for_ACME(domain) + _fetch_and_enable_new_certificate(domain, staging) logger.success( @@ -487,6 +501,17 @@ location '/.well-known/acme-challenge' app_ssowatconf(auth) +def _check_acme_challenge_configuration(domain): + # Check nginx conf file exists + nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain + nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder + + if not os.path.exists(nginx_conf_file): + return False + else: + return True + + def _fetch_and_enable_new_certificate(domain, staging=False): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") From d6d7abd959d9555bbf494418b273a79b8fafd245 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 21 Dec 2016 21:38:49 -0500 Subject: [PATCH 0178/1066] Fix bad validity check.. --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f1d663eba..74aa3c396 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -349,7 +349,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal status = _get_status(domain) # Does it expire soon? - if not force or status["validity"] <= VALIDITY_LIMIT: + if status["validity"] > VALIDITY_LIMIT and not force: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_valid_cert', domain=domain)) From ab279d9c6b717ec5317ed3854b43e4ef5e8e2143 Mon Sep 17 00:00:00 2001 From: zimo2001 Date: Sat, 17 Dec 2016 00:19:53 +0200 Subject: [PATCH 0179/1066] Update yunohost.conf In my nginx_error.log, the authentication errors are "helpers.lua", not "access.lua". --- data/templates/fail2ban/yunohost.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/fail2ban/yunohost.conf b/data/templates/fail2ban/yunohost.conf index 54d4a779f..3ca8f1c8f 100644 --- a/data/templates/fail2ban/yunohost.conf +++ b/data/templates/fail2ban/yunohost.conf @@ -14,7 +14,7 @@ # (?:::f{4,6}:)?(?P[\w\-.^_]+) # Values: TEXT # -failregex = access.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: +failregex = helpers.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22 # Option: ignoreregex From 6cf57e3bb06759b1854c0bd845228f4ac1ad4c36 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 2 Jan 2017 15:12:22 +0100 Subject: [PATCH 0180/1066] [fix] Ignore dyndns option is not needed with small domain --- src/yunohost/tools.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 7cc221683..ac3d2c490 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -178,7 +178,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): Keyword argument: domain -- YunoHost main domain - ignore_dyndns -- Do not subscribe domain to a DynDNS service + ignore_dyndns -- Do not subscribe domain to a DynDNS service (only + needed for nohost.me, noho.st domains) password -- YunoHost admin password """ @@ -203,6 +204,10 @@ def tools_postinstall(domain, password, ignore_dyndns=False): else: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) + else: + dyndns = False + else: + dyndns = False logger.info(m18n.n('yunohost_installing')) @@ -296,7 +301,6 @@ def tools_postinstall(domain, password, ignore_dyndns=False): os.system('service yunohost-firewall start') service_regen_conf(force=True) - logger.success(m18n.n('yunohost_configured')) From 6277e6d7f62906a8f5d6258e880fccc88c415b9b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 2 Jan 2017 22:03:55 +0100 Subject: [PATCH 0181/1066] [mod] logger was used instead of loggin #microdecision --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 7cc221683..ce9445daf 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -416,7 +416,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): apt.progress.base.InstallProgress()) except Exception as e: failure = True - logging.warning('unable to upgrade packages: %s' % str(e)) + logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) @@ -428,7 +428,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): app_upgrade(auth) except Exception as e: failure = True - logging.warning('unable to upgrade apps: %s' % str(e)) + logger.warning('unable to upgrade apps: %s' % str(e)) logger.error(m18n.n('app_upgrade_failed')) if not failure: From b829de72180288707aa16e8435ecd39f1e9f9ac3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 3 Jan 2017 09:36:33 -0500 Subject: [PATCH 0182/1066] Adding check that domain is resolved locally for cert management --- locales/en.json | 1 + src/yunohost/certificate.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/locales/en.json b/locales/en.json index 468be0c1c..25d0f6542 100644 --- a/locales/en.json +++ b/locales/en.json @@ -253,6 +253,7 @@ "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved locally. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 01852d2ec..652d6b653 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -785,6 +785,13 @@ def _check_domain_is_ready_for_ACME(domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_http_not_working', domain=domain)) + # Check if domain is resolved locally (Might happen despite the previous + # checks because of dns propagation ?... Acme-tiny won't work in that case, + # because it explicitly requests() the domain.) + if not _domain_is_resolved_locally(domain): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_not_resolved_locally', domain=domain)) + def _dns_ip_match_public_ip(public_ip, domain): try: @@ -809,6 +816,15 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True +def _domain_is_resolved_locally(domain): + try: + requests.head("http://%s/" % domain) + except Exception: + return False + + return True + + def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") From b419c8c5641f29284befc29b059ee37db0b74a59 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 3 Jan 2017 23:18:01 +0100 Subject: [PATCH 0183/1066] [fix] new rspamd version replace rspamd.socket with rspamd.service --- data/hooks/conf_regen/28-rmilter | 13 ++++--------- data/hooks/conf_regen/31-rspamd | 7 +++---- data/templates/postfix/main.cf | 2 +- data/templates/rmilter/rmilter.conf | 3 +-- data/templates/rmilter/rmilter.socket | 5 ----- 5 files changed, 9 insertions(+), 21 deletions(-) delete mode 100644 data/templates/rmilter/rmilter.socket diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter index 05f921e09..b783a5188 100755 --- a/data/hooks/conf_regen/28-rmilter +++ b/data/hooks/conf_regen/28-rmilter @@ -9,8 +9,6 @@ do_pre_regen() { install -D -m 644 rmilter.conf \ "${pending_dir}/etc/rmilter.conf" - install -D -m 644 rmilter.socket \ - "${pending_dir}/etc/systemd/system/rmilter.socket" } do_post_regen() { @@ -40,14 +38,11 @@ do_post_regen() { [ -z "$regen_conf_files" ] && exit 0 # reload systemd daemon - [[ "$regen_conf_files" =~ rmilter\.socket ]] && { - sudo systemctl -q daemon-reload - } + sudo systemctl -q daemon-reload - # ensure that the socket is listening and stop the service - it will be - # started again by the socket as needed - sudo systemctl -q start rmilter.socket - sudo systemctl -q stop rmilter.service 2>&1 || true + # Restart rmilter due to the rspamd update + # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html + sudo systemctl -q restart rmilter.service } FORCE=${2:-0} diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index 327bedef1..afdfc1bf1 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -25,10 +25,9 @@ do_post_regen() { sudo systemctl restart dovecot } - # ensure that the socket is listening and stop the service - it will be - # started again by the socket as needed - sudo systemctl -q start rspamd.socket - sudo systemctl -q stop rspamd.service 2>&1 || true + # Restart rspamd due to the upgrade + # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html + sudo systemctl -q restart rspamd.service } FORCE=${2:-0} diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index f3597e136..ffea0aee3 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -141,7 +141,7 @@ smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter # Rmilter milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_protocol = 6 -smtpd_milters = inet:localhost:11000 +smtpd_milters = unix:/var/tmp/rmilter.sock # Skip email without checking if milter has died milter_default_action = accept diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf index d585b9217..c09a604b8 100644 --- a/data/templates/rmilter/rmilter.conf +++ b/data/templates/rmilter/rmilter.conf @@ -5,8 +5,7 @@ # pidfile - path to pid file pidfile = /run/rmilter/rmilter.pid; -# rmilter is socket-activated under systemd -bind_socket = fd:3; +bind_socket = unix:/var/tmp/rmilter.sock; # DKIM signing dkim { diff --git a/data/templates/rmilter/rmilter.socket b/data/templates/rmilter/rmilter.socket deleted file mode 100644 index dc3ae7a2a..000000000 --- a/data/templates/rmilter/rmilter.socket +++ /dev/null @@ -1,5 +0,0 @@ -.include /lib/systemd/system/rmilter.socket - -[Socket] -ListenStream= -ListenStream=127.0.0.1:11000 From 86f97d6f071602d5ca9402331fa9607a8de558b6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 4 Jan 2017 13:05:57 -0500 Subject: [PATCH 0184/1066] Changing the way to check domain is locally resolved --- locales/en.json | 2 +- src/yunohost/certificate.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index 25d0f6542..387b8c2e8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -253,7 +253,7 @@ "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved locally. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 652d6b653..eb1e44732 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,6 +31,7 @@ import grp import smtplib import requests import subprocess +import socket import dns.resolver import glob @@ -788,7 +789,7 @@ def _check_domain_is_ready_for_ACME(domain): # Check if domain is resolved locally (Might happen despite the previous # checks because of dns propagation ?... Acme-tiny won't work in that case, # because it explicitly requests() the domain.) - if not _domain_is_resolved_locally(domain): + if not _domain_is_resolved_locally(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_not_resolved_locally', domain=domain)) @@ -816,13 +817,13 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True -def _domain_is_resolved_locally(domain): +def _domain_is_resolved_locally(public_ip, domain): try: - requests.head("http://%s/" % domain) - except Exception: + ip = socket.gethostbyname(domain) + except socket.error: return False - return True + return (ip in ["127.0.0.1", public_ip]) def _name_self_CA(): From 8737e69359745642ae12c341409db6483792af9e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 4 Jan 2017 13:41:00 -0500 Subject: [PATCH 0185/1066] Attempt to fix missing perm for metronome in weird cases --- src/yunohost/certificate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 01852d2ec..7ffd468b6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -562,7 +562,9 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(new_cert_folder, "root", "root", 0655) # Move the private key - shutil.move(domain_key_file, os.path.join(new_cert_folder, "key.pem")) + domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") + shutil.move(domain_key_file, domain_key_file_finaldest) + _set_permissions(domain_key_file_finaldest, "root", "metronome", 0640) # Write the cert domain_cert_file = os.path.join(new_cert_folder, "crt.pem") From 047026df6caa9fad6b5e4c45216a9a57a65968f6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 8 Jan 2017 05:28:13 +0100 Subject: [PATCH 0186/1066] [mod] remove () --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index eb1e44732..b91bc74b6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -823,7 +823,7 @@ def _domain_is_resolved_locally(public_ip, domain): except socket.error: return False - return (ip in ["127.0.0.1", public_ip]) + return ip in ["127.0.0.1", public_ip] def _name_self_CA(): From d665984789058c99ca046f707f9ecf7d20b537cd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 8 Jan 2017 05:31:11 +0100 Subject: [PATCH 0187/1066] [mod] add debugging code to avoid becoming mad when things goes wrong --- src/yunohost/certificate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b91bc74b6..a53f5f669 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -820,9 +820,11 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _domain_is_resolved_locally(public_ip, domain): try: ip = socket.gethostbyname(domain) - except socket.error: + except socket.error as e: + logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) return False + logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, public_ip)) return ip in ["127.0.0.1", public_ip] From 4b9d2526db388baf9e4710806c358b155297db6c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 8 Jan 2017 05:36:39 +0100 Subject: [PATCH 0188/1066] [mod] more debug logging --- src/yunohost/certificate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9d52f3a5d..5f851471c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -838,7 +838,8 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: requests.head("http://" + ip, headers={"Host": domain}) - except Exception: + except Exception as e: + logger.debug("Couldn't reach domain '%s' by requesting this ip '%s' because: %s" % (domain, ip, e)) return False return True From d4e4d01b4f1641e6bf9bea8388fd91565955a2f3 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 9 Jan 2017 13:37:34 +0100 Subject: [PATCH 0189/1066] [fix] Remove residual rmilter socket file --- data/hooks/conf_regen/28-rmilter | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter index b783a5188..db42371e8 100755 --- a/data/hooks/conf_regen/28-rmilter +++ b/data/hooks/conf_regen/28-rmilter @@ -9,6 +9,10 @@ do_pre_regen() { install -D -m 644 rmilter.conf \ "${pending_dir}/etc/rmilter.conf" + # Remove old socket file (we stopped using it, since rspamd 1.3.1) + # Regen-conf system need an empty file to delete it + install -D -m 644 /dev/null \ + "${pending_dir}/etc/systemd/system/rmilter.socket" } do_post_regen() { From 2cf4768aa17fca02f1d0e15f6f980c4a279bc931 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 9 Jan 2017 15:34:52 +0100 Subject: [PATCH 0190/1066] [fix] Postfix can't access rmilter socket due to chroot --- data/hooks/conf_regen/28-rmilter | 5 +++++ data/templates/postfix/main.cf | 2 +- data/templates/rmilter/rmilter.conf | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter index db42371e8..011856cd6 100755 --- a/data/hooks/conf_regen/28-rmilter +++ b/data/hooks/conf_regen/28-rmilter @@ -39,6 +39,11 @@ do_post_regen() { sudo chown _rmilter /etc/dkim/*.mail.key sudo chmod 400 /etc/dkim/*.mail.key + # fix rmilter socket permission (postfix is chrooted in /var/spool/postfix ) + sudo mkdir -p /var/spool/postfix/run/rmilter + sudo chown -R postfix:_rmilter /var/spool/postfix/run/rmilter + sudo chmod g+w /var/spool/postfix/run/rmilter + [ -z "$regen_conf_files" ] && exit 0 # reload systemd daemon diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index ffea0aee3..b0b2688d9 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -141,7 +141,7 @@ smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter # Rmilter milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_protocol = 6 -smtpd_milters = unix:/var/tmp/rmilter.sock +smtpd_milters = unix:/run/rmilter/rmilter.sock # Skip email without checking if milter has died milter_default_action = accept diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf index c09a604b8..829d76418 100644 --- a/data/templates/rmilter/rmilter.conf +++ b/data/templates/rmilter/rmilter.conf @@ -5,7 +5,7 @@ # pidfile - path to pid file pidfile = /run/rmilter/rmilter.pid; -bind_socket = unix:/var/tmp/rmilter.sock; +bind_socket = unix:/var/spool/postfix/run/rmilter/rmilter.sock; # DKIM signing dkim { From 7f5604653b1499df861825abe9bfbb5b9ce06c3a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Jan 2017 18:43:40 +0100 Subject: [PATCH 0191/1066] Update changelog for 2.5.3 release --- debian/changelog | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/debian/changelog b/debian/changelog index 248c934fd..9004d14bd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,37 @@ +yunohost (2.5.3) testing; urgency=low + + Love: + * [enh][love] Add CONTRIBUTORS.md + + LE: + * Check acme challenge conf exists in nginx when renewing cert + * Fix bad validity check.. + + Fix a situation where to domain for the LE cert can't be locally resolved: + * Adding check that domain is resolved locally for cert management + * Changing the way to check domain is locally resolved + + Fix a situation where a cert could end up with bad perms for metronome: + * Attempt to fix missing perm for metronome in weird cases + + Rspamd cannot be activate on socket anymore: + * [fix] new rspamd version replace rspamd.socket with rspamd.service + * [fix] Remove residual rmilter socket file + * [fix] Postfix can't access rmilter socket due to chroot + + Various: + * fix fail2ban rules to take into account failed loggin on ssowat + * [fix] Ignore dyndns option is not needed with small domain + * [enh] add yaml syntax check in travis.yml + * [mod] autopep8 on all files that aren't concerned by a PR + * [fix] add timeout to fetchlist's wget + + Thanks to all contributors: Aleks, Bram, ju, ljf, opi, zimo2001 and to the + people who are participating to the beta and giving us feedback <3 + + + -- Laurent Peuch Mon, 09 Jan 2017 18:38:30 +0100 + yunohost (2.5.2) testing; urgency=low LDAP admin user: From 8a70c33c40ce0d2add85cf5051ed8b730d8aa3e4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 10 Jan 2017 02:56:35 +0100 Subject: [PATCH 0192/1066] [fix] I'm a fucking idiot --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5f851471c..49de94996 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -852,7 +852,7 @@ def _domain_is_resolved_locally(public_ip, domain): logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) return False - logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, public_ip)) + logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, ip, public_ip)) return ip in ["127.0.0.1", public_ip] From b1885ddf64c7ae7687f101916b08a8c83db35607 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 10 Jan 2017 02:56:35 +0100 Subject: [PATCH 0193/1066] [fix] I'm a fucking idiot --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5f851471c..49de94996 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -852,7 +852,7 @@ def _domain_is_resolved_locally(public_ip, domain): logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) return False - logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, public_ip)) + logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, ip, public_ip)) return ip in ["127.0.0.1", public_ip] From c1b5fbab61923ab08d54dfba4e5ad1a5751fc462 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 10 Jan 2017 02:59:24 +0100 Subject: [PATCH 0194/1066] Update changelog for 2.5.3.1 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9004d14bd..2a719fc83 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.5.3.1) testing; urgency=low + + * super quickfix release for a typo that break LE certificates + + -- Laurent Peuch Tue, 10 Jan 2017 02:58:56 +0100 + yunohost (2.5.3) testing; urgency=low Love: From 73d1630459b3c0256007cf441df6046abe6889ee Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 13 Jan 2017 02:09:14 +0100 Subject: [PATCH 0195/1066] [enh] Compatibility with debian stretch --- debian/control | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 47f0e61b2..357b2c766 100644 --- a/debian/control +++ b/debian/control @@ -16,12 +16,12 @@ Depends: ${python:Depends}, ${misc:Depends} , glances , dnsutils, bind9utils, unzip, git, curl, cron , ca-certificates, netcat-openbsd, iproute - , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd + , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd | php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban - , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl + , nginx-extras (>=1.6.2), php5-fpm | php-fpm, php5-ldap | php-ldap, php5-intl | php-intl , dnsmasq, openssl, avahi-daemon , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools @@ -29,7 +29,7 @@ Depends: ${python:Depends}, ${misc:Depends} Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper - , php5-gd, php5-curl, php-gettext, php5-mcrypt + , php5-gd | php-gd, php5-curl | php-curl, php5-gettext | php-gettext, php5-mcrypt | php-mcrypt , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl From e714effdc76c0d6b0c7153aae07015fc85c886d0 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 15 Jan 2017 16:05:50 +0100 Subject: [PATCH 0196/1066] =?UTF-8?q?Hack=20d=C3=A9gueux=20pour=20=C3=A9vi?= =?UTF-8?q?ter=20d'=C3=A9crire=20dans=20le=20log=20cli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un hack vraiment sale pour contourner le log de YunoHost pour les commandes en conditionnel. Autrement, la commande log une erreur dans son log et est considérée comme une commande échouée par l'intégration continue. Le fruit d'une longue discussion stérile: >(14:01:23) Maniack C: @Bram, tu sais comment sont gérés les sorties d'erreurs des commandes YunoHost, par rapport au log? (14:01:44) Bram: Maniack C: il va falloir que tu détails ta question (14:02:27) Maniack C: Les commandes ynh en cli log dans le fichier de log, mais si on redirige la sortie des commandes dans /dev/null, elles log toujours de la même manière (14:03:11) Maniack C: On dirait qu'on a aucun contrôle sur le log des commandes (14:04:13) Maniack C: Est-ce que l'argument --quiet change se comportement? (14:05:00) Bram: je ne sais pas (14:05:22) Bram: si les commandes loguent dans un fichier, rediriger l'output ne changera rien (14:05:41) Maniack C: Oui remarque c'est évident... (14:05:43) Bram: rediriger l'output ça change uniquement les fd stdout/stderr (14:08:18) Maniack C: Hum, et quiet coupe juste la sortie standard (14:08:27) Maniack C: C'est un peu inutile du coup (14:20:47) Maniack C: Je ne trouve --quiet que dans une branche du code... Et là il vire seulement tty (14:20:57) Bram: Maniack C: tu veux faire quoi en fait ? (14:21:17) Maniack C: Trouver ce que fait --quiet et pourquoi (14:21:33) Maniack C: Dans le fond surtout, supprimer toute sortie d'une commande (14:21:44) Maniack C: Pour les usages en if ou while (14:21:46) Bram: pourquoi tu veux faire ça ? (14:21:57) Maniack C: Ça provoque des erreurs (14:22:13) Bram: mais encore ? (14:22:36) Maniack C: typiquement ça: while ! sudo yunohost app checkport $port ; do port=$(($port + 1)) done (14:22:45) Maniack C: La commande sort une erreur (14:23:04) Maniack C: Alors qu'on voudrait juste tester le résultat de la commande (14:23:46) Maniack C: Et le quiet ou le /dev/null te fait juste croire qu'il n'y a pas d'erreur (14:23:55) Bram: et ça devrait faire quoi exactement ? (14:24:01) Bram: (on va finir par y arriver :p) (14:24:04) Bram: #xyproblem (14:24:56) Maniack C: Ba quand tu testes une commande en if, tu élude la sortie d'erreur et tu test juste la sortie de commande (14:25:06) Maniack C: Et aucune erreur n'est affichée (14:25:15) Maniack C: Là on a quand même une erreur dans le log (14:25:32) Bram: tu testerais pas le code de retour ? (14:25:33) Maniack C: Résultat en intégration continue, l'app est cassée (14:25:40) Bram: sinon en vrai tu veux une commande pour avoir un port libre, c'est ça ? (14:25:45) Maniack C: C'est le cas dans ce while (14:26:23) Maniack C: Oui, mais scith a eu le même problème avec ynh_app_setting_get (14:26:56) Bram: ça ressemble surtout à des commandes mal foutu (14:27:06) Bram: et vous devriez plutôt en parler que chercher des hacks en bash :/ (14:27:08) Maniack C: Le problème est qu'on ne peut pas rendre la commande vraiment silencieuse (14:27:49) Maniack C: Pour tester la sortie de commande, il n'y a pas beaucoup de solution (14:27:55) Maniack C: Il faut l'éxecuter (14:28:36) Maniack C: Mais quand on fait ça, on rend la commande silencieuse pour utiliser uniquement son code de sortie (14:28:58) Bram: tu peux utiliser $? (14:29:03) Bram: pour choper uniquement le code de sortie (14:29:09) Bram: au lieu de l'output (14:29:32) Maniack C: Le problème c'est que la commande log une erreur quoi qu'il en soit (14:29:56) Maniack C: Indépendemment de sa sortie, le while travaille sur le code de sortie, ça pose pas de problème ça (14:30:13) Maniack C: Mais elle log toujours une erreur (14:31:55) Bram: hm (14:31:57) Bram: si je comprend bien (14:32:04) Bram: la commande produit pas le bon code d'erreur quand elle devrait (14:32:05) Bram: c'est ça ? (14:32:10) Maniack C: non (14:32:10) Bram: si oui (14:32:20) Bram: alors je capte pas pourquoi le fait qu'elle log est un problème (14:32:47) Maniack C: Parce que derrière c'est le log qui est analysé pour vérifier si tout c'est bien passé (14:32:59) Bram: et pourquoi vous analysez pas le code de sortie ? (14:33:37) Maniack C: > (14:25:33) Maniack C: Résultat en intégration continue, l'app est cassée (14:33:44) Bram: arf (14:33:46) Maniack C: Le problème se répercute ici (14:33:58) Maniack C: C'est le log complet qui est analysé (14:33:58) Bram: ouais donc faudrait bien plutôt des commandes qui marchent comme prévu quoi (14:34:10) Maniack C: Car c'est le seul à être vraiment comlet (14:35:11) Maniack C: Ben faudrait pouvoir faire un quiet complet (14:35:23) Maniack C: Pour n'avoir aucune sortie quand c'est nécessaire (14:35:45) Bram: mouais, ça ressemble vraiment à un gros hack vu d'ici (14:36:20) Maniack C: Le problème c'est que la commande, tu peux bien l'appeler comme tu veux, elle sort toujours une erreur dans son log avant de donner la main au shell parent (14:36:37) Maniack C: Donc on peut pas la faire taire (14:36:46) Maniack C: Pas depuis le shell du moins (14:37:07) Bram: ça me semble un comportement pas déconnant franchement (14:37:34) Maniack C: Disons que le problème c'est qu'il nous faudrait un -qq au lieu d'un simple -q (14:37:42) Maniack C: Le quiet ne fait que couper stdout (14:37:57) Maniack C: Ce qu'on pourrait tout aussi bien faire côté shell (14:38:48) Maniack C: --quietlog par exemple nous serait utile pour rendre la commande vraiment silencieuse quand on veut l'utiliser en if (14:42:47) Maniack C: Et je trouve pas l'argument dans le code... (14:42:56) Bram: je pense pas que ça soit prévu (14:43:05) Bram: ce qui me semble logique d'un point de vue dev hein (14:43:14) Maniack C: Non c'est pas prévu (14:43:15) Bram: y a jamais un moment où un dev va vouloir faire ça pour son soft (14:43:28) Maniack C: Ben si, tu as ça avec d'autres commandes (14:43:36) Maniack C: Aucune sortie (14:43:42) Bram: oui mais là on parle de logs (14:43:46) Bram: c'est pas pareil qu'une sortie (14:44:01) Maniack C: Du coup ça bloque le CI cette histoire (14:44:04) Bram: surtout quand t'as un service qui tourne, refuser de loguer ça me semble pas logique (14:44:52) Bram: si tu veux vraiment empêcher de loger tu peux bouger le fichier de log avant de lancer la commande (14:45:07) Maniack C: Là c'est un cas isolé où on veut aucune sortie pour tester une commande (14:45:14) Bram: mais je continue de penser que cette approche de "pas logger" est une mauvaise approche à un vrai problème (14:45:49) Maniack C: Le problème c'est que là ça empêche d'utiliser des commandes yunohost en test dans les scripts (14:45:53) Bram: ben si c'est un cas isolé faut vraiment pas modifier un comportement globale pour ça mais analyser le cas et y apporter une solution spécifique (14:46:12) Maniack C: C'est pour ça que je pense à un argument comme --quiet (14:46:25) Maniack C: Puisque qu'en shell on peut rien faire (14:54:43) Maniack C: Sans modifier --quiet, on pourrait simplement ajouter un argument avec handlers.remove('file') (14:56:28) Bram: je continue de penser que tu dois lister les cas où ça pose problèmes et qu'on doit les analyser pour voir comment les résoudre plutôt que de faire un gros hack hein (14:56:34) Bram: mais bon, t'as pas l'air de vouloir entendre ça :p (14:56:50) Maniack C: Ben les cas c'est tout les if et les while (14:57:11) Maniack C: Car ils vont potentiellement sortir en erreur (14:57:18) Bram: ben liste tous les if et while où t'as besoin de faire ça (14:57:33) Bram: là par exemple y a le fait qu'il faut une commande pour trouver un port libre qui génère pas d'erreur (14:57:39) Bram: je veux voir quels sont les autres cas (14:57:50) Maniack C: Plus on encourage l'usage des helpers, plus on créer ces situations ! (14:58:04) Maniack C: Jusqu'à présent on codait tous nos propres solutions (14:58:17) Maniack C: Mais attend, j'en ai sous le coude moi (15:00:31) Maniack C: https://github.com/maniackcrudelis/modele_ynh/blob/master/scripts/.fonctions#L97 https://github.com/maniackcrudelis/modele_ynh/blob/master/scripts/.fonctions#L125 https://github.com/maniackcrudelis/modele_ynh/blob/master/scripts/.fonctions#L133 https://github.com/maniackcrudelis/modele_ynh/blob/master/scripts/.fonctions#L209 (15:00:40) Maniack C: Et j'utilise peu de helpers (15:01:11) Maniack C: Plus on encourage leur usage, plus on va créer des situations où ils seront conditionnés (15:02:29) Maniack C: On peut toujours les contourner bien sûr C'est le cas ici: https://github.com/YunoHost-Apps/nextcloud_ynh/pull/12/files#diff-44cb16c778719320333118c04d509a7cR17 (15:03:00) Maniack C: Mais on régresse par rapport à l'encouragement à l'usage des helpers (15:05:43) Maniack C: Josue-T pour son app monitorix je peux lui dire de ne pas utiliser sudo yunohost app checkport pour qu'elle fonctionne correctement. Mais ça me semble aller à l 'encontre de l'idée des helpers (15:07:44) Bram: ben c'est pas un problème de son app hein (15:07:53) Bram: c'est un problème au niveau CI/yunohost donc lui a rien a changer (15:08:27) Maniack C: Pour nextcloud j'ai changé mon code, car c'était simple à faire pour éviter ça (15:09:13) Bram: le problème c'est que ces commandes ont pas été pensés pour être utilisé dans ton cas (15:09:22) Maniack C: Je comprend bien (15:09:30) Bram: c'est des commandes qui sont pensés pour utilisateur alors que tu t'en sers comme des briques de programmation (15:09:36) Bram: ce qui est logique remarque vu les commandes (15:09:38) Maniack C: Mais de fait on les utilises de plus en plus comme ça (15:09:56) Bram: ça revient aussi à la volonté de jérôme de virer ça du code de yunohost (15:10:09) Maniack C: Alors en l'occurrence Josue-T à pris mon code, mais je suis pas le seul (15:10:59) Bram: j'ai le même problème dans mon code pour les settings globals (15:11:14) Bram: je sais pas comment retourner un code système 1/0 pour que ça soit utilisé programmatiquement (15:11:41) Maniack C: Là on l'a ce code de sortie sur ces commandes (15:11:55) Bram: ouais mais ce code est généré par le logger (15:12:01) Bram: qui fait les messages qui t'emmerdent (15:12:07) Maniack C: ah ok (15:12:09) Bram: https://github.com/YunoHost/yunohost/blob/unstable/src/yunohost/app.py#L882 (15:12:43) Bram: le problème c'est le mélange entre ces commandes utilisateurs (15:12:49) Bram: et des commandes pour de la programmation (15:12:55) Bram: jérôme a tenté de foutre ça dans des helpers (15:13:07) Bram: mais c'est un peu mettre un pansement sur le problème (15:13:16) Bram: faudrait idéalement un yunocode $stuff (15:13:25) Bram: qui soit autre chose que la commande yunohost (15:13:58) Bram: (d'ailleurs la commande check_port est méga bancale) (15:14:40) Maniack C: Donc en somme il ne faudrait pas utiliser la moulinette CLI pour les helpers? (15:14:55) Bram: idéalement oui (15:15:09) Maniack C: Difficile à justifier pour qui n'a pas le nez dedans (15:15:18) Bram: oui enfin (15:15:20) Bram: là je te dis "idéalement" (15:15:26) Bram: c'est un problème de design (15:15:30) Bram: on a pas de solution sous la main (15:15:45) Bram: donc aujourd'hui utiliser la moulinette dans les helpers est tout à fait raisonable (15:15:52) Bram: mais ça pose les problèmes que tu as (15:15:56) Maniack C: C'est que les commandes cli, yunohost ou autre, sont habituellement utiliser en shell (15:16:36) Maniack C: A mon avis, si tu dis à un nouvel utilisateur qu'il ne doit pas scripter des commandes Yunohost, il ne comprendra pas (15:16:54) Bram: ça sera un dev hein (15:16:57) Bram: pas un utilisateur (15:17:02) Maniack C: Certes oui (15:17:03) Bram: et il aura théoriquement un manuel avec les helpers (15:17:17) Bram: on devrait pas avoir de "check_port" dans les commandes yunohost par exemple (15:17:48) Maniack C: Mais je crois que beaucoup de helpers utilisent la moulinette en fin de compte (15:18:00) Bram: ben oui, c'est normal (15:18:01) Bram: y a que ça (15:18:50) Maniack C: Bon en somme, ma meilleur chance reste encore de réécrire un helper pour remplacer cette commande (15:18:53) Maniack C: Ou pire... (15:19:04) Maniack C: Un helper "hack dégueux" pour niquer le log... (15:19:21) Bram: à très court terme oui (15:19:27) Bram: à moyen terme il faudrait qu'on bosse sur ça (15:19:39) Bram: pour qu'il y a des opérations bas niveau disponible qui fassent ce dont tu as besoin (15:19:56) Maniack C: C'est si difficile en python de sortir le code d'erreur indépendemment? (15:19:59) Maniack C: (vrai question) (15:20:05) Bram: non, c'est trivial (15:20:13) Bram: sys.exit(code) (15:20:20) Maniack C: En effet (15:20:25) Bram: là c'est le design de la moulinette et de yunohost (pour la millième fois :p) (15:21:07) Maniack C: Tu pourrais me le dire 10000 fois, n'ayant pas le nez dedans j'en comprend pas la profondeur (15:21:16) Bram: oui je vois bien ça :p (15:21:16) Maniack C: J'imagine --- data/helpers.d/print | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/data/helpers.d/print b/data/helpers.d/print index 4cc8417db..36f4a120e 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -4,3 +4,16 @@ ynh_die() { echo "$1" 1>&2 exit "${2:-1}" } + +# Ignore the yunohost-cli log to prevent errors with conditionals commands +# usage: ynh_no_log COMMAND +# Simply duplicate the log, execute the yunohost command and replace the log without the result of this command +# It's a very badly hack... +ynh_no_log() { + ynh_cli_log=/var/log/yunohost/yunohost-cli.log + sudo cp -a ${ynh_cli_log} ${ynh_cli_log}-move + eval $@ + exit_code=$? + sudo mv ${ynh_cli_log}-move ${ynh_cli_log} + return $? +} From 905a04146bb8b958b3858920581a5e0046579ad4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 16 Jan 2017 09:24:41 +0100 Subject: [PATCH 0197/1066] [mod] start putting timeout in certificate code --- src/yunohost/certificate.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 49de94996..ccd1a810f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -37,6 +37,8 @@ import glob from OpenSSL import crypto from datetime import datetime +from requests.exceptions import Timeout + from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError @@ -567,7 +569,11 @@ def _fetch_and_enable_new_certificate(domain, staging=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) - intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text + try: + intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text + except Timeout: + # XXX what should we do here? retry? + pass # Now save the key and signed certificate logger.info("Saving the key and signed certificate...") @@ -837,7 +843,10 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - requests.head("http://" + ip, headers={"Host": domain}) + requests.head("http://" + ip, headers={"Host": domain}, timeout=30) + except Timeout as e: + # XXX what should we do here? retry? + pass except Exception as e: logger.debug("Couldn't reach domain '%s' by requesting this ip '%s' because: %s" % (domain, ip, e)) return False From 3e9d086f7ff64f923b2d623df41ec42c88c8a8ef Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 22 Jan 2017 20:01:27 +0100 Subject: [PATCH 0198/1066] Nouveau helper ynh_find_port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouveau helper pour trouver un port libre. Déclinaison du code classique avec une vérification préalable de l'argument. --- data/helpers.d/network | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 data/helpers.d/network diff --git a/data/helpers.d/network b/data/helpers.d/network new file mode 100644 index 000000000..080ac68e2 --- /dev/null +++ b/data/helpers.d/network @@ -0,0 +1,15 @@ +# Find a free port and return it +# +# example: port=$(ynh_find_port 8080) +# +# usage: ynh_find_port begin_port +# | arg: begin_port - port to start to search +ynh_find_port () { + port=$(echo "$1" | sed 's/[^0-9]//g') # Eliminate all non-digit characters + test -n "$port" || ynh_die "The argument of ynh_find_port must be a valid port." + while ! sudo yunohost app checkport $port --quiet # Check if the port is free + do + port=$((port+1)) # Else, pass to next port + done + echo $port +} From abb9f44b87cfed5fa14be9471b536fc27939d920 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 23 Jan 2017 00:48:56 +0100 Subject: [PATCH 0199/1066] Nouveaux helpers ynh_system_user_create et ynh_system_user_delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helpers pour créer un utilisateur système et pour le supprimer. La création d'user système permettra de créer des users dédiés aux applications. Cf YEP 3.4 --- data/helpers.d/user | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/data/helpers.d/user b/data/helpers.d/user index 5ee6acd68..0bb0736af 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -38,3 +38,34 @@ ynh_user_list() { ynh_system_user_exists() { getent passwd "$1" &>/dev/null } + +# Create a system user +# +# usage: ynh_system_user_create user_name [home_dir] +# | arg: user_name - Name of the system user that will be create +# | arg: home_dir - Path of the home dir for the user. Usually the final path of the app. If this argument is omitted, the user will be created without home +ynh_system_user_create () { + if ! ynh_system_user_exists "$1" # Check if the user exists on the system + then # If the user doesn't exist + if [ $# -ge 2 ]; then # If a home dir is mentioned + user_home_dir="-d $2" + else + user_home_dir="--no-create-home" + fi + sudo useradd $user_home_dir --system --user-group $1 --shell /usr/sbin/nologin || ynh_die "Unable to create $1 system account" + fi +} + +# Delete a system user +# +# usage: ynh_system_user_delete user_name +# | arg: user_name - Name of the system user that will be create +ynh_system_user_delete () { + if ynh_system_user_exists "$1" # Check if the user exists on the system + then + echo "Remove the user $1" >&2 + sudo userdel $1 + else + echo "The user $1 was not found" >&2 + fi +} From 3be7aff0655d38d4ceba2f9aa54666c1071769a4 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 23 Jan 2017 11:46:21 +0100 Subject: [PATCH 0200/1066] Revert "[enh] Compatibility with debian stretch" This reverts commit 73d1630459b3c0256007cf441df6046abe6889ee. --- debian/control | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index 357b2c766..47f0e61b2 100644 --- a/debian/control +++ b/debian/control @@ -16,12 +16,12 @@ Depends: ${python:Depends}, ${misc:Depends} , glances , dnsutils, bind9utils, unzip, git, curl, cron , ca-certificates, netcat-openbsd, iproute - , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd | php-mysql | php-mysqlnd + , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban - , nginx-extras (>=1.6.2), php5-fpm | php-fpm, php5-ldap | php-ldap, php5-intl | php-intl + , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl , dnsmasq, openssl, avahi-daemon , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools @@ -29,7 +29,7 @@ Depends: ${python:Depends}, ${misc:Depends} Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper - , php5-gd | php-gd, php5-curl | php-curl, php5-gettext | php-gettext, php5-mcrypt | php-mcrypt + , php5-gd, php5-curl, php-gettext, php5-mcrypt , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl From cd93427a97378ab635c85c0ae9a1e45132d6245c Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 23 Jan 2017 15:17:13 +0100 Subject: [PATCH 0201/1066] Retire la commande ynh --- data/helpers.d/network | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/network b/data/helpers.d/network index 080ac68e2..e06656820 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -7,7 +7,7 @@ ynh_find_port () { port=$(echo "$1" | sed 's/[^0-9]//g') # Eliminate all non-digit characters test -n "$port" || ynh_die "The argument of ynh_find_port must be a valid port." - while ! sudo yunohost app checkport $port --quiet # Check if the port is free + while netcat -z 127.0.0.1 $port # Check if the port is free do port=$((port+1)) # Else, pass to next port done From 901e3df9b604f542f2c460aad05bcc8efc9fd054 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 23 Jan 2017 15:18:52 +0100 Subject: [PATCH 0202/1066] Pas de correction de l'argument --- data/helpers.d/network | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/network b/data/helpers.d/network index e06656820..42616d513 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -5,7 +5,7 @@ # usage: ynh_find_port begin_port # | arg: begin_port - port to start to search ynh_find_port () { - port=$(echo "$1" | sed 's/[^0-9]//g') # Eliminate all non-digit characters + port=$1 test -n "$port" || ynh_die "The argument of ynh_find_port must be a valid port." while netcat -z 127.0.0.1 $port # Check if the port is free do From 656b2e61ba3ef1056bdb4154f86076188037b8bd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jan 2017 12:03:30 -0500 Subject: [PATCH 0203/1066] Implement timeout exceptions --- locales/en.json | 2 ++ src/yunohost/certificate.py | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index e145a1d38..cacac5d18 100644 --- a/locales/en.json +++ b/locales/en.json @@ -266,5 +266,7 @@ "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", + "certmanager_http_check_timeout" : "Timed out when server tried to contact itself through HTTP using public IP address. You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", + "certmanager_couldnt_fetch_intermediate_cert" : "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ccd1a810f..386894f65 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -572,8 +572,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text except Timeout: - # XXX what should we do here? retry? - pass + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) # Now save the key and signed certificate logger.info("Saving the key and signed certificate...") @@ -843,10 +842,10 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - requests.head("http://" + ip, headers={"Host": domain}, timeout=30) + requests.head("http://" + ip, headers={"Host": domain}, timeout=10) except Timeout as e: - # XXX what should we do here? retry? - pass + logger.warning(m18n.n('certmanager_http_check_timeout')) + return False except Exception as e: logger.debug("Couldn't reach domain '%s' by requesting this ip '%s' because: %s" % (domain, ip, e)) return False From cd88428248494fac1c22db4d53887fb5b25cc575 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jan 2017 18:38:30 -0500 Subject: [PATCH 0204/1066] Implementing opi's comments --- locales/en.json | 2 +- src/yunohost/certificate.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index cacac5d18..5e9d02e06 100644 --- a/locales/en.json +++ b/locales/en.json @@ -266,7 +266,7 @@ "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", - "certmanager_http_check_timeout" : "Timed out when server tried to contact itself through HTTP using public IP address. You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", + "certmanager_http_check_timeout" : "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", "certmanager_couldnt_fetch_intermediate_cert" : "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 386894f65..bd7d02962 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -571,7 +571,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text - except Timeout: + except Timeout as e: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) # Now save the key and signed certificate @@ -844,7 +844,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): try: requests.head("http://" + ip, headers={"Host": domain}, timeout=10) except Timeout as e: - logger.warning(m18n.n('certmanager_http_check_timeout')) + logger.warning(m18n.n('certmanager_http_check_timeout', domain=domain, ip=ip)) return False except Exception as e: logger.debug("Couldn't reach domain '%s' by requesting this ip '%s' because: %s" % (domain, ip, e)) From 4486b24388020e566b79688e0c0cce60edab3edf Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Tue, 31 Jan 2017 15:28:04 +0100 Subject: [PATCH 0205/1066] ynh_backup: Fix error message when source path doesn't exist --- data/helpers.d/filesystem | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 8a2bd9aff..856b7cb86 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -17,7 +17,7 @@ ynh_backup() { # validate arguments [[ -e "${SRCPATH}" ]] || { - echo "Source path '${DESTPATH}' does not exist" >&2 + echo "Source path '${SRCPATH}' does not exist" >&2 return 1 } From e8c916197903646821479131cce0898f6fea518a Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:00:15 +0100 Subject: [PATCH 0206/1066] Remove helper ynh_mkdir_tmp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le helper `ynh_mkdir_tmp` est inutile. Il existe la commande `mktemp -d` pour créer des dossiers temporaires. --- data/helpers.d/filesystem | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 856b7cb86..3517f5e44 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,15 +62,3 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } - -# Create a directory under /tmp -# -# usage: ynh_mkdir_tmp -# | ret: the created directory path -ynh_mkdir_tmp() { - TMPDIR="/tmp/$(ynh_string_random 6)" - while [ -d $TMPDIR ]; do - TMPDIR="/tmp/$(ynh_string_random 6)" - done - mkdir -p "$TMPDIR" && echo "$TMPDIR" -} From cfef34a39f05ff38a00b7d8c7a9f1a445c6e531b Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:19:19 +0100 Subject: [PATCH 0207/1066] Update filesystem --- data/helpers.d/filesystem | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 3517f5e44..9933563cc 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,3 +62,13 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } + +# Create a directory under /tmp +# +# Deprecated helper +# +# usage: ynh_mkdir_tmp +# | ret: the created directory path +ynh_mkdir_tmp() { + mktemp -d +} From 671fd047fb4317b01723d57a3fa673d9916cac64 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 29 Nov 2016 15:27:51 +0100 Subject: [PATCH 0208/1066] [enh] Add warning about deprecated ynh_mkdir_tmp helper --- data/helpers.d/filesystem | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 9933563cc..27a016e63 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -70,5 +70,6 @@ ynh_bind_or_cp() { # usage: ynh_mkdir_tmp # | ret: the created directory path ynh_mkdir_tmp() { + echo "This helper is deprecated, you should use 'mktemp -d' instead." >&2 mktemp -d } From d1199b62ab11174f94f50d87c620356d36a17262 Mon Sep 17 00:00:00 2001 From: Juanu Date: Tue, 13 Dec 2016 15:01:21 +0100 Subject: [PATCH 0209/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) --- locales/es.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/locales/es.json b/locales/es.json index 093fb17fa..9a7e4314e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,12 +1,12 @@ { - "action_invalid": "Acción no válida '{action:s}'", + "action_invalid": "Acción no válida '{action:s} 1'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se pudo cambiar la contraseña", + "admin_password_change_failed": "No se puede cambiar la contraseña", "admin_password_changed": "La contraseña administrativa ha sido cambiada", - "app_already_installed": "{app:s} ya está instalada", - "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s}', deber una de {choices:s}", - "app_argument_invalid": "Valor no válido para el argumento '{name:s}': {error:s}", - "app_argument_required": "Se requiere el argumento '{name:s}'", + "app_already_installed": "{app:s} 2 ya está instalada", + "app_argument_choice_invalid": "Opción no válida para el argumento '{name:s} 3', deber una de {choices:s} 4", + "app_argument_invalid": "Valor no válido para el argumento '{name:s} 5': {error:s} 6", + "app_argument_required": "Se requiere el argumento '{name:s} 7'", "app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_id_invalid": "Id de la aplicación no válida", "app_incompatible": "La aplicación no es compatible con su versión de YunoHost", @@ -15,9 +15,9 @@ "app_location_install_failed": "No se puede instalar la aplicación en esta localización", "app_manifest_invalid": "El manifiesto de la aplicación no es válido", "app_no_upgrade": "No hay aplicaciones para actualizar", - "app_not_correctly_installed": "La aplicación {app:s} parece estar incorrectamente instalada", - "app_not_installed": "{app:s} no está instalada", - "app_not_properly_removed": "La {app:s} no ha sido desinstalada correctamente", + "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", + "app_not_installed": "{app:s} 9 no está instalada", + "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente", "app_package_need_update": "Es necesario actualizar el paquete de la aplicación debido a los cambios en YunoHost", "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", "app_removed": "{app:s} ha sido eliminada", @@ -71,7 +71,7 @@ "diagnostic_monitor_network_error": "No se puede monitorizar la red: {error}", "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", - "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", + "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecute 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", @@ -84,11 +84,11 @@ "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminarlo.", "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zona del DNS ya existe", - "domain_zone_not_found": "No se ha encontrado el archivo de zona DNS para el dominio [:s]", + "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]", "done": "Hecho.", "downloading": "Descargando...", "dyndns_cron_installed": "La tarea cron para DynDNS ha sido instalada", - "dyndns_cron_remove_failed": "No se pudo eliminar la tarea del cron DynDNS", + "dyndns_cron_remove_failed": "No se pudo eliminar la tarea cron DynDNS", "dyndns_cron_removed": "La tarea cron DynDNS ha sido eliminada", "dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en el DynDNS", "dyndns_ip_updated": "Su dirección IP ha sido actualizada en el DynDNS", @@ -193,13 +193,13 @@ "service_conf_file_updated": "El archivo de configuración '{conf}' ha sido actualizado", "service_conf_up_to_date": "La configuración del servicio '{service}' ya está actualizada", "service_conf_updated": "La configuración ha sido actualizada para el servicio '{service}'", - "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service}'", + "service_conf_would_be_updated": "La configuración podría haber sido actualizada para el servicio '{service} 1'", "service_disable_failed": "No se pudo deshabilitar el servicio '{service:s}'", "service_disabled": "El servicio '{service:s}' ha sido deshabilitado", "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_no_log": "No hay ningún registro para el servicio '{service:s}'", - "service_regenconf_dry_pending_applying": "Comprobando configuración que podría haber sido aplicada al servicio '{service}'...", + "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service} 1'...", "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}", "service_regenconf_pending_applying": "Aplicando la configuración para el servicio '{service}'...", "service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'", @@ -242,13 +242,13 @@ "yunohost_installing": "Instalando YunoHost...", "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", - "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón", + "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón de correo", "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s} 1! (Use --force para omitir este mensaje)", "certmanager_domain_unknown": "Dominio desconocido {domain:s} 1", - "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Estás seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", - "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para {domain:s} 1 falla de alguna manera...", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} 1 ha fallado de alguna manera...", "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} 1 no está emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} 1 no está a punto de expirar! Utilice --force para omitir este mensaje", "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} 1 a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", @@ -258,7 +258,7 @@ "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s} 1!", "certmanager_cert_install_success": "Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s} 1!", "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar certificados para todos los dominios con un certificado Let's Encrypt o un certificado autofirmado", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s} 1. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} 1 (archivo: {file:s} 2)", From e6b10f2af1c17a02f240b214b50b25e86c19ba58 Mon Sep 17 00:00:00 2001 From: Juanu Date: Tue, 13 Dec 2016 15:53:25 +0100 Subject: [PATCH 0210/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (264 of 264 strings) --- locales/es.json | 68 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/locales/es.json b/locales/es.json index 9a7e4314e..b3dd4f14e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -113,16 +113,16 @@ "hook_list_by_invalid": "Propiedad no válida para listar por hook", "hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "installation_complete": "Instalación finalizada", - "installation_failed": "No pudo realizar la instalación", + "installation_failed": "No se pudo realizar la instalación", "ip6tables_unavailable": "No puede modificar ip6tables aquí. O bien está en un 'container' o su kernel no soporta esta opción", "iptables_unavailable": "No puede modificar iptables aquí. O bien está en un 'container' o su kernel no soporta esta opción", - "ldap_initialized": "LDAP iniciado", + "ldap_initialized": "Se ha inicializado LDAP", "license_undefined": "indefinido", "mail_alias_remove_failed": "No se pudo eliminar el alias de correo '{mail:s}'", "mail_domain_unknown": "El dominio de correo '{domain:s}' es desconocido", "mail_forward_remove_failed": "No se pudo eliminar el reenvío de correo '{mail:s}'", "maindomain_change_failed": "No se pudo cambiar el dominio principal", - "maindomain_changed": "El dominio principal ha sido cambiado", + "maindomain_changed": "Se ha cambiado el dominio principal", "monitor_disabled": "La monitorización del sistema ha sido deshabilitada", "monitor_enabled": "La monitorización del sistema ha sido habilitada", "monitor_glances_con_failed": "No se pudo conectar al servidor Glances", @@ -130,13 +130,13 @@ "monitor_period_invalid": "Período de tiempo no válido", "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticas", "monitor_stats_no_update": "No hay estadísticas de monitorización para actualizar", - "monitor_stats_period_unavailable": "No hay estadísticas para ese período", + "monitor_stats_period_unavailable": "No hay estadísticas para el período", "mountpoint_unknown": "Punto de montaje desconocido", "mysql_db_creation_failed": "No se pudo crear la base de datos MySQL", "mysql_db_init_failed": "No se pudo iniciar la base de datos MySQL", - "mysql_db_initialized": "La base de datos MySQL ha sido iniciada", + "mysql_db_initialized": "La base de datos MySQL ha sido inicializada", "network_check_mx_ko": "El registro DNS MX no está configurado", - "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado en su red", + "network_check_smtp_ko": "El puerto 25 (SMTP) para el correo saliente parece estar bloqueado por su red", "network_check_smtp_ok": "El puerto de salida del correo electrónico (25, SMTP) no está bloqueado", "new_domain_required": "Debe proporcionar el nuevo dominio principal", "no_appslist_found": "No se ha encontrado ninguna lista de aplicaciones", @@ -145,13 +145,13 @@ "no_restore_script": "No se ha encontrado un script de restauración para la aplicación '{app:s}'", "not_enough_disk_space": "No hay suficiente espacio en '{path:s}'", "package_not_installed": "El paquete '{pkgname}' no está instalado", - "package_unexpected_error": "Un error inesperado procesando el paquete '{pkgname}'", + "package_unexpected_error": "Ha ocurrido un error inesperado procesando el paquete '{pkgname}'", "package_unknown": "Paquete desconocido '{pkgname}'", - "packages_no_upgrade": "No hay paquetes que actualizar", + "packages_no_upgrade": "No hay paquetes para actualizar", "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", - "path_removal_failed": "No se pudo borrar la ruta {:s}", - "pattern_backup_archive_name": "Debe que ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", + "path_removal_failed": "No se pudo eliminar la ruta {:s}", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida (por ejemplo, alguien@dominio.org)", "pattern_firstname": "Debe ser un nombre válido", @@ -180,7 +180,7 @@ "restore_running_hooks": "Ejecutando los hooks de restauración...", "service_add_failed": "No se pudo añadir el servicio '{service:s}'", "service_added": "Servicio '{service:s}' ha sido añadido", - "service_already_started": "El servicio '{service:s}' ya ha sido iniciado", + "service_already_started": "El servicio '{service:s}' ya ha sido inicializado", "service_already_stopped": "El servicio '{service:s}' ya ha sido detenido", "service_cmd_exec_failed": "No se pudo ejecutar el comando '{command:s}'", "service_conf_file_backed_up": "Se ha realizado una copia de seguridad del archivo de configuración '{conf}' en '{backup}'", @@ -199,9 +199,9 @@ "service_enable_failed": "No se pudo habilitar el servicio '{service:s}'", "service_enabled": "El servicio '{service:s}' ha sido habilitado", "service_no_log": "No hay ningún registro para el servicio '{service:s}'", - "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service} 1'...", + "service_regenconf_dry_pending_applying": "Comprobando configuración pendiente que podría haber sido aplicada al servicio '{service}'...", "service_regenconf_failed": "No se puede regenerar la configuración para el servicio(s): {services}", - "service_regenconf_pending_applying": "Aplicando la configuración para el servicio '{service}'...", + "service_regenconf_pending_applying": "Aplicando la configuración pendiente para el servicio '{service}'...", "service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'", "service_removed": "El servicio '{service:s}' ha sido desinstalado", "service_start_failed": "No se pudo iniciar el servicio '{service:s}'", @@ -219,7 +219,7 @@ "unit_unknown": "Unidad desconocida '{unit:s}'", "unlimit": "Sin cuota", "unrestore_app": "La aplicación '{app:s}' no será restaurada", - "update_cache_failed": "No se pudo actualizar la caché APT", + "update_cache_failed": "No se pudo actualizar la caché de APT", "updating_apt_cache": "Actualizando lista de paquetes disponibles...", "upgrade_complete": "Actualización finalizada", "upgrading_packages": "Actualizando paquetes...", @@ -243,27 +243,27 @@ "yunohost_not_installed": "YunoHost no está instalado o ha habido errores en la instalación. Ejecute 'yunohost tools postinstall'", "ldap_init_failed_to_create_admin": "La inicialización de LDAP falló al crear el usuario administrador", "mailbox_used_space_dovecot_down": "El servicio de e-mail Dovecot debe estar funcionando si desea obtener el espacio utilizado por el buzón de correo", - "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", - "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s} 1. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", - "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s} 1! (Use --force para omitir este mensaje)", - "certmanager_domain_unknown": "Dominio desconocido {domain:s} 1", - "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} 1 no es autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", - "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} 1 ha fallado de alguna manera...", - "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} 1 no está emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", - "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} 1 no está a punto de expirar! Utilice --force para omitir este mensaje", - "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} 1 a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", - "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s} 1. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} 1 es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", - "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} 1 (archivo: {file:s} 2), razón: {reason:s} 3", - "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s} 1!", - "certmanager_cert_install_success": "Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s} 1!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada y esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", - "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s} 1. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", + "ssowat_persistent_conf_read_error": "Error al leer la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "ssowat_persistent_conf_write_error": "Error al guardar la configuración persistente de SSOwat: {error:s}. Edite el archivo /etc/ssowat/conf.json.persistent para corregir la sintaxis de JSON", + "certmanager_attempt_to_replace_valid_cert": "Está intentando sobrescribir un certificado correcto y válido para el dominio {domain:s}! (Use --force para omitir este mensaje)", + "certmanager_domain_unknown": "Dominio desconocido {domain:s}", + "certmanager_domain_cert_not_selfsigned": "El certificado para el dominio {domain:s} no es un certificado autofirmado. ¿Está seguro de que quiere reemplazarlo? (Use --force para omitir este mensaje)", + "certmanager_certificate_fetching_or_enabling_failed": "Parece que al habilitar el nuevo certificado para el dominio {domain:s} ha fallado de alguna manera...", + "certmanager_attempt_to_renew_nonLE_cert": "El certificado para el dominio {domain:s} no ha sido emitido por Let's Encrypt. ¡No se puede renovar automáticamente!", + "certmanager_attempt_to_renew_valid_cert": "El certificado para el dominio {domain:s} no está a punto de expirar! Utilice --force para omitir este mensaje", + "certmanager_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Compruebe que la configuración del DNS y de nginx es correcta", + "certmanager_error_no_A_record": "No se ha encontrado un registro DNS 'A' para el dominio {domain:s}. Debe hacer que su nombre de dominio apunte a su máquina para poder instalar un certificado Let's Encrypt. (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_domain_dns_ip_differs_from_public_ip": "El registro DNS 'A' para el dominio {domain:s} es diferente de la IP de este servidor. Si recientemente modificó su registro A, espere a que se propague (existen algunos controladores de propagación DNS disponibles en línea). (Si sabe lo que está haciendo, use --no-checks para desactivar esas comprobaciones.)", + "certmanager_cannot_read_cert": "Se ha producido un error al intentar abrir el certificado actual para el dominio {domain:s} (archivo: {file:s}), razón: {reason:s}", + "certmanager_cert_install_success_selfsigned": "¡Se ha instalado correctamente un certificado autofirmado para el dominio {domain:s}!", + "certmanager_cert_install_success": "¡Se ha instalado correctamente un certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_cert_renew_success": "¡Se ha renovado correctamente el certificado Let's Encrypt para el dominio {domain:s}!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost ha detectado que la aplicación 'letsencrypt' está instalada, esto produce conflictos con las nuevas funciones de administración de certificados integradas en Yunohost. Si desea utilizar las nuevas funciones integradas, ejecute los siguientes comandos para migrar su instalación:\n\n Yunohost app remove letsencrypt\n Yunohost domain cert-install\n\nP.D.: esto intentará reinstalar los certificados para todos los dominios con un certificado Let's Encrypt o con un certificado autofirmado", + "certmanager_hit_rate_limit": "Se han emitido demasiados certificados recientemente para el conjunto de dominios {domain:s}. Por favor, inténtelo de nuevo más tarde. Consulte https://letsencrypt.org/docs/rate-limits/ para obtener más detalles", "certmanager_cert_signing_failed": "No se pudo firmar el nuevo certificado", - "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} 1 (archivo: {file:s} 2)", - "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} 1 está en conflicto y debe ser eliminado primero", + "certmanager_no_cert_file": "No se puede leer el certificado para el dominio {domain:s} (archivo: {file:s})", + "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} está en conflicto y debe ser eliminado primero", "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", - "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s} 1)", + "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)" } From 4f6a11d31d6cc4c2b3a0f758f1d1ede576111ae2 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 13 Dec 2016 07:20:51 +0100 Subject: [PATCH 0211/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (265 of 265 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 35749030c..cbd619c38 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -273,5 +273,6 @@ "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", - "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie" + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", + "domains_available": "Domaines disponibles :" } From 60d8ecd761b252b19744bc9c51f8d6d977036bb4 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Thu, 15 Dec 2016 22:10:52 +0100 Subject: [PATCH 0212/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (266 of 266 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index cbd619c38..af935f1ea 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -274,5 +274,6 @@ "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", - "domains_available": "Domaines disponibles :" + "domains_available": "Domaines disponibles :", + "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})" } From 18bdcf2c5f47cca5bbe3d2f56cdb338bc2c3b049 Mon Sep 17 00:00:00 2001 From: paddy Date: Sat, 24 Dec 2016 13:47:32 +0100 Subject: [PATCH 0213/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 99.6% (265 of 266 strings) --- locales/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index b3dd4f14e..c5ead2908 100644 --- a/locales/es.json +++ b/locales/es.json @@ -265,5 +265,6 @@ "certmanager_conflicting_nginx_file": "No se puede preparar el dominio para el desafío ACME: el archivo de configuración nginx {filepath:s} está en conflicto y debe ser eliminado primero", "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", - "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)" + "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)", + "domains_available": "Dominios instalados:" } From 0633025224a9bc59f4f3f32bbaa489ecded5a776 Mon Sep 17 00:00:00 2001 From: paddy Date: Sat, 24 Dec 2016 13:39:26 +0100 Subject: [PATCH 0214/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (267 of 267 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index af935f1ea..ee9b17d40 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -275,5 +275,6 @@ "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", - "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})" + "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", + "certmanager_acme_not_configured_for_domain": "266 (EN vers FR) :\nLe certificat pour le domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine." } From 3b0da3651873b78a2ae421864d6f364f46531393 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 3 Jan 2017 19:29:48 +0100 Subject: [PATCH 0215/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (267 of 267 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index ee9b17d40..c32ce2f2f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -276,5 +276,5 @@ "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", - "certmanager_acme_not_configured_for_domain": "266 (EN vers FR) :\nLe certificat pour le domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine." + "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine." } From 776562a6ef318ac022bf19a3ff6a5b818bc686b5 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 3 Jan 2017 19:29:48 +0100 Subject: [PATCH 0216/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (268 of 268 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index c32ce2f2f..6f0908838 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -276,5 +276,6 @@ "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", - "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine." + "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", + "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être résolu depuis votre serveur Yunohost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d'attendre quelques heures qu'il se propage. Si le problème persiste, envisager d'ajouter {domain:s} au fichier /etc/hosts (Si vous savez ce que vou faites, utilisez --no-checks pour désactiver ces vérifications.)" } From 18364aee7423a0867837af64940166ea5d1137c5 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Mon, 9 Jan 2017 23:18:22 +0100 Subject: [PATCH 0217/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (268 of 268 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 6f0908838..f89ac7482 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -277,5 +277,5 @@ "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", - "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être résolu depuis votre serveur Yunohost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d'attendre quelques heures qu'il se propage. Si le problème persiste, envisager d'ajouter {domain:s} au fichier /etc/hosts (Si vous savez ce que vou faites, utilisez --no-checks pour désactiver ces vérifications.)" + "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)" } From 28ec1dec8fcb46c9f79586b985ec60d1c3b58b1e Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:00:15 +0100 Subject: [PATCH 0218/1066] Remove helper ynh_mkdir_tmp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le helper `ynh_mkdir_tmp` est inutile. Il existe la commande `mktemp -d` pour créer des dossiers temporaires. --- data/helpers.d/filesystem | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 856b7cb86..3517f5e44 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,15 +62,3 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } - -# Create a directory under /tmp -# -# usage: ynh_mkdir_tmp -# | ret: the created directory path -ynh_mkdir_tmp() { - TMPDIR="/tmp/$(ynh_string_random 6)" - while [ -d $TMPDIR ]; do - TMPDIR="/tmp/$(ynh_string_random 6)" - done - mkdir -p "$TMPDIR" && echo "$TMPDIR" -} From 65d412055d7e9848c9083e06a762eb8ecd021143 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 29 Nov 2016 14:19:19 +0100 Subject: [PATCH 0219/1066] Update filesystem --- data/helpers.d/filesystem | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 3517f5e44..9933563cc 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -62,3 +62,13 @@ ynh_bind_or_cp() { echo "This helper is deprecated, you should use ynh_backup instead" >&2 ynh_backup "$1" "$2" 1 "$NO_ROOT" } + +# Create a directory under /tmp +# +# Deprecated helper +# +# usage: ynh_mkdir_tmp +# | ret: the created directory path +ynh_mkdir_tmp() { + mktemp -d +} From 8e811fd0ac1e79d6d09d14eb576826d655d3099a Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 29 Nov 2016 15:27:51 +0100 Subject: [PATCH 0220/1066] [enh] Add warning about deprecated ynh_mkdir_tmp helper --- data/helpers.d/filesystem | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 9933563cc..27a016e63 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -70,5 +70,6 @@ ynh_bind_or_cp() { # usage: ynh_mkdir_tmp # | ret: the created directory path ynh_mkdir_tmp() { + echo "This helper is deprecated, you should use 'mktemp -d' instead." >&2 mktemp -d } From c89d1e25d2bf1e989f3c77d2986724092c4aff11 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Wed, 1 Feb 2017 22:16:53 +0100 Subject: [PATCH 0221/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (270 of 270 strings) --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f89ac7482..6f1369b7a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -277,5 +277,7 @@ "domains_available": "Domaines disponibles :", "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", - "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)" + "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", + "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", + "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement." } From 4daaba82d67388960336b5b2554e8f5978fffdb9 Mon Sep 17 00:00:00 2001 From: opi Date: Thu, 2 Feb 2017 11:26:09 +0100 Subject: [PATCH 0222/1066] Update changelog for 2.5.4 release --- debian/changelog | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2a719fc83..e86ca03a8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,35 @@ +yunohost (2.5.4) stable; urgency=low + + [ Maniack Crudelis ] + * Remove helper ynh_mkdir_tmp + * Update filesystem + + [ opi ] + * [enh] Add warning about deprecated ynh_mkdir_tmp helper + * [enh] Increase fail2ban maxretry on user login, narrow nginx log files + + [ Juanu ] + * [i18n] Translated using Weblate (Spanish) + + [ Jean-Baptiste Holcroft ] + * [i18n] Translated using Weblate (French) + + [ Laurent Peuch ] + * [mod] start putting timeout in certificate code + + [ Alexandre Aubin ] + * Implement timeout exceptions + * Implementing opi's comments + + [ JimboJoe ] + * ynh_backup: Fix error message when source path doesn't exist + + [ paddy ] + * [i18n] Translated using Weblate (Spanish) + * [i18n] Translated using Weblate (French) + + -- opi Thu, 02 Feb 2017 11:24:55 +0100 + yunohost (2.5.3.1) testing; urgency=low * super quickfix release for a typo that break LE certificates From 7d4aa63c430516f815a8cdfd2f517f79565efe2f Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 5 Feb 2017 14:23:55 +0100 Subject: [PATCH 0223/1066] [fix] Rspamd & Rmilter are no more sockets Changes introduced by https://github.com/YunoHost/yunohost/commit/b419c8c5641f29284befc29b059ee37db0b74a59 --- data/templates/yunohost/services.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index b4e63479b..350faae37 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -14,10 +14,10 @@ postfix: status: service log: [/var/log/mail.log,/var/log/mail.err] rmilter: - status: systemctl status rmilter.socket + status: systemctl status rmilter.service log: /var/log/mail.log rspamd: - status: systemctl status rspamd.socket + status: systemctl status rspamd.service log: /var/log/mail.log redis-server: status: service From 98d88f2364eda28ddc6b98d45a7fbe2bbbaba3d4 Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 5 Feb 2017 22:28:43 +0100 Subject: [PATCH 0224/1066] [fix] Unattended upgrades configuration syntax. --- data/templates/unattended/02periodic | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/templates/unattended/02periodic b/data/templates/unattended/02periodic index c3ec92913..f16105466 100644 --- a/data/templates/unattended/02periodic +++ b/data/templates/unattended/02periodic @@ -1,5 +1,4 @@ -02periodic 50unattended-upgrades -root@65ba01d0c078:/usr/share/yunohost/yunohost-config/unattended# cat 02periodic +# https://wiki.debian.org/UnattendedUpgrades#automatic_call_via_.2Fetc.2Fapt.2Fapt.conf.d.2F02periodic APT::Periodic::Enable "1"; APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; From 5b006dbf0e074f4070f6832d2c64f3b306935e3f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2017 08:37:41 -0500 Subject: [PATCH 0225/1066] Adding info/debug message for fetchlist --- src/yunohost/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 211009a04..d5874d1e4 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -101,6 +101,7 @@ def app_fetchlist(url=None, name=None): m18n.n('custom_appslist_name_required')) try: + logger.info("Fetching app list '%s' from %s ...", name, url) urlretrieve(url, '%s/%s.json' % (repo_path, name)) except Exception as e: raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error'), error=str(e)) From 730156dd92bbd1b0c479821ffc829e8d4f3d2019 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 6 Feb 2017 12:54:32 -0500 Subject: [PATCH 0226/1066] Using request insteqd of urlretrieve, to have timeout --- src/yunohost/app.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d5874d1e4..3f2351318 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -35,8 +35,8 @@ import socket import urlparse import errno import subprocess +import requests from collections import OrderedDict -from urllib import urlretrieve from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -100,12 +100,18 @@ def app_fetchlist(url=None, name=None): raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) + # Download file try: - logger.info("Fetching app list '%s' from %s ...", name, url) - urlretrieve(url, '%s/%s.json' % (repo_path, name)) + applist = requests.get(url, timeout=30).text except Exception as e: - raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error'), error=str(e)) + raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error', error=str(e))) + # Write app list to file + list_file = '%s/%s.json' % (repo_path, name) + with open(list_file, "w") as f: + f.write(applist) + + # Setup a cron job to re-fetch the list at midnight open("/etc/cron.d/yunohost-applist-%s" % name, "w").write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) logger.success(m18n.n('appslist_fetched')) From a61445c9c3d231b9248fd247a0dd3345fc0ac6df Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2017 08:18:52 -0500 Subject: [PATCH 0227/1066] Checking for 404 error and valid json format --- locales/en.json | 1 + src/yunohost/app.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6d5dfc628..0880fdddd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -31,6 +31,7 @@ "appslist_fetched": "The app list has been fetched", "appslist_removed": "The app list has been removed", "appslist_retrieve_error": "Unable to retrieve the remote app list: {error}", + "appslist_retrieve_bad_format": "Retrieved file is not a valid app list", "appslist_unknown": "Unknown app list", "ask_current_admin_password": "Current administration password", "ask_email": "Email address", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f1c0bae20..5499f4b02 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -100,10 +100,22 @@ def app_fetchlist(url=None, name=None): # Download file try: - applist = requests.get(url, timeout=30).text + applist_request = requests.get(url, timeout=30) except Exception as e: raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error', error=str(e))) - + + if (applist_request.status_code != 200): + raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error', error="404, not found")) + + # Validate app list format + # TODO / Possible improvement : better validation for app list (check that + # json fields actually look like an app list and not any json file) + applist = applist_request.text + try: + json.loads(applist) + except ValueError, e: + raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_bad_format')) + # Write app list to file list_file = '%s/%s.json' % (repo_path, name) with open(list_file, "w") as f: From d98b5e036c582c1f9040269c1fae70dffc9971ea Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 8 Feb 2017 19:23:27 +0100 Subject: [PATCH 0228/1066] [enh] New helpers for equivs use - `ynh_package_remove` and `ynh_package_autoremove` were just moved up. - Small modifications on `ynh_package_install_from_equivs`. Just added some comments and used popd instead of cd. - Added `ynh_app_dependencies` to manage easily installation of dependencies with equivs - And `ynh_remove_app_dependencies` to remove them. --- data/helpers.d/package | 111 +++++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 6dd42ff65..71aabdb67 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -47,44 +47,6 @@ ynh_package_install() { -o Dpkg::Options::=--force-confold install $@ } -# Build and install a package from an equivs control file -# -# example: generate an empty control file with `equivs-control`, adjust its -# content and use helper to build and install the package: -# ynh_package_install_from_equivs /path/to/controlfile -# -# usage: ynh_package_install_from_equivs controlfile -# | arg: controlfile - path of the equivs control file -ynh_package_install_from_equivs() { - controlfile=$1 - - # install equivs package as needed - ynh_package_is_installed 'equivs' \ - || ynh_package_install equivs - - # retrieve package information - pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) - pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) - [[ -z "$pkgname" || -z "$pkgversion" ]] \ - && echo "Invalid control file" && exit 1 - - # update packages cache - ynh_package_update - - # build and install the package - TMPDIR=$(ynh_mkdir_tmp) - (cp "$controlfile" "${TMPDIR}/control" \ - && cd "$TMPDIR" \ - && equivs-build ./control 1>/dev/null \ - && sudo dpkg --force-depends \ - -i "./${pkgname}_${pkgversion}_all.deb" 2>&1 \ - && ynh_package_install -f) \ - && ([[ -n "$TMPDIR" ]] && rm -rf $TMPDIR) - - # check if the package is actually installed - ynh_package_is_installed "$pkgname" -} - # Remove package(s) # # usage: ynh_package_remove name [name [...]] @@ -100,3 +62,76 @@ ynh_package_remove() { ynh_package_autoremove() { ynh_apt autoremove $@ } + +# Build and install a package from an equivs control file +# +# example: generate an empty control file with `equivs-control`, adjust its +# content and use helper to build and install the package: +# ynh_package_install_from_equivs /path/to/controlfile +# +# usage: ynh_package_install_from_equivs controlfile +# | arg: controlfile - path of the equivs control file +ynh_package_install_from_equivs () { + controlfile=$1 + + # Check if the equivs package is installed. Or install it. + ynh_package_is_installed 'equivs' \ + || ynh_package_install equivs + + # retrieve package information + pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) # Retrieve the name of the debian package + pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) # And its version number + [[ -z "$pkgname" || -z "$pkgversion" ]] \ + && echo "Invalid control file" && exit 1 # Check if this 2 variables aren't empty. + + # Update packages cache + ynh_package_update + + # Build and install the package + TMPDIR=$(ynh_mkdir_tmp) + cp "$controlfile" "${TMPDIR}/control" && pushd "$TMPDIR" # pushd is like a cd, but it stores the previous directory + # Create a fake deb package with equivs-build and the given control file + # Install the fake package without its dependencies with dpkg + # Install missing dependencies with ynh_package_install + equivs-build ./control 1>/dev/null \ + && sudo dpkg --force-depends \ + -i "./${pkgname}_${pkgversion}_all.deb" 2>&1 \ + && ynh_package_install -f + [[ -n "$TMPDIR" ]] && rm -rf $TMPDIR # Remove the temp dir. + + # check if the package is actually installed + ynh_package_is_installed "$pkgname" + popd # Like a cd on the directory stored by pushd +} + +# Install dependencies with a equivs control file +# +# usage: ynh_app_dependencies dep [dep [...]] +# | arg: dep - the package name to install in dependence +ynh_app_dependencies () { + dependencies=$1 + version=$(sudo python3 -c "import sys, json;print(json.load(open('../manifest.json'))['version'])") # Retrieve the version number in the manifest file. + dep_app=${app/_/-} # Replace all '_' by '-' + cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build +Section: misc +Priority: optional +Package: ${dep_app}-ynh-deps +Version: ${version} +Depends: ${dependencies} +Architecture: all +Description: Fake package for ${app} (YunoHost app) dependencies + This meta-package is only responsible of installing its dependencies. +EOF + ynh_package_install_from_equivs ./${dep_app}-ynh-deps.control \ + || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies +} + +# Remove fake package and its dependencies +# +# Dependencies will removed only if no other package need them. +# +# usage: ynh_remove_app_dependencies +ynh_remove_app_dependencies () { + dep_app=${app/_/-} # Replace all '_' by '-' + ynh_package_autoremove ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. +} From 12b223b2f55edfafa39e4ce2e98ae64e07ed308a Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 9 Feb 2017 00:02:47 +0100 Subject: [PATCH 0229/1066] [enh] New helpers for logrotate Add and remove logrotate config --- data/helpers.d/backend | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 data/helpers.d/backend diff --git a/data/helpers.d/backend b/data/helpers.d/backend new file mode 100644 index 000000000..efc180d90 --- /dev/null +++ b/data/helpers.d/backend @@ -0,0 +1,53 @@ +# Use logrotate to manage the logfile +# +# usage: ynh_use_logrotate [logfile] +# | arg: logfile - absolute path of logfile +# +# If no argument provided, a standard directory will be use. /var/log/${app} +# You can provide a path with the directory only or with the logfile. +# /parentdir/logdir/ +# /parentdir/logdir/logfile.log +# +# It's possible to use this helper several times, each config will added to same logrotate config file. +ynh_use_logrotate () { + if [ -n "$1" ]; then + if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile + logfile=$1 # In this case, focus logrotate on the logfile + else + logfile=$1/.log # Else, uses the directory and all logfile into it. + fi + else + logfile="/var/log/${app}/.log" # Without argument, use a defaut directory in /var/log + fi + cat > ./${app}-logrotate << EOF # Build a config file for logrotate +$logfile { + # Rotate if the logfile exceeds 100Mo + size 100M + # Keep 12 old log maximum + rotate 12 + # Compress the logs with gzip + compress + # Compress the log at the next cycle. So keep always 2 non compressed logs + delaycompress + # Copy and truncate the log to allow to continue write on it. Instead of move the log. + copytruncate + # Do not do an error if the log is missing + missingok + # Not rotate if the log is empty + notifempty + # Keep old logs in the same dir + noolddir +} +EOF + sudo mkdir -p $(dirname "$logfile") # Create the log directory, if not exist + cat ${app}-logrotate | sudo tee -a /etc/logrotate.d/$app > /dev/null # Append this config to the others for this app. If a config file already exist +} + +# Remove the app's logrotate config. +# +# usage: ynh_remove_logrotate +ynh_remove_logrotate () { + if [ -e "/etc/logrotate.d/$app" ]; then + sudo rm "/etc/logrotate.d/$app" + fi +} From 0989e27d584c394b9dfa92928ebced0642aef81d Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 9 Feb 2017 17:22:24 +0100 Subject: [PATCH 0230/1066] Update package --- data/helpers.d/package | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 71aabdb67..3b29a1f3d 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -109,8 +109,12 @@ ynh_package_install_from_equivs () { # usage: ynh_app_dependencies dep [dep [...]] # | arg: dep - the package name to install in dependence ynh_app_dependencies () { - dependencies=$1 - version=$(sudo python3 -c "import sys, json;print(json.load(open('../manifest.json'))['version'])") # Retrieve the version number in the manifest file. + dependencies=$@ + manifest_path="../manifest.json" + if [ ! -e "$manifest_path" ]; then + manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place + fi + version=$(sudo python3 -c "import sys, json;print(json.load(open(\"$manifest_path\"))['version'])") # Retrieve the version number in the manifest file. dep_app=${app/_/-} # Replace all '_' by '-' cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc From b062c014b0dfb0cf89a5a11ab051c456a564b414 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 9 Feb 2017 18:35:18 +0100 Subject: [PATCH 0231/1066] Restore use of subshell --- data/helpers.d/package | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 3b29a1f3d..9cafd3970 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -89,19 +89,19 @@ ynh_package_install_from_equivs () { # Build and install the package TMPDIR=$(ynh_mkdir_tmp) - cp "$controlfile" "${TMPDIR}/control" && pushd "$TMPDIR" # pushd is like a cd, but it stores the previous directory + # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg # Install missing dependencies with ynh_package_install - equivs-build ./control 1>/dev/null \ + (cp "$controlfile" "${TMPDIR}/control" && cd "$TMPDIR" \ + && equivs-build ./control 1>/dev/null \ && sudo dpkg --force-depends \ -i "./${pkgname}_${pkgversion}_all.deb" 2>&1 \ - && ynh_package_install -f + && ynh_package_install -f) [[ -n "$TMPDIR" ]] && rm -rf $TMPDIR # Remove the temp dir. # check if the package is actually installed ynh_package_is_installed "$pkgname" - popd # Like a cd on the directory stored by pushd } # Install dependencies with a equivs control file From cc77cd788deebe036180099ccca0af5ca43af3f0 Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 9 Feb 2017 13:37:51 +0100 Subject: [PATCH 0232/1066] [fix] Permission issue on install of some apps 778 --- data/helpers.d/filesystem | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 27a016e63..bce58b5cf 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -70,6 +70,13 @@ ynh_bind_or_cp() { # usage: ynh_mkdir_tmp # | ret: the created directory path ynh_mkdir_tmp() { - echo "This helper is deprecated, you should use 'mktemp -d' instead." >&2 - mktemp -d + echo "The helper ynh_mkdir_tmp is deprecated." >&2 + echo "You should use 'mktemp -d' instead and manage permissions \ +properly with chmod/chown." >&2 + local TMP_DIR=$(mktemp -d) + + # Give rights to other users could be a security risk. + # But for retrocompatibility we need it. (This helpers is deprecated) + chmod 755 $TMP_DIR + echo $TMP_DIR } From 1af7b9cf328ffff195677a9c70d960bcc0b4c320 Mon Sep 17 00:00:00 2001 From: opi Date: Thu, 9 Feb 2017 22:27:26 +0100 Subject: [PATCH 0233/1066] Update changelog for 2.5.5 release --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index e86ca03a8..42f5eb241 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (2.5.5) stable; urgency=low + + Hotfix release + + [ ljf ] + * [fix] Permission issue on install of some apps 778 + + -- opi Thu, 09 Feb 2017 22:27:08 +0100 + yunohost (2.5.4) stable; urgency=low [ Maniack Crudelis ] From f6c7702dfaf3a7879323a9df60fde6ac58d3aff7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 12 Feb 2017 03:34:15 +0100 Subject: [PATCH 0234/1066] [mod] rename all global variables to uppercase --- src/yunohost/app.py | 82 ++++++++++++++++++++-------------------- src/yunohost/backup.py | 40 ++++++++++---------- src/yunohost/firewall.py | 20 +++++----- src/yunohost/hook.py | 34 ++++++++--------- src/yunohost/monitor.py | 24 ++++++------ src/yunohost/service.py | 24 ++++++------ src/yunohost/tools.py | 5 ++- 7 files changed, 115 insertions(+), 114 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5499f4b02..aa5ec23cc 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,11 +44,11 @@ from yunohost.utils import packages logger = getActionLogger('yunohost.app') -repo_path = '/var/cache/yunohost/repo' -apps_path = '/usr/share/yunohost/apps' -apps_setting_path= '/etc/yunohost/apps/' -install_tmp = '/var/cache/yunohost' -app_tmp_folder = install_tmp + '/from_file' +REPO_PATH = '/var/cache/yunohost/repo' +APPS_PATH = '/usr/share/yunohost/apps' +APPS_SETTING_PATH= '/etc/yunohost/apps/' +INSTALL_TMP = '/var/cache/yunohost' +APP_TMP_FOLDER = INSTALL_TMP + '/from_file' re_github_repo = re.compile( r'^(http[s]?://|git@)github.com[/:]' @@ -69,7 +69,7 @@ def app_listlists(): """ list_list = [] try: - for filename in os.listdir(repo_path): + for filename in os.listdir(REPO_PATH): if '.json' in filename: list_list.append(filename[:len(filename)-5]) except OSError: @@ -88,8 +88,8 @@ def app_fetchlist(url=None, name=None): """ # Create app path if not exists - if not os.path.exists(repo_path): - os.makedirs(repo_path) + if not os.path.exists(REPO_PATH): + os.makedirs(REPO_PATH) if url is None: url = 'https://app.yunohost.org/official.json' @@ -117,7 +117,7 @@ def app_fetchlist(url=None, name=None): raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_bad_format')) # Write app list to file - list_file = '%s/%s.json' % (repo_path, name) + list_file = '%s/%s.json' % (REPO_PATH, name) with open(list_file, "w") as f: f.write(applist) @@ -136,7 +136,7 @@ def app_removelist(name): """ try: - os.remove('%s/%s.json' % (repo_path, name)) + os.remove('%s/%s.json' % (REPO_PATH, name)) os.remove("/etc/cron.d/yunohost-applist-%s" % name) except OSError: raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown')) @@ -177,13 +177,13 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w applists = app_listlists()['lists'] for applist in applists: - with open(os.path.join(repo_path, applist + '.json')) as json_list: + with open(os.path.join(REPO_PATH, applist + '.json')) as json_list: for app, info in json.loads(str(json_list.read())).items(): if app not in app_dict: info['repository'] = applist app_dict[app] = info - for app in os.listdir(apps_setting_path): + for app in os.listdir(APPS_SETTING_PATH): if app not in app_dict: # Look for forks if '__' in app: @@ -191,7 +191,7 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w if original_app in app_dict: app_dict[app] = app_dict[original_app] continue - with open( apps_setting_path + app +'/manifest.json') as json_manifest: + with open( APPS_SETTING_PATH + app +'/manifest.json') as json_manifest: app_dict[app] = {"manifest":json.loads(str(json_manifest.read()))} app_dict[app]['repository'] = None @@ -212,8 +212,8 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w # Filter only apps with backup and restore scripts if with_backup and ( - not os.path.isfile(apps_setting_path + app_id + '/scripts/backup') or - not os.path.isfile(apps_setting_path + app_id + '/scripts/restore') + not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/backup') or + not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/restore') ): continue @@ -269,7 +269,7 @@ def app_info(app, show_status=False, raw=False): ret['settings'] = _get_app_settings(app) return ret - app_setting_path = apps_setting_path + app + app_setting_path = APPS_SETTING_PATH + app # Retrieve manifest and status with open(app_setting_path + '/manifest.json') as f: @@ -309,7 +309,7 @@ def app_map(app=None, raw=False, user=None): m18n.n('app_not_installed', app=app)) apps = [app,] else: - apps = os.listdir(apps_setting_path) + apps = os.listdir(APPS_SETTING_PATH) for app_id in apps: app_settings = _get_app_settings(app_id) @@ -363,7 +363,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # If no app is specified, upgrade all apps if not app: if (not url and not file): - app = os.listdir(apps_setting_path) + app = os.listdir(APPS_SETTING_PATH) elif not isinstance(app, list): app = [ app ] @@ -398,7 +398,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Check requirements _check_manifest_requirements(manifest) - app_setting_path = apps_setting_path +'/'+ app_instance_name + app_setting_path = APPS_SETTING_PATH +'/'+ app_instance_name # Retrieve current app status status = _get_app_status(app_instance_name) @@ -418,7 +418,7 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) # Execute App upgrade script - os.system('chown -hR admin: %s' % install_tmp) + os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder +'/scripts/upgrade', args=args_list, env=env_dict) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: @@ -467,8 +467,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): from yunohost.hook import hook_add, hook_remove, hook_exec # Fetch or extract sources - try: os.listdir(install_tmp) - except OSError: os.makedirs(install_tmp) + try: os.listdir(INSTALL_TMP) + except OSError: os.makedirs(INSTALL_TMP) status = { 'installed_at': int(time.time()), @@ -521,7 +521,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Create app directory - app_setting_path = os.path.join(apps_setting_path, app_instance_name) + app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) os.makedirs(app_setting_path) @@ -538,7 +538,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.system('chown -R admin: '+ extracted_app_folder) # Execute App install script - os.system('chown -hR admin: %s' % install_tmp) + os.system('chown -hR admin: %s' % INSTALL_TMP) # Move scripts and manifest to the right place os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) @@ -614,7 +614,7 @@ def app_remove(auth, app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) - app_setting_path = apps_setting_path + app + app_setting_path = APPS_SETTING_PATH + app #TODO: display fail messages from script try: @@ -783,7 +783,7 @@ def app_debug(app): Keyword argument: app """ - with open(apps_setting_path + app + '/manifest.json') as f: + with open(APPS_SETTING_PATH + app + '/manifest.json') as f: manifest = json.loads(f.read()) return { @@ -1021,7 +1021,7 @@ def app_ssowatconf(auth): for app in apps_list: if _is_installed(app['id']): - with open(apps_setting_path + app['id'] +'/settings.yml') as f: + with open(APPS_SETTING_PATH + app['id'] +'/settings.yml') as f: app_settings = yaml.load(f) for item in _get_setting(app_settings, 'skipped_uris'): if item[-1:] == '/': @@ -1092,7 +1092,7 @@ def _get_app_settings(app_id): m18n.n('app_not_installed', app=app_id)) try: with open(os.path.join( - apps_setting_path, app_id, 'settings.yml')) as f: + APPS_SETTING_PATH, app_id, 'settings.yml')) as f: settings = yaml.load(f) if app_id == settings['id']: return settings @@ -1112,7 +1112,7 @@ def _set_app_settings(app_id, settings): """ with open(os.path.join( - apps_setting_path, app_id, 'settings.yml'), 'w') as f: + APPS_SETTING_PATH, app_id, 'settings.yml'), 'w') as f: yaml.safe_dump(settings, f, default_flow_style=False) @@ -1125,7 +1125,7 @@ def _get_app_status(app_id, format_date=False): format_date -- Format date fields """ - app_setting_path = apps_setting_path + app_id + app_setting_path = APPS_SETTING_PATH + app_id if not os.path.isdir(app_setting_path): raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) status = {} @@ -1170,22 +1170,22 @@ def _extract_app_from_file(path, remove=False): """ logger.info(m18n.n('extracting')) - if os.path.exists(app_tmp_folder): shutil.rmtree(app_tmp_folder) - os.makedirs(app_tmp_folder) + if os.path.exists(APP_TMP_FOLDER): shutil.rmtree(APP_TMP_FOLDER) + os.makedirs(APP_TMP_FOLDER) path = os.path.abspath(path) if ".zip" in path: - extract_result = os.system('unzip %s -d %s > /dev/null 2>&1' % (path, app_tmp_folder)) + extract_result = os.system('unzip %s -d %s > /dev/null 2>&1' % (path, APP_TMP_FOLDER)) if remove: os.remove(path) elif ".tar" in path: - extract_result = os.system('tar -xf %s -C %s > /dev/null 2>&1' % (path, app_tmp_folder)) + extract_result = os.system('tar -xf %s -C %s > /dev/null 2>&1' % (path, APP_TMP_FOLDER)) if remove: os.remove(path) elif os.path.isdir(path): - shutil.rmtree(app_tmp_folder) + shutil.rmtree(APP_TMP_FOLDER) if path[len(path)-1:] != '/': path = path + '/' - extract_result = os.system('cp -a "%s" %s' % (path, app_tmp_folder)) + extract_result = os.system('cp -a "%s" %s' % (path, APP_TMP_FOLDER)) else: extract_result = 1 @@ -1193,7 +1193,7 @@ def _extract_app_from_file(path, remove=False): raise MoulinetteError(errno.EINVAL, m18n.n('app_extraction_failed')) try: - extracted_app_folder = app_tmp_folder + extracted_app_folder = APP_TMP_FOLDER if len(os.listdir(extracted_app_folder)) == 1: for folder in os.listdir(extracted_app_folder): extracted_app_folder = extracted_app_folder +'/'+ folder @@ -1240,7 +1240,7 @@ def _fetch_app_from_git(app): Dict manifest """ - extracted_app_folder = app_tmp_folder + extracted_app_folder = APP_TMP_FOLDER app_tmp_archive = '{0}.zip'.format(extracted_app_folder) if os.path.exists(extracted_app_folder): @@ -1383,9 +1383,9 @@ def _installed_instance_number(app, last=False): if last: number = 0 try: - installed_apps = os.listdir(apps_setting_path) + installed_apps = os.listdir(APPS_SETTING_PATH) except OSError: - os.makedirs(apps_setting_path) + os.makedirs(APPS_SETTING_PATH) return 0 for installed_app in installed_apps: @@ -1419,7 +1419,7 @@ def _is_installed(app): Boolean """ - return os.path.isdir(apps_setting_path + app) + return os.path.isdir(APPS_SETTING_PATH + app) def _value_for_locale(values): diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 587da31c7..97a171dfb 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -42,13 +42,13 @@ from yunohost.app import ( app_info, app_ssowatconf, _is_installed, _parse_app_instance_name ) from yunohost.hook import ( - hook_info, hook_callback, hook_exec, custom_hook_folder + hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER ) from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall -backup_path = '/home/yunohost.backup' -archives_path = '%s/archives' % backup_path +BACKUP_PATH = '/home/yunohost.backup' +ARCHIVES_PATH = '%s/archives' % BACKUP_PATH logger = getActionLogger('yunohost.backup') @@ -95,7 +95,7 @@ def backup_create(name=None, description=None, output_directory=None, output_directory = os.path.abspath(output_directory) # Check for forbidden folders - if output_directory.startswith(archives_path) or \ + if output_directory.startswith(ARCHIVES_PATH) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): raise MoulinetteError(errno.EINVAL, @@ -118,11 +118,11 @@ def backup_create(name=None, description=None, output_directory=None, tmp_dir = output_directory env_var['CAN_BIND'] = 0 else: - output_directory = archives_path + output_directory = ARCHIVES_PATH # Create archives directory if it does not exists - if not os.path.isdir(archives_path): - os.mkdir(archives_path, 0750) + if not os.path.isdir(ARCHIVES_PATH): + os.mkdir(ARCHIVES_PATH, 0750) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) @@ -135,7 +135,7 @@ def backup_create(name=None, description=None, output_directory=None, # Create temporary directory if not tmp_dir: - tmp_dir = "%s/tmp/%s" % (backup_path, name) + tmp_dir = "%s/tmp/%s" % (BACKUP_PATH, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for backup '%s' already exists", tmp_dir) @@ -305,12 +305,12 @@ def backup_create(name=None, description=None, output_directory=None, # Move info file shutil.move(tmp_dir + '/info.json', - '{:s}/{:s}.info.json'.format(archives_path, name)) + '{:s}/{:s}.info.json'.format(ARCHIVES_PATH, name)) # If backuped to a non-default location, keep a symlink of the archive # to that location - if output_directory != archives_path: - link = "%s/%s.tar.gz" % (archives_path, name) + if output_directory != ARCHIVES_PATH: + link = "%s/%s.tar.gz" % (ARCHIVES_PATH, name) os.symlink(archive_file, link) # Clean temporary directory @@ -354,19 +354,19 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory - tmp_dir = "%s/tmp/%s" % (backup_path, name) + tmp_dir = "%s/tmp/%s" % (BACKUP_PATH, name) if os.path.isdir(tmp_dir): logger.debug("temporary directory for restoration '%s' already exists", tmp_dir) os.system('rm -rf %s' % tmp_dir) # Check available disk space - statvfs = os.statvfs(backup_path) + statvfs = os.statvfs(BACKUP_PATH) free_space = statvfs.f_frsize * statvfs.f_bavail if free_space < info['size']: logger.debug("%dB left but %dB is needed", free_space, info['size']) raise MoulinetteError( - errno.EIO, m18n.n('not_enough_disk_space', path=backup_path)) + errno.EIO, m18n.n('not_enough_disk_space', path=BACKUP_PATH)) def _clean_tmp_dir(retcode=0): ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) @@ -455,7 +455,7 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, continue # Add restoration hook from the backup to the system # FIXME: Refactor hook_add and use it instead - restore_hook_folder = custom_hook_folder + 'restore' + restore_hook_folder = CUSTOM_HOOK_FOLDER + 'restore' filesystem.mkdir(restore_hook_folder, 755, True) for f in tmp_hooks: logger.debug("adding restoration hook '%s' to the system " @@ -580,7 +580,7 @@ def backup_list(with_info=False, human_readable=False): try: # Retrieve local archives - archives = os.listdir(archives_path) + archives = os.listdir(ARCHIVES_PATH) except OSError: logger.debug("unable to iterate over local archives", exc_info=1) else: @@ -612,7 +612,7 @@ def backup_info(name, with_details=False, human_readable=False): human_readable -- Print sizes in human readable format """ - archive_file = '%s/%s.tar.gz' % (archives_path, name) + archive_file = '%s/%s.tar.gz' % (ARCHIVES_PATH, name) # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): @@ -628,7 +628,7 @@ def backup_info(name, with_details=False, human_readable=False): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_broken_link', path=archive_file)) - info_file = "%s/%s.info.json" % (archives_path, name) + info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) try: with open(info_file) as f: @@ -673,9 +673,9 @@ def backup_delete(name): """ hook_callback('pre_backup_delete', args=[name]) - archive_file = '%s/%s.tar.gz' % (archives_path, name) + archive_file = '%s/%s.tar.gz' % (ARCHIVES_PATH, name) - info_file = "%s/%s.info.json" % (archives_path, name) + info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) for backup_file in [archive_file, info_file]: if not os.path.isfile(backup_file): raise MoulinetteError(errno.EIO, diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 91f484f48..a4e68eac5 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -38,8 +38,8 @@ from moulinette.utils import process from moulinette.utils.log import getActionLogger from moulinette.utils.text import prependlines -firewall_file = '/etc/yunohost/firewall.yml' -upnp_cron_job = '/etc/cron.d/yunohost-firewall-upnp' +FIREWALL_FILE = '/etc/yunohost/firewall.yml' +UPNP_CRON_JOB = '/etc/cron.d/yunohost-firewall-upnp' logger = getActionLogger('yunohost.firewall') @@ -161,7 +161,7 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False): list_forwarded -- List forwarded ports with UPnP """ - with open(firewall_file) as f: + with open(FIREWALL_FILE) as f: firewall = yaml.load(f) if raw: return firewall @@ -318,7 +318,7 @@ def firewall_upnp(action='status', no_refresh=False): return {'enabled': enabled} elif action == 'enable' or (enabled and action == 'status'): # Add cron job - with open(upnp_cron_job, 'w+') as f: + with open(UPNP_CRON_JOB, 'w+') as f: f.write('*/50 * * * * root ' '/usr/bin/yunohost firewall upnp status >>/dev/null\n') # Open port 1900 to receive discovery message @@ -330,7 +330,7 @@ def firewall_upnp(action='status', no_refresh=False): elif action == 'disable' or (not enabled and action == 'status'): try: # Remove cron job - os.remove(upnp_cron_job) + os.remove(UPNP_CRON_JOB) except: pass enabled = False @@ -384,8 +384,8 @@ def firewall_upnp(action='status', no_refresh=False): firewall['uPnP']['enabled'] = enabled # Make a backup and update firewall file - os.system("cp {0} {0}.old".format(firewall_file)) - with open(firewall_file, 'w') as f: + os.system("cp {0} {0}.old".format(FIREWALL_FILE)) + with open(FIREWALL_FILE, 'w') as f: yaml.safe_dump(firewall, f, default_flow_style=False) if not no_refresh: @@ -427,7 +427,7 @@ def firewall_stop(): os.system("ip6tables -F") os.system("ip6tables -X") - if os.path.exists(upnp_cron_job): + if os.path.exists(UPNP_CRON_JOB): firewall_upnp('disable') @@ -450,8 +450,8 @@ def _get_ssh_port(default=22): def _update_firewall_file(rules): """Make a backup and write new rules to firewall file""" - os.system("cp {0} {0}.old".format(firewall_file)) - with open(firewall_file, 'w') as f: + os.system("cp {0} {0}.old".format(FIREWALL_FILE)) + with open(FIREWALL_FILE, 'w') as f: yaml.safe_dump(rules, f, default_flow_style=False) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index db7cd9504..137232ed3 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -31,8 +31,8 @@ from glob import iglob from moulinette.core import MoulinetteError from moulinette.utils import log -hook_folder = '/usr/share/yunohost/hooks/' -custom_hook_folder = '/etc/yunohost/hooks.d/' +HOOK_FOLDER = '/usr/share/yunohost/hooks/' +CUSTOM_HOOK_FOLDER = '/etc/yunohost/hooks.d/' logger = log.getActionLogger('yunohost.hook') @@ -49,12 +49,12 @@ def hook_add(app, file): path, filename = os.path.split(file) priority, action = _extract_filename_parts(filename) - try: os.listdir(custom_hook_folder + action) - except OSError: os.makedirs(custom_hook_folder + action) + try: os.listdir(CUSTOM_HOOK_FOLDER + action) + except OSError: os.makedirs(CUSTOM_HOOK_FOLDER + action) - finalpath = custom_hook_folder + action +'/'+ priority +'-'+ app + finalpath = CUSTOM_HOOK_FOLDER + action +'/'+ priority +'-'+ app os.system('cp %s %s' % (file, finalpath)) - os.system('chown -hR admin: %s' % hook_folder) + os.system('chown -hR admin: %s' % HOOK_FOLDER) return { 'hook': finalpath } @@ -68,10 +68,10 @@ def hook_remove(app): """ try: - for action in os.listdir(custom_hook_folder): - for script in os.listdir(custom_hook_folder + action): + for action in os.listdir(CUSTOM_HOOK_FOLDER): + for script in os.listdir(CUSTOM_HOOK_FOLDER + action): if script.endswith(app): - os.remove(custom_hook_folder + action +'/'+ script) + os.remove(CUSTOM_HOOK_FOLDER + action +'/'+ script) except OSError: pass @@ -89,7 +89,7 @@ def hook_info(action, name): # Search in custom folder first for h in iglob('{:s}{:s}/*-{:s}'.format( - custom_hook_folder, action, name)): + CUSTOM_HOOK_FOLDER, action, name)): priority, _ = _extract_filename_parts(os.path.basename(h)) priorities.add(priority) hooks.append({ @@ -98,7 +98,7 @@ def hook_info(action, name): }) # Append non-overwritten system hooks for h in iglob('{:s}{:s}/*-{:s}'.format( - hook_folder, action, name)): + HOOK_FOLDER, action, name)): priority, _ = _extract_filename_parts(os.path.basename(h)) if priority not in priorities: hooks.append({ @@ -183,23 +183,23 @@ def hook_list(action, list_by='name', show_info=False): # Append system hooks first if list_by == 'folder': result['system'] = dict() if show_info else set() - _append_folder(result['system'], hook_folder) + _append_folder(result['system'], HOOK_FOLDER) else: - _append_folder(result, hook_folder) + _append_folder(result, HOOK_FOLDER) except OSError: logger.debug("system hook folder not found for action '%s' in %s", - action, hook_folder) + action, HOOK_FOLDER) try: # Append custom hooks if list_by == 'folder': result['custom'] = dict() if show_info else set() - _append_folder(result['custom'], custom_hook_folder) + _append_folder(result['custom'], CUSTOM_HOOK_FOLDER) else: - _append_folder(result, custom_hook_folder) + _append_folder(result, CUSTOM_HOOK_FOLDER) except OSError: logger.debug("custom hook folder not found for action '%s' in %s", - action, custom_hook_folder) + action, CUSTOM_HOOK_FOLDER) return { 'hooks': result } diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 18089e328..8fd8b7732 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -44,9 +44,9 @@ from yunohost.domain import get_public_ip logger = getActionLogger('yunohost.monitor') -glances_uri = 'http://127.0.0.1:61209' -stats_path = '/var/lib/yunohost/stats' -crontab_path = '/etc/cron.d/yunohost-monitor' +GLANCES_URI = 'http://127.0.0.1:61209' +STATS_PATH = '/var/lib/yunohost/stats' +CRONTAB_PATH = '/etc/cron.d/yunohost-monitor' def monitor_disk(units=None, mountpoint=None, human_readable=False): @@ -422,7 +422,7 @@ def monitor_enable(with_stats=False): '3 * * * * root {cmd} week >> /dev/null\n' '6 */4 * * * root {cmd} month >> /dev/null').format( cmd='/usr/bin/yunohost --quiet monitor update-stats') - with open(crontab_path, 'w') as f: + with open(CRONTAB_PATH, 'w') as f: f.write(rules) logger.success(m18n.n('monitor_enabled')) @@ -447,7 +447,7 @@ def monitor_disable(): # Remove crontab try: - os.remove(crontab_path) + os.remove(CRONTAB_PATH) except: pass @@ -460,7 +460,7 @@ def _get_glances_api(): """ try: - p = xmlrpclib.ServerProxy(glances_uri) + p = xmlrpclib.ServerProxy(GLANCES_URI) p.system.methodHelp('getAll') except (xmlrpclib.ProtocolError, IOError): pass @@ -552,9 +552,9 @@ def _retrieve_stats(period, date=None): # Retrieve pickle file if date is not None: timestamp = calendar.timegm(date) - pkl_file = '%s/%d_%s.pkl' % (stats_path, timestamp, period) + pkl_file = '%s/%d_%s.pkl' % (STATS_PATH, timestamp, period) else: - pkl_file = '%s/%s.pkl' % (stats_path, period) + pkl_file = '%s/%s.pkl' % (STATS_PATH, period) if not os.path.isfile(pkl_file): return False @@ -581,11 +581,11 @@ def _save_stats(stats, period, date=None): # Set pickle file name if date is not None: timestamp = calendar.timegm(date) - pkl_file = '%s/%d_%s.pkl' % (stats_path, timestamp, period) + pkl_file = '%s/%d_%s.pkl' % (STATS_PATH, timestamp, period) else: - pkl_file = '%s/%s.pkl' % (stats_path, period) - if not os.path.isdir(stats_path): - os.makedirs(stats_path) + pkl_file = '%s/%s.pkl' % (STATS_PATH, period) + if not os.path.isdir(STATS_PATH): + os.makedirs(STATS_PATH) # Limit stats if date is None: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 01b85dfbe..e9cd6c4fe 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -39,9 +39,9 @@ from moulinette.utils import log, filesystem from yunohost.hook import hook_callback -base_conf_path = '/home/yunohost.conf' -backup_conf_dir = os.path.join(base_conf_path, 'backup') -pending_conf_dir = os.path.join(base_conf_path, 'pending') +BASE_CONF_PATH = '/home/yunohost.conf' +BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup') +PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending') logger = log.getActionLogger('yunohost.service') @@ -300,15 +300,15 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, return pending_conf # Clean pending conf directory - if os.path.isdir(pending_conf_dir): + if os.path.isdir(PENDING_CONF_DIR): if not names: - shutil.rmtree(pending_conf_dir, ignore_errors=True) + shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True) else: for name in names: - shutil.rmtree(os.path.join(pending_conf_dir, name), + shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: - filesystem.mkdir(pending_conf_dir, 0755, True) + filesystem.mkdir(PENDING_CONF_DIR, 0755, True) # Format common hooks arguments common_args = [1 if force else 0, 1 if dry_run else 0] @@ -318,7 +318,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, def _pre_call(name, priority, path, args): # create the pending conf directory for the service - service_pending_path = os.path.join(pending_conf_dir, name) + service_pending_path = os.path.join(PENDING_CONF_DIR, name) filesystem.mkdir(service_pending_path, 0755, True, uid='admin') # return the arguments to pass to the script return pre_args + [service_pending_path, ] @@ -615,12 +615,12 @@ def _get_pending_conf(services=[]): """ result = {} - if not os.path.isdir(pending_conf_dir): + if not os.path.isdir(PENDING_CONF_DIR): return result if not services: - services = os.listdir(pending_conf_dir) + services = os.listdir(PENDING_CONF_DIR) for name in services: - service_pending_path = os.path.join(pending_conf_dir, name) + service_pending_path = os.path.join(PENDING_CONF_DIR, name) if not os.path.isdir(service_pending_path): continue path_index = len(service_pending_path) @@ -672,7 +672,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): """ if save: - backup_path = os.path.join(backup_conf_dir, '{0}-{1}'.format( + backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format( system_conf.lstrip('/'), time.strftime("%Y%m%d.%H%M%S"))) backup_dir = os.path.dirname(backup_path) if not os.path.isdir(backup_dir): diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 15c8a98f8..9d1d1c668 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -46,7 +46,8 @@ from yunohost.service import service_status, service_regen_conf, service_log from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version -apps_setting_path= '/etc/yunohost/apps/' +# FIXME this is a duplicate from apps.py +APPS_SETTING_PATH= '/etc/yunohost/apps/' logger = getActionLogger('yunohost.tools') @@ -341,7 +342,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): app_fetchlist() except MoulinetteError: pass - app_list = os.listdir(apps_setting_path) + app_list = os.listdir(APPS_SETTING_PATH) if len(app_list) > 0: for app_id in app_list: if '__' in app_id: From 5abcaadaeabdd60b40baf6e79fff3273c1dd6108 Mon Sep 17 00:00:00 2001 From: ZeHiro Date: Sun, 12 Feb 2017 12:16:54 +0100 Subject: [PATCH 0235/1066] [fix] handle the case where services[service] is set to null in the services.yml. Fix #785 --- src/yunohost/service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 01b85dfbe..a45a9164a 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -658,6 +658,11 @@ def _update_conf_hashes(service, hashes): service, hashes) services = _get_services() service_conf = services.get(service, {}) + + # Handle the case where services[service] is set to null in the yaml + if service_conf is None: + service_conf = {} + service_conf['conffiles'] = hashes services[service] = service_conf _save_services(services) From f28be91b5d25120aa13d9861b0b3be840f330ac0 Mon Sep 17 00:00:00 2001 From: opi Date: Sun, 12 Feb 2017 21:25:04 +0100 Subject: [PATCH 0236/1066] [fix] Uppercase global variable even in comment. --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index aa5ec23cc..d759c01cb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1158,7 +1158,7 @@ def _get_app_status(app_id, format_date=False): def _extract_app_from_file(path, remove=False): """ - Unzip or untar application tarball in app_tmp_folder, or copy it from a directory + Unzip or untar application tarball in APP_TMP_FOLDER, or copy it from a directory Keyword arguments: path -- Path of the tarball or directory @@ -1231,7 +1231,7 @@ def _get_git_last_commit_hash(repository, reference='HEAD'): def _fetch_app_from_git(app): """ - Unzip or untar application tarball in app_tmp_folder + Unzip or untar application tarball in APP_TMP_FOLDER Keyword arguments: app -- App_id or git repo URL From 0e55b17665cf1cd05c157950cbc5601421910a2e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 12 Feb 2017 18:49:59 -0500 Subject: [PATCH 0237/1066] Fixing also get_conf_hashes --- src/yunohost/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index a45a9164a..e3812f3db 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -645,7 +645,8 @@ def _get_conf_hashes(service): if service not in services: logger.debug("Service %s is not in services.yml yet.", service) return {} - elif 'conffiles' not in services[service]: + + if (services[service] is None) or ('conffiles' not in services[service]): logger.debug("No configuration files for service %s.", service) return {} else: From 1d561123b6f6fad1712c795c31409dedc24d0160 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 13 Feb 2017 16:07:09 +0100 Subject: [PATCH 0238/1066] [enh] Set main domain as hostname (#219) --- locales/en.json | 1 + src/yunohost/tools.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/locales/en.json b/locales/en.json index 0880fdddd..fb1c58b43 100644 --- a/locales/en.json +++ b/locales/en.json @@ -81,6 +81,7 @@ "domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "Domain already exists", + "domain_hostname_failed": "Failed to set new hostname", "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", "domain_unknown": "Unknown domain", "domain_zone_exists": "DNS zone file already exists", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 9d1d1c668..8470a7b27 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -163,6 +163,28 @@ def tools_maindomain(auth, new_domain=None): logger.warning("%s" % e, exc_info=1) raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) + # Set hostname + pretty_hostname = "(YunoHost/%s)" % new_domain + commands = [ + "sudo hostnamectl --static set-hostname".split() + [new_domain], + "sudo hostnamectl --transient set-hostname".split() + [new_domain], + "sudo hostnamectl --pretty set-hostname".split() + [pretty_hostname] + ] + + for command in commands: + p = subprocess.Popen(command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + out, _ = p.communicate() + + if p.returncode != 0: + logger.warning(command) + logger.warning(out) + raise MoulinetteError(errno.EIO, m18n.n('domain_hostname_failed')) + else: + logger.info(out) + # Regen configurations try: with open('/etc/yunohost/installed', 'r') as f: From d4feb879d44171447be33a65538503223b4a56fb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 13 Feb 2017 16:10:37 +0100 Subject: [PATCH 0239/1066] [enh] include script to reset ldap password (#217) --- debian/install | 1 + sbin/yunohost-reset-ldap-password | 69 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100755 sbin/yunohost-reset-ldap-password diff --git a/debian/install b/debian/install index 19523bec5..70add7992 100644 --- a/debian/install +++ b/debian/install @@ -1,4 +1,5 @@ bin/* /usr/bin/ +sbin/* /usr/sbin/ data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ diff --git a/sbin/yunohost-reset-ldap-password b/sbin/yunohost-reset-ldap-password new file mode 100755 index 000000000..916b70b18 --- /dev/null +++ b/sbin/yunohost-reset-ldap-password @@ -0,0 +1,69 @@ +#!/bin/bash + +################################ +# Set a temporary password # +################################ + +# Generate a random temporary password (won't be valid after this script ends !) +# and hash it +TMP_LDAPROOT_PASSWORD=`slappasswd -g` +TMP_LDAPROOT_PASSWORD_HASH=`slappasswd -h {SSHA} -s ${TMP_LDAPROOT_PASSWORD}` + +# Stop slapd service... +service slapd stop + +# Backup slapd.conf (to be restored at the end of script) +cp /etc/ldap/slapd.conf /root/slapd.conf.bkp + +# Append lines to slapd.conf to manually define root password hash +echo 'rootdn "cn=admin,dc=yunohost,dc=org"' >> /etc/ldap/slapd.conf +echo "rootpw $TMP_LDAPROOT_PASSWORD_HASH" >> /etc/ldap/slapd.conf + +# Test conf (might not be entirely necessary though :P) +slaptest -Q -u -f /etc/ldap/slapd.conf + +# Regenerate slapd.d directory +rm -Rf /etc/ldap/slapd.d +mkdir /etc/ldap/slapd.d +slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1 + +# Set permissions to slapd.d +chown -R openldap:openldap /etc/ldap/slapd.d/ + +# Restore slapd.conf +mv /root/slapd.conf.bkp /etc/ldap/slapd.conf + +# Restart slapd service +service slapd start + +####################################### +# Properly set new admin password # +####################################### + +# Display tmp password to user +# NB : we do NOT pass it as a command line argument for "yunohost tools adminpw" +# as a malicious user could run a script in background waiting for this command +# to pop in ps -ef and automatically do nasty stuff in the ldap database +# meanwhile. +echo "Use this temporary password when asked for the administration password : $TMP_LDAPROOT_PASSWORD" + +# Call yunohost tools adminpw for user to set new password +yunohost tools adminpw + +########################### +# Forget tmp password # +########################### + +# Stop slapd service +service slapd stop + +# Regenerate slapd.d directory +rm -Rf /etc/ldap/slapd.d +mkdir /etc/ldap/slapd.d +slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1 + +# Set permissions to slapd.d +chown -R openldap:openldap /etc/ldap/slapd.d/ + +# Restart slapd service +service slapd start From cc4451253917040c3a464dce4c12e9e7cf486b15 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 14 Feb 2017 13:53:07 +0100 Subject: [PATCH 0240/1066] Clean app upgrade (#193) * [mod] current_app_dict == new_app_dict so test is always false * [mod] simplify condition\n\nUntestedgit add -p * [mod] useless variable * [mod] rename variable * [mod] app_dict can't be None (exception is raised instead) * Adding debug message for upgrade * Using app_list to list installed apps * [mod] pep8 * [mod] please opi --- src/yunohost/app.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d759c01cb..17223228d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -362,10 +362,12 @@ def app_upgrade(auth, app=[], url=None, file=None): # If no app is specified, upgrade all apps if not app: - if (not url and not file): - app = os.listdir(APPS_SETTING_PATH) + if not url and not file: + app = [app["id"] for app in app_list(installed=True)["apps"]] elif not isinstance(app, list): - app = [ app ] + app = [app] + + logger.info("Upgrading apps %s", ", ".join(app)) for app_instance_name in app: installed = _is_installed(app_instance_name) @@ -376,21 +378,18 @@ def app_upgrade(auth, app=[], url=None, file=None): if app_instance_name in upgraded_apps: continue - current_app_dict = app_info(app_instance_name, raw=True) - new_app_dict = app_info(app_instance_name, raw=True) + app_dict = app_info(app_instance_name, raw=True) + + locale_update_time = app_dict['settings'].get('update_time', app_dict['settings']['install_time']) if file: manifest, extracted_app_folder = _extract_app_from_file(file) elif url: manifest, extracted_app_folder = _fetch_app_from_git(url) - elif new_app_dict is None or 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict: + elif 'lastUpdate' not in app_dict or 'git' not in app_dict: logger.warning(m18n.n('custom_app_url_required', app=app_instance_name)) continue - elif (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \ - or ('update_time' not in current_app_dict['settings'] \ - and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \ - or ('update_time' in current_app_dict['settings'] \ - and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])): + elif app_dict['lastUpdate'] > locale_update_time: manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) else: continue @@ -399,7 +398,7 @@ def app_upgrade(auth, app=[], url=None, file=None): _check_manifest_requirements(manifest) app_setting_path = APPS_SETTING_PATH +'/'+ app_instance_name - + # Retrieve current app status status = _get_app_status(app_instance_name) status['remote'] = manifest.get('remote', None) From accb78271ebefd4130ea23378d6289ac0fa9d0e4 Mon Sep 17 00:00:00 2001 From: julienmalik Date: Wed, 15 Feb 2017 13:20:58 +0100 Subject: [PATCH 0241/1066] [fix] Any address in the range 127.0.0.0/8 is a valid loopback address --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index bd7d02962..181e8aa22 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -860,8 +860,8 @@ def _domain_is_resolved_locally(public_ip, domain): logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) return False - logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, ip, public_ip)) - return ip in ["127.0.0.1", public_ip] + logger.debug("Domain '%s' IP address is resolved to %s, expect it to be %s or in the 127.0.0.0/8 address block" % (domain, public_ip, ip)) + return ip.startswith("127.") or ip == public_ip def _name_self_CA(): From f291d11c844d9e6f532f1ec748a5e1eddb24c2f6 Mon Sep 17 00:00:00 2001 From: julienmalik Date: Wed, 15 Feb 2017 13:33:53 +0100 Subject: [PATCH 0242/1066] [fix] cert-renew email headers appear as text in the body --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index bd7d02962..24a41b572 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -444,7 +444,7 @@ investigate : """ % (domain, exception_message, stack, logs) - message = """ + message = """\ From: %s To: %s Subject: %s From 81915150b2de3268172a1c35d3f4d0060d4cdee0 Mon Sep 17 00:00:00 2001 From: julienmalik Date: Wed, 15 Feb 2017 13:20:58 +0100 Subject: [PATCH 0243/1066] [fix] Any address in the range 127.0.0.0/8 is a valid loopback address --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index bd7d02962..181e8aa22 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -860,8 +860,8 @@ def _domain_is_resolved_locally(public_ip, domain): logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) return False - logger.debug("Domain '%s' ip is %s, except it to be 127.0.0.1 or %s" % (domain, ip, public_ip)) - return ip in ["127.0.0.1", public_ip] + logger.debug("Domain '%s' IP address is resolved to %s, expect it to be %s or in the 127.0.0.0/8 address block" % (domain, public_ip, ip)) + return ip.startswith("127.") or ip == public_ip def _name_self_CA(): From 1c2de37f6338cdd35f04adecc2b6a4bc0d9b4f87 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 14 Feb 2017 16:23:49 +0100 Subject: [PATCH 0244/1066] [fix] Update Rmilter configuration to fix dkim signing. --- data/hooks/conf_regen/28-rmilter | 9 ++++++++- data/templates/rmilter/rmilter.conf | 27 +++++++++++++++++++-------- data/templates/rmilter/ynh_dkim.conf | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 data/templates/rmilter/ynh_dkim.conf diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter index 011856cd6..f505b6d99 100755 --- a/data/hooks/conf_regen/28-rmilter +++ b/data/hooks/conf_regen/28-rmilter @@ -7,8 +7,14 @@ do_pre_regen() { cd /usr/share/yunohost/templates/rmilter + # Install main configuration install -D -m 644 rmilter.conf \ "${pending_dir}/etc/rmilter.conf" + + # Install DKIM specific configuration + install -D -m 644 ynh_dkim.conf \ + "${pending_dir}/etc/rmilter.conf.d/ynh_dkim.conf" + # Remove old socket file (we stopped using it, since rspamd 1.3.1) # Regen-conf system need an empty file to delete it install -D -m 644 /dev/null \ @@ -21,8 +27,9 @@ do_post_regen() { # retrieve variables domain_list=$(sudo yunohost domain list --output-as plain --quiet) - # create DKIM directory + # create DKIM directory with proper permission sudo mkdir -p /etc/dkim + sudo chown _rmilter /etc/dkim # create DKIM key for domains for domain in $domain_list; do diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf index 829d76418..dcd13e9b7 100644 --- a/data/templates/rmilter/rmilter.conf +++ b/data/templates/rmilter/rmilter.conf @@ -1,5 +1,21 @@ # systemd-specific settings for rmilter +# DKIM signing +# Defined before including /etc/rmilter.conf.common because rmilter seems to be +# unable to override dkim{} settings, even if it's already defined in +# /etc/rmilter.conf.d/ynh_dkim.conf +dkim { + enable = true; + domain { + key = /etc/dkim; + domain = "*"; + selector = "mail"; + }; + header_canon = relaxed; + body_canon = relaxed; + sign_alg = sha256; +}; + .include /etc/rmilter.conf.common # pidfile - path to pid file @@ -7,11 +23,6 @@ pidfile = /run/rmilter/rmilter.pid; bind_socket = unix:/var/spool/postfix/run/rmilter/rmilter.sock; -# DKIM signing -dkim { - domain { - key = /etc/dkim; - domain = "*"; - selector = "mail"; - }; -}; +# include user's configuration +.try_include /etc/rmilter.conf.local +.try_include /etc/rmilter.conf.d/*.conf diff --git a/data/templates/rmilter/ynh_dkim.conf b/data/templates/rmilter/ynh_dkim.conf new file mode 100644 index 000000000..1e5598d06 --- /dev/null +++ b/data/templates/rmilter/ynh_dkim.conf @@ -0,0 +1,14 @@ +# DKIM signing +# Note that DKIM signing should be done by rspamd in the near future +# See https://github.com/vstakhov/rmilter/issues/174 +dkim { + enable = true; + domain { + key = /etc/dkim; + domain = "*"; + selector = "mail"; + }; + header_canon = relaxed; + body_canon = relaxed; + sign_alg = sha256; +}; From 82060f2082c90f52fb173095d0175331abfde79d Mon Sep 17 00:00:00 2001 From: opi Date: Sat, 18 Feb 2017 15:51:18 +0100 Subject: [PATCH 0245/1066] Update changelog for 2.5.6 release --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 42f5eb241..5f182a9de 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (2.5.6) stable; urgency=low + + [ julienmalik ] + * [fix] Any address in the range 127.0.0.0/8 is a valid loopback address + + [ opi ] + * [fix] Update Rmilter configuration to fix dkim signing. + + -- opi Sat, 18 Feb 2017 15:51:13 +0100 + yunohost (2.5.5) stable; urgency=low Hotfix release From 834cf459dcd544919f893e73c6be6a471c7e0554 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 19 Feb 2017 20:15:37 +0100 Subject: [PATCH 0246/1066] Please Bram :D --- src/yunohost/service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index e3812f3db..17d5e50ab 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -645,8 +645,7 @@ def _get_conf_hashes(service): if service not in services: logger.debug("Service %s is not in services.yml yet.", service) return {} - - if (services[service] is None) or ('conffiles' not in services[service]): + elif services[service] is None or 'conffiles' not in services[service]: logger.debug("No configuration files for service %s.", service) return {} else: From 8486f440fb18d513468b696f84c0efe833298d77 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 20 Feb 2017 00:30:36 +0100 Subject: [PATCH 0247/1066] [enh] Add unit test mechanism (#254) --- src/yunohost/tests/__init__.py | 0 src/yunohost/tests/conftest.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 src/yunohost/tests/__init__.py create mode 100644 src/yunohost/tests/conftest.py diff --git a/src/yunohost/tests/__init__.py b/src/yunohost/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py new file mode 100644 index 000000000..2845de4ee --- /dev/null +++ b/src/yunohost/tests/conftest.py @@ -0,0 +1,16 @@ +import sys +import moulinette + +sys.path.append("..") + +old_init = moulinette.core.Moulinette18n.__init__ + + +def monkey_path_i18n_init(self, package, default_locale="en"): + old_init(self, package, default_locale) + self.load_namespace("yunohost") + + +moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init + +moulinette.init() From e0ee4542df5a7b9a10d6167f8c13407d41434ad8 Mon Sep 17 00:00:00 2001 From: opi Date: Mon, 20 Feb 2017 16:34:03 +0100 Subject: [PATCH 0248/1066] Update changelog for 2.6.0 release --- debian/changelog | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/debian/changelog b/debian/changelog index 5f182a9de..0945c5ad0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,62 @@ +yunohost (2.6.0) testing; urgency=low + + Important changes + + - [enh] Add unit test mechanism (#254) + - [fix] Any address in the range 127.0.0.0/8 is a valid loopback address for localhost + - [enh] include script to reset ldap password (#217) + - [enh] Set main domain as hostname (#219) + - [enh] New bash helpers for app scripts: ynh_system_user_create, ynh_system_user_delete, helper ynh_find_port + + Thanks to every contributors (Bram, Aleks, Maniack Crudelis, ZeHiro, opi, julienmalik + + Full changes log: + + 8486f440fb18d513468b696f84c0efe833298d77 [enh] Add unit test mechanism (#254) + 45e85fef821bd8c60c9ed1856b3b7741b45e4158 Merge pull request #252 from ZeHiro/fix-785 + 834cf459dcd544919f893e73c6be6a471c7e0554 Please Bram :D + 088abd694e0b0be8c8a9b7d96a3894baaf436459 Merge branch 'testing' into unstable + f80653580cd7be31484496dbe124b88e34ca066b Merge pull request #257 from YunoHost/fix_localhost_address_range + f291d11c844d9e6f532f1ec748a5e1eddb24c2f6 [fix] cert-renew email headers appear as text in the body + accb78271ebefd4130ea23378d6289ac0fa9d0e4 [fix] Any address in the range 127.0.0.0/8 is a valid loopback address + cc4451253917040c3a464dce4c12e9e7cf486b15 Clean app upgrade (#193) + d4feb879d44171447be33a65538503223b4a56fb [enh] include script to reset ldap password (#217) + 1d561123b6f6fad1712c795c31409dedc24d0160 [enh] Set main domain as hostname (#219) + 0e55b17665cf1cd05c157950cbc5601421910a2e Fixing also get_conf_hashes + 035100d6dbcd209dceb68af49b593208179b0595 Merge pull request #251 from YunoHost/uppercase_for_global_variables + f28be91b5d25120aa13d9861b0b3be840f330ac0 [fix] Uppercase global variable even in comment. + 5abcaadaeabdd60b40baf6e79fff3273c1dd6108 [fix] handle the case where services[service] is set to null in the services.yml. Fix #785 + 5d3e1c92126d861605bd209ff56b8b0d77d3ff39 Merge pull request #233 from YunoHost/ynh_find_port + 83dca8e7c6ec4efb206140c234f51dfa5b3f3bf7 Merge pull request #237 from YunoHost/ynh_system_user_create_delete + f6c7702dfaf3a7879323a9df60fde6ac58d3aff7 [mod] rename all global variables to uppercase + 3804f33b2f712eb067a0fcbb6fb5c60f3a813db4 Merge pull request #159 from YunoHost/clean_app_fetchlist + 8b44276af627ec05ac376c57e098716cacd165f9 Merge branch 'testing' into unstable + dea89fc6bb209047058f050352e3c082b9e62f32 Merge pull request #243 from YunoHost/fix-rspamd-rmilter-status + dea6177c070b9176e3955c4f32b8a602977cf424 Merge pull request #244 from YunoHost/fix-unattended-upgrade-syntax + a61445c9c3d231b9248fd247a0dd3345fc0ac6df Checking for 404 error and valid json format + 991b64db92e60f3bc92cb1ba4dc25f7e11fb1a8d Merge branch 'unstable' into clean_app_fetchlist + 730156dd92bbd1b0c479821ffc829e8d4f3d2019 Using request insteqd of urlretrieve, to have timeout + 5b006dbf0e074f4070f6832d2c64f3b306935e3f Adding info/debug message for fetchlist + 98d88f2364eda28ddc6b98d45a7fbe2bbbaba3d4 [fix] Unattended upgrades configuration syntax. + 7d4aa63c430516f815a8cdfd2f517f79565efe2f [fix] Rspamd & Rmilter are no more sockets + 5be13fd07e12d95f05272b9278129da4be0bc2d7 Merge pull request #220 from YunoHost/conf-hashes-logs + 901e3df9b604f542f2c460aad05bcc8efc9fd054 Pas de correction de l'argument + cd93427a97378ab635c85c0ae9a1e45132d6245c Retire la commande ynh + abb9f44b87cfed5fa14be9471b536fc27939d920 Nouveaux helpers ynh_system_user_create et ynh_system_user_delete + 3e9d086f7ff64f923b2d623df41ec42c88c8a8ef Nouveau helper ynh_find_port + 0b6ccaf31a8301b50648ec0ba0473d2190384355 Implementing comments + 5b7536cf1036cecee6fcc187b2d1c3f9b7124093 Style for Bram :) + e857f4f0b27d71299c498305b24e4b3f7e4571c4 [mod] Cleaner logs for _get_conf_hashes + 99f0f761a5e2737b55f9f8b6ce6094b5fd7fb1ca [mod] include execption into appslist_retrieve_error message + 2aab7bdf1bcc6f025c7c5bf618d0402439abd0f4 [mod] simplify code + 97128d7d636836068ad6353f331d051121023136 [mod] exception should only be used for exceptional situations and not when buildin functions allow you to do the expected stuff + d9081bddef1b2129ad42b05b28a26cc7680f7d51 [mod] directly use python to retreive json list + c4cecfcea5f51f1f9fb410358386eb5a6782cdb2 [mod] use python instead of os.system + cf3e28786cf829bc042226283399699195e21d79 [mod] remove useless line + + + -- opi Mon, 20 Feb 2017 16:31:52 +0100 + yunohost (2.5.6) stable; urgency=low [ julienmalik ] From 57ae1ad010c446d06ce0903dec9f28b4b9b80f04 Mon Sep 17 00:00:00 2001 From: Trollken Date: Tue, 7 Feb 2017 16:47:09 +0100 Subject: [PATCH 0249/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 42.2% (114 of 270 strings) --- locales/pt.json | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index d3796d2e9..3f7f417f5 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -5,21 +5,21 @@ "admin_password_changed": "Senha de administração alterada com êxito", "app_already_installed": "{app:s} já está instalada", "app_extraction_failed": "Não foi possível extrair os ficheiros para instalação", - "app_id_invalid": "ID da aplicação invélida", + "app_id_invalid": "A ID da aplicação é inválida", "app_install_files_invalid": "Ficheiros para instalação corrompidos", "app_location_already_used": "Já existe uma aplicação instalada neste diretório", - "app_location_install_failed": "Não foi possível instalar a aplicação neste diretório", + "app_location_install_failed": "Não é possível instalar a aplicação neste diretório", "app_manifest_invalid": "Manifesto da aplicação inválido", "app_no_upgrade": "Não existem aplicações para atualizar", "app_not_installed": "{app:s} não está instalada", "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette", "app_removed": "{app:s} removida com êxito", - "app_sources_fetch_failed": "Impossível obter os códigos fontes", + "app_sources_fetch_failed": "Impossível obter os ficheiros fonte", "app_unknown": "Aplicação desconhecida", - "app_upgrade_failed": "Unable to upgrade all apps", - "app_upgraded": "{app:s} atualizada com êxito", - "appslist_fetched": "Lista de aplicações processada com êxito", - "appslist_removed": "Lista de aplicações removida com êxito", + "app_upgrade_failed": "Impossível atualizar {app:s}", + "app_upgraded": "{app:s} atualizada com sucesso", + "appslist_fetched": "Lista de aplicações processada com sucesso", + "appslist_removed": "Lista de aplicações removida com sucesso", "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas", "appslist_unknown": "Lista de aplicaçoes desconhecida", "ask_current_admin_password": "Senha de administração atual", @@ -34,7 +34,7 @@ "backup_creating_archive": "A criar ficheiro de backup...", "backup_invalid_archive": "Arquivo de backup inválido", "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", - "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}", + "custom_app_url_required": "Deve fornecer um link para atualizar a sua aplicação personalizada {app:s}", "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", "domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", @@ -144,5 +144,23 @@ "yunohost_ca_creation_failed": "Incapaz criar o certificado de autoridade", "yunohost_configured": "YunoHost configurada com êxito", "yunohost_installing": "A instalar a YunoHost...", - "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'." + "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'.", + "app_incompatible": "A aplicação é incompatível com a sua versão de Yunohost", + "app_not_correctly_installed": "{app:s} parece não estar corretamente instalada", + "app_not_properly_removed": "{app:s} não foi corretamente removido", + "app_requirements_checking": "Verificando os pacotes necessários...", + "app_unsupported_remote_type": "Remoto tipo utilizado para a aplicação não suportado", + "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup", + "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path: s} )", + "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup", + "backup_archive_name_exists": "O nome do arquivo de backup já existe", + "backup_archive_open_failed": "Impossível abrir o arquivo de backup", + "backup_cleaning_failed": "Impossível apagar o diretório temporário de backup", + "backup_creation_failed": "A criação do backup falhou", + "backup_delete_error": "Impossível apagar '{path:s}'", + "backup_deleted": "O backup foi suprimido", + "backup_extracting_archive": "Extraindo arquivo de backup...", + "backup_hook_unknown": "Gancho de backup '{hook:s}' desconhecido", + "backup_nothings_done": "Não há nada para guardar", + "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas" } From 279ad6753fdeb08662f90e3a4db70dc10b3a8041 Mon Sep 17 00:00:00 2001 From: rokaz Date: Wed, 8 Feb 2017 23:03:56 +0100 Subject: [PATCH 0250/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (270 of 270 strings) --- locales/es.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index c5ead2908..16859f009 100644 --- a/locales/es.json +++ b/locales/es.json @@ -266,5 +266,10 @@ "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)", - "domains_available": "Dominios instalados:" + "domains_available": "Dominios instalados:", + "backup_archive_broken_link": "Acceso a la copia de seguridad imposible (enlace roto {path:s})", + "certmanager_domain_not_resolved_locally": "Su servidor Yunohost no consigue resolver el dominio {domain:s}. Esto puede suceder si modificó su registro DNS. Si es el caso, espere unas horas que se propague la modificación. Si el problema persiste, considere añadir {domain:s} a /etc/hosts. (Si sabe usted lo que está haciendo, use --no-checks para deshabilitar estas verificaciones.)", + "certmanager_acme_not_configured_for_domain": "El certificado para el dominio {domain:s} no parece instalado correctamente. Ejecute primero cert-install para este dominio.", + "certmanager_http_check_timeout": "Plazo expirado, el servidor no ha podido contactarse a si mismo a través de HTTP usando su dirección IP pública (dominio {domain:s} con ip {ip:s}). Puede ser debido a hairpinning o a una mala configuración del cortafuego/router al que está conectado su servidor.", + "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde." } From de4406565d75c3bd80a56bbcb85461750b638612 Mon Sep 17 00:00:00 2001 From: rokaz Date: Wed, 8 Feb 2017 23:22:49 +0100 Subject: [PATCH 0251/1066] [i18n] Translated using Weblate (French) Currently translated at 99.2% (268 of 270 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 6f1369b7a..82044cbeb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -111,7 +111,7 @@ "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", - "hook_list_by_invalid": "Propriété pour lister les scripts incorrects", + "hook_list_by_invalid": "Propriété invalide pour lister les scripts", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", From 19de060269f4e8e4d2587a24fe29af73ef162ab6 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Thu, 9 Feb 2017 07:39:01 +0100 Subject: [PATCH 0252/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (270 of 270 strings) --- locales/fr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 82044cbeb..a609a1d77 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -82,7 +82,7 @@ "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine. ", "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", @@ -105,13 +105,13 @@ "field_invalid": "Champ incorrect : « {:s} »", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Le pare-feu a été rechargé", - "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal", + "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", "format_datetime_short": "%d/%m/%Y %H:%M", "hook_argument_missing": "Argument manquant : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", - "hook_list_by_invalid": "Propriété invalide pour lister les scripts", + "hook_list_by_invalid": "La variable de tri de la liste des connexions est invalide", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", @@ -275,7 +275,7 @@ "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", - "backup_archive_broken_link": "Impossible d'accéder à l'archive de la sauvegarde (lien cassé à {path:s})", + "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path:s})", "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", From 347f33a61b0393f4cab56fe927c63adf102342b2 Mon Sep 17 00:00:00 2001 From: rokaz Date: Wed, 8 Feb 2017 23:13:54 +0100 Subject: [PATCH 0253/1066] [i18n] Translated using Weblate (English) Currently translated at 100.0% (270 of 270 strings) --- locales/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index fb1c58b43..62acc4e10 100644 --- a/locales/en.json +++ b/locales/en.json @@ -106,7 +106,7 @@ "field_invalid": "Invalid field '{:s}'", "firewall_reload_failed": "Unable to reload the firewall", "firewall_reloaded": "The firewall has been reloaded", - "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log", + "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", "format_datetime_short": "%m/%d/%Y %I:%M %p", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", @@ -261,14 +261,14 @@ "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", - "certmanager_hit_rate_limit":"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_cert_signing_failed": "Signing the new certificate failed", "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", - "certmanager_http_check_timeout" : "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", - "certmanager_couldnt_fetch_intermediate_cert" : "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", + "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", + "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } From d4fa3917cf8f41ca2a4fcba4813e2e63324d0c34 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Thu, 9 Feb 2017 00:00:58 +0100 Subject: [PATCH 0254/1066] [i18n] Translated using Weblate (German) Currently translated at 76.2% (206 of 270 strings) --- locales/de.json | 64 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/locales/de.json b/locales/de.json index 0fff3e0b4..d6562a2b1 100644 --- a/locales/de.json +++ b/locales/de.json @@ -81,7 +81,7 @@ "dyndns_registered": "DynDNS Domain erfolgreich registriert", "dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error:s}", "dyndns_unavailable": "DynDNS Subdomain ist nicht verfügbar", - "executing_command": "Führe Kommendo '{command:s}' aus...", + "executing_command": "Führe den Behfehl '{command:s}' aus...", "executing_script": "Skript '{script:s}' wird ausgeührt...", "extracting": "Wird entpackt...", "field_invalid": "Feld '{:s}' ist unbekannt", @@ -162,7 +162,7 @@ "service_added": "Service erfolgreich hinzugefügt", "service_already_started": "Der Dienst '{service:s}' läutt bereits", "service_already_stopped": "Dienst '{service:s}' wurde bereits gestoppt", - "service_cmd_exec_failed": "Kommando '{command:s}' kann nicht ausgeführt werden", + "service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden", "service_configuration_conflict": "Die Datei {file:s} wurde zwischenzeitlich verändert. Bitte übernehme die Änderungen manuell oder nutze die Option --force (diese wird alle Änderungen überschreiben).", "service_disable_failed": "Dienst '{service:s}' konnte nicht deaktiviert werden", "service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert", @@ -218,5 +218,63 @@ "package_not_installed": "Paket '{pkgname}' ist nicht installiert", "pattern_positive_number": "Muss eine positive Zahl sein", "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", - "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf" + "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", + "app_incompatible": "Die Anwendung ist mit deiner YunoHost Version inkompatibel", + "app_not_correctly_installed": "{app:s} scheint nicht richtig installiert worden zu sein", + "app_requirements_checking": "Überprüfe notwendige Pakete...", + "app_requirements_failed": "Anforderungen werden nicht erfüllt: {error}", + "app_requirements_unmeet": "Anforderungen werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", + "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", + "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", + "diagnostic_debian_version_error": "Debian Version konnte nicht abgerufen werden: {error}", + "diagnostic_monitor_disk_error": "Festplatten können nicht aufgelistet werden: {error}", + "diagnostic_monitor_network_error": "Netzwerk kann nicht angezeigt werden: {error}", + "diagnostic_monitor_system_error": "System kann nicht angezeigt werden: {error}", + "diagnostic_no_apps": "Keine Anwendung ist installiert", + "domains_available": "Verfügbare Domains:", + "dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", + "dyndns_no_domain_registered": "Es wurde keine Domain mit DynDNS registriert", + "ldap_init_failed_to_create_admin": "Die LDAP Initialisierung konnte keinen admin Benutzer erstellen", + "mailbox_used_space_dovecot_down": "Der Dovecot Mailbox Dienst muss gestartet sein, wenn du den von der Mailbox belegten Speicher angezeigen lassen willst", + "package_unknown": "Unbekanntes Paket '{pkgname}'", + "service_conf_file_backed_up": "Von der Konfigurationsdatei {conf} wurde ein Backup in {backup} erstellt", + "service_conf_file_copy_failed": "Die neue Konfigurationsdatei konnte von {new} nach {conf} nicht kopiert werden", + "service_conf_file_manually_modified": "Die Konfigurationsdatei {conf} wurde manuell verändert und wird nicht aktualisiert", + "service_conf_file_manually_removed": "Die Konfigurationsdatei {conf} wurde manuell entfern und wird nicht erstellt", + "service_conf_file_not_managed": "Die Konfigurationsdatei {conf} wurde noch nicht verwaltet und wird nicht aktualisiert", + "service_conf_file_remove_failed": "Die Konfigurationsdatei {conf} konnte nicht entfernt werden", + "service_conf_file_removed": "Die Konfigurationsdatei {conf} wurde entfernt", + "service_conf_file_updated": "Die Konfigurationsdatei {conf} wurde aktualisiert", + "service_conf_updated": "Die Konfigurationsdatei wurde für den Service {service} aktualisiert", + "service_conf_would_be_updated": "Die Konfigurationsdatei sollte für den Service {service} aktualisiert werden", + "ssowat_persistent_conf_read_error": "Ein Fehler ist aufgetreten, als die persistente SSOwat Konfiguration eingelesen wurde {error:s} Bearbeite die persistente Datei /etc/ssowat/conf.json , um die JSON syntax zu korregieren", + "ssowat_persistent_conf_write_error": "Ein Fehler ist aufgetreten, als die persistente SSOwat Konfiguration gespeichert wurde {error:s} Bearbeite die persistente Datei /etc/ssowat/conf.json , um die JSON syntax zu korregieren", + "certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain:s} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)", + "certmanager_domain_unknown": "Unbekannte Domain {domain:s}", + "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain:s} is kein selbstsigniertes Zertifikat. Bist du dir sicher, dass du es ersetzen willst? (Benutze --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Es scheint so als wäre die Aktivierung des Zertifikats für die Domain {domain:s} fehlgeschlagen...", + "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain {domain:s} wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", + "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft in Kürze ab! Benutze --force um diese Nachricht zu umgehen", + "certmanager_domain_http_not_working": "Es scheint so, dass die Domain {domain:s} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS und nginx Konfiguration in Ordnung ist", + "certmanager_error_no_A_record": "Kein DNS 'A' Eintrag für die Domain {domain:s} gefunden. Dein Domainname muss auf diese Maschine weitergeleitet werden, um ein Let's Encrypt Zertifikat installieren zu können! (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )", + "certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS 'A' Eintrag der Domain {domain:s} unterscheidet sich von dieser Server-IP. Wenn du gerade deinen A Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (du kannst die DNS Propagation mittels Website überprüfen) (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )", + "certmanager_domain_not_resolved_locally": "Die Domain {domain:s} konnte von innerhalb des Yunohost-Servers nicht aufgelöst werden. Das kann passieren, wenn du den DNS Eintrag vor Kurzem verändert hast. Falls dies der Fall ist, warte bitte ein paar Stunden, damit die Änderungen wirksam werden. Wenn der Fehler bestehen bleibt, ziehe in Betracht die Domain {domain:s} in /etc/hosts einzutragen. (Wenn du weißt was du tust, benutze --no-checks , um diese Nachricht zu umgehen. )", + "certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain:s} zu öffnen (Datei: {file:s}), Grund: {reason:s}", + "certmanager_cert_install_success_selfsigned": "Ein selbstsigniertes Zertifikat für die Domain {domain:s} wurde erfolgreich installiert!", + "certmanager_cert_install_success": "Für die Domain {domain:s} wurde erfolgreich ein Let's Encrypt installiert!", + "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert!", + "certmanager_old_letsencrypt_app_detected": "\nYunohost hat erkannt, dass eine Version von 'letsencrypt' installiert ist, die mit den neuen, integrierten Zertifikatsmanagement-Features in Yunohost kollidieren. Wenn du die neuen Features nutzen willst, führe die folgenden Befehle aus:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nAnm.: Diese Befehle werden die selbstsignierten und Let's Encrypt Zertifikate aller Domains neu installieren", + "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit schon zu viele Zertifikate für die exakt gleiche Domain {domain:s} ausgestellt. Bitte versuche es später nochmal. Besuche https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", + "certmanager_cert_signing_failed": "Signieren des neuen Zertifikats ist fehlgeschlagen", + "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden", + "certmanager_conflicting_nginx_file": "Die Domain konnte nicht für die ACME challenge vorbereitet werden: Die nginx Konfigurationsdatei {filepath:s} verursacht Probleme und sollte vorher entfernt werden", + "domain_cannot_remove_main": "Die primäre Domain konnten nicht entfernt werden. Lege zuerst einen neue primäre Domain fest", + "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file:s})", + "certmanager_acme_not_configured_for_domain": "Das Zertifikat für die Domain {domain:s} scheint nicht richtig installiert zu sein. Bitte führe den Befehl cert-install für diese Domain nochmals aus.", + "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht analysiert werden (Datei: {file:s})", + "app_package_need_update": "Es ist notwendig das Paket zu aktualisieren, um Aktualisierungen für YunoHost zu erhalten", + "service_regenconf_dry_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server {service} notwendig sind...", + "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...", + "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine misskonfigurierte Firewall/Router deines Servers dafür verantwortlich.", + "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut." } From 9d891b524dcbc438bad3cafd5a342aafc0842daf Mon Sep 17 00:00:00 2001 From: bricabraque Date: Wed, 8 Feb 2017 16:40:14 +0100 Subject: [PATCH 0255/1066] [i18n] Translated using Weblate (Italian) Currently translated at 12.2% (33 of 270 strings) --- locales/it.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index e6285eaf7..92a24365b 100644 --- a/locales/it.json +++ b/locales/it.json @@ -27,5 +27,17 @@ "system_username_exists": "Nome utente esiste già negli utenti del sistema", "unrestore_app": "Applicazione '{app:s}' non verrà ripristinato", "upgrading_packages": "Aggiornamento dei pacchetti...", - "user_deleted": "Utente cancellato con successo" + "user_deleted": "Utente cancellato con successo", + "admin_password": "Password dell'amministrazione", + "admin_password_change_failed": "Non è possibile cambiare la password", + "admin_password_changed": "La password dell'amministrazione è stata cambiata", + "app_incompatible": "L'app non è compatibile con la tua versione di Yunohost", + "app_install_files_invalid": "Non sono validi i file di installazione", + "app_location_already_used": "Una app è già installata in questa postazione", + "app_location_install_failed": "Non è possibile installare l'app in questa postazione", + "app_manifest_invalid": "", + "app_no_upgrade": "Nessun app da aggiornare", + "app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente", + "app_not_properly_removed": "{app:s} non è stata correttamente rimossa", + "action_invalid": "L'azione '{action:s}' non è valida" } From d05cf26b40f6c68409bb0a9c4bcd8fcbdb8aeeea Mon Sep 17 00:00:00 2001 From: Trollken Date: Wed, 8 Feb 2017 01:19:02 +0100 Subject: [PATCH 0256/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 42.2% (114 of 270 strings) --- locales/pt.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index 3f7f417f5..b0260b73a 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -102,7 +102,7 @@ "pattern_listname": "Apenas são permitidos caracteres alfanuméricos e travessões", "pattern_password": "Deve ter no mínimo 3 caracteres", "pattern_port": "Deve ser um número de porta válido (entre 0-65535)", - "pattern_username": "Must be lower-case alphanumeric and underscore characters only", + "pattern_username": "Devem apenas ser carácteres minúsculos alfanuméricos e subtraços", "restore_confirm_yunohost_installed": "Quer mesmo restaurar um sistema já instalado? [{answers:s}]", "service_add_failed": "Incapaz adicionar serviço '{service:s}'", "service_added": "Serviço adicionado com êxito", @@ -151,7 +151,7 @@ "app_requirements_checking": "Verificando os pacotes necessários...", "app_unsupported_remote_type": "Remoto tipo utilizado para a aplicação não suportado", "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup", - "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path: s} )", + "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path:s} )", "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup", "backup_archive_name_exists": "O nome do arquivo de backup já existe", "backup_archive_open_failed": "Impossível abrir o arquivo de backup", From f49809f15fccf8e461fd26022ec21554bc15a3a0 Mon Sep 17 00:00:00 2001 From: rokaz Date: Wed, 8 Feb 2017 23:36:36 +0100 Subject: [PATCH 0257/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (270 of 270 strings) --- locales/es.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/es.json b/locales/es.json index 16859f009..0d2bed3cc 100644 --- a/locales/es.json +++ b/locales/es.json @@ -81,7 +81,7 @@ "domain_dyndns_invalid": "Dominio no válido para usar con DynDNS", "domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido", "domain_exists": "El dominio ya existe", - "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminarlo.", + "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminar el dominio. ", "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zona del DNS ya existe", "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]", @@ -110,7 +110,7 @@ "hook_choice_invalid": "Selección inválida '{:s}'", "hook_exec_failed": "No se puede ejecutar el script: {path:s}", "hook_exec_not_terminated": "La ejecución del script no ha terminado: {path:s}", - "hook_list_by_invalid": "Propiedad no válida para listar por hook", + "hook_list_by_invalid": "Enumerar los hooks por validez", "hook_name_unknown": "Nombre de hook desconocido '{name:s}'", "installation_complete": "Instalación finalizada", "installation_failed": "No se pudo realizar la instalación", @@ -266,7 +266,7 @@ "domain_cannot_remove_main": "No se puede eliminar el dominio principal. Primero debe establecer un nuevo dominio principal", "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)", - "domains_available": "Dominios instalados:", + "domains_available": "Dominios disponibles:", "backup_archive_broken_link": "Acceso a la copia de seguridad imposible (enlace roto {path:s})", "certmanager_domain_not_resolved_locally": "Su servidor Yunohost no consigue resolver el dominio {domain:s}. Esto puede suceder si modificó su registro DNS. Si es el caso, espere unas horas que se propague la modificación. Si el problema persiste, considere añadir {domain:s} a /etc/hosts. (Si sabe usted lo que está haciendo, use --no-checks para deshabilitar estas verificaciones.)", "certmanager_acme_not_configured_for_domain": "El certificado para el dominio {domain:s} no parece instalado correctamente. Ejecute primero cert-install para este dominio.", From e210dc17ff0e7ae82f79bb77db0230d30a535494 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Fri, 10 Feb 2017 21:33:10 +0100 Subject: [PATCH 0258/1066] [i18n] Translated using Weblate (German) Currently translated at 100.0% (270 of 270 strings) --- locales/de.json | 106 ++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/locales/de.json b/locales/de.json index d6562a2b1..7f01ab978 100644 --- a/locales/de.json +++ b/locales/de.json @@ -2,7 +2,7 @@ "action_invalid": "Ungültige Aktion '{action:s}'", "admin_password": "Verwaltungspasswort", "admin_password_change_failed": "Passwort kann nicht geändert werden", - "admin_password_changed": "Verwaltungspasswort wurde erfolgreich geändert", + "admin_password_changed": "Das Verwaltungspasswort wurde erfolgreich geändert", "app_already_installed": "{app:s} ist schon installiert", "app_argument_choice_invalid": "Invalide Auswahl für Argument '{name:s}'. Muss einer der folgenden Werte sein {choices:s}", "app_argument_invalid": "Das Argument '{name:s}' hat einen falschen Wert: {error:s}", @@ -19,17 +19,17 @@ "app_removed": "{app:s} wurde erfolgreich entfernt", "app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden", "app_unknown": "Unbekannte App", - "app_upgrade_failed": "Apps konnten nicht aktualisiert werden", + "app_upgrade_failed": "{app:s} konnte nicht aktualisiert werden", "app_upgraded": "{app:s} wurde erfolgreich aktualisiert", - "appslist_fetched": "Liste der Apps wurde erfolgreich heruntergelanden", - "appslist_removed": "Appliste erfolgreich entfernt", - "appslist_retrieve_error": "Entfernte App Liste kann nicht gezogen werden", - "appslist_unknown": "Unbekannte App Liste", + "appslist_fetched": "Appliste wurde erfolgreich heruntergelanden", + "appslist_removed": "Appliste wurde erfolgreich entfernt", + "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden", + "appslist_unknown": "Unbekannte Appliste", "ask_current_admin_password": "Derzeitiges Verwaltungspasswort", "ask_email": "E-Mail Adresse", "ask_firstname": "Vorname", "ask_lastname": "Nachname", - "ask_list_to_remove": "Liste enternen", + "ask_list_to_remove": "Liste entfernen", "ask_main_domain": "Hauptdomain", "ask_new_admin_password": "Neues Verwaltungskennwort", "ask_password": "Passwort", @@ -40,74 +40,74 @@ "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits", "backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name:s}' gefunden", "backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen", - "backup_cleaning_failed": "Verzeichnis von temporäre Sicherungsdaten konnte nicht geleert werden", + "backup_cleaning_failed": "Verzeichnis der temporären Sicherungsdaten konnte nicht geleert werden", "backup_created": "Datensicherung komplett", "backup_creating_archive": "Datensicherung wird erstellt...", "backup_delete_error": "Pfad '{path:s}' konnte nicht gelöscht werden", - "backup_deleted": "Datensicherung erfolgreich gelöscht", + "backup_deleted": "Datensicherung wurde entfernt", "backup_extracting_archive": "Entpacke Sicherungsarchiv...", "backup_hook_unknown": "Datensicherungshook '{hook:s}' unbekannt", "backup_invalid_archive": "Ungültige Datensicherung", "backup_nothings_done": "Es gibt keine Änderungen zur Speicherung", - "backup_output_directory_forbidden": "Verbotenes Ausgabeverzeichnis", + "backup_output_directory_forbidden": "Verbotenes Ausgabeverzeichnis. Datensicherung können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden", "backup_output_directory_not_empty": "Ausgabeordner ist nicht leer", "backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden", "backup_running_app_script": "Datensicherung für App '{app:s}' wurd durchgeführt...", "backup_running_hooks": "Datensicherunghook wird ausgeführt...", - "custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren", + "custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben", "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", - "domain_created": "Domain erfolgreich erzeugt", + "domain_created": "Die Domain wurde angelegt", "domain_creation_failed": "Konnte Domain nicht erzeugen", - "domain_deleted": "Domain erfolgreich gelöscht", + "domain_deleted": "Die Domain wurde gelöscht", "domain_deletion_failed": "Konnte Domain nicht löschen", - "domain_dyndns_already_subscribed": "Du hast dich schon für einen DynDNS-Domain angemeldet", + "domain_dyndns_already_subscribed": "Du hast bereits eine DynDNS-Domain abonniert", "domain_dyndns_invalid": "Domain nicht mittels DynDNS nutzbar", "domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain", "domain_exists": "Die Domain existiert bereits", - "domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte zuerst die App deinstallieren und erst dann die Domain löschen..", + "domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte deinstalliere zuerst die App, bevor du die Domain löschst.. ", "domain_unknown": "Unbekannte Domain", "domain_zone_exists": "DNS Zonen Datei existiert bereits", "domain_zone_not_found": "DNS Zonen Datei kann nicht für Domäne {:s} gefunden werden", - "done": "Erledigt.", + "done": "Erledigt", "downloading": "Wird heruntergeladen...", - "dyndns_cron_installed": "DynDNS Cronjob erfolgreich installiert", - "dyndns_cron_remove_failed": "DynDNS Cronjob konnte nicht entfernt werden", - "dyndns_cron_removed": "DynDNS Cronjob wurde erfolgreich gelöscht", + "dyndns_cron_installed": "DynDNS Cronjob erfolgreich angelegt", + "dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte nicht entfernt werden", + "dyndns_cron_removed": "Der DynDNS Cronjob wurde gelöscht", "dyndns_ip_update_failed": "IP Adresse konnte nicht für DynDNS aktualisiert werden", - "dyndns_ip_updated": "IP Adresse wurde erfolgreich für DynDNS aktualisiert", + "dyndns_ip_updated": "Deine IP Adresse wurde bei DynDNS aktualisiert", "dyndns_key_generating": "DNS Schlüssel wird generiert, das könnte eine Weile dauern...", - "dyndns_registered": "DynDNS Domain erfolgreich registriert", + "dyndns_registered": "Deine DynDNS Domain wurde registriert", "dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error:s}", "dyndns_unavailable": "DynDNS Subdomain ist nicht verfügbar", "executing_command": "Führe den Behfehl '{command:s}' aus...", "executing_script": "Skript '{script:s}' wird ausgeührt...", "extracting": "Wird entpackt...", "field_invalid": "Feld '{:s}' ist unbekannt", - "firewall_reload_failed": "Firewall konnte nicht neu geladen werden", - "firewall_reloaded": "Firewall erfolgreich neu geladen", + "firewall_reload_failed": "Die Firewall konnte nicht neu geladen werden", + "firewall_reloaded": "Die Firewall wurde neu geladen", "firewall_rules_cmd_failed": "Einzelne Firewallregeln konnten nicht übernommen werden. Mehr Informationen sind im Log zu finden.", - "format_datetime_short": "%m/%d/%Y %I:%M %p", + "format_datetime_short": "%d/%m/%Y %I:%M %p", "hook_argument_missing": "Fehlend Argument '{:s}'", "hook_choice_invalid": "ungültige Wahl '{:s}'", - "hook_exec_failed": "Skriptausführung fehlgeschlagen", - "hook_exec_not_terminated": "Skriptausführung noch nicht beendet", + "hook_exec_failed": "Skriptausführung fehlgeschlagen: {path:s}", + "hook_exec_not_terminated": "Skriptausführung noch nicht beendet: {path:s}", "hook_list_by_invalid": "Ungültiger Wert zur Anzeige von Hooks", "hook_name_unknown": "Hook '{name:s}' ist nicht bekannt", "installation_complete": "Installation vollständig", "installation_failed": "Installation fehlgeschlagen", - "ip6tables_unavailable": "ip6tables kann nicht verwendet werden. Du befindest dich entweder in einem Container, oder es wird nicht vom Kernel unterstützt.", - "iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container, oder es wird nicht vom Kernel unterstützt.", - "ldap_initialized": "LDAP erfolgreich initialisiert", + "ip6tables_unavailable": "ip6tables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt", + "iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt", + "ldap_initialized": "LDAP wurde initialisiert", "license_undefined": "Undeiniert", "mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden", "mail_domain_unknown": "Unbekannte Mail Domain '{domain:s}'", "mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden", - "maindomain_change_failed": "Hauptdomain konnte nicht geändert werden", - "maindomain_changed": "Hauptdomain wurde erfolgreich geändert", - "monitor_disabled": "Servermonitoring erfolgreich deaktiviert", - "monitor_enabled": "Servermonitoring erfolgreich aktiviert", + "maindomain_change_failed": "Die Hauptdomain konnte nicht geändert werden", + "maindomain_changed": "Die Hauptdomain wurde geändert", + "monitor_disabled": "Das Servermonitoring wurde erfolgreich deaktiviert", + "monitor_enabled": "Das Servermonitoring wurde aktiviert", "monitor_glances_con_failed": "Verbindung mit Glances nicht möglich", "monitor_not_enabled": "Servermonitoring ist nicht aktiviert", "monitor_period_invalid": "Falscher Zeitraum", @@ -117,7 +117,7 @@ "mountpoint_unknown": "Unbekannten Einhängepunkt", "mysql_db_creation_failed": "MySQL Datenbankerzeugung fehlgeschlagen", "mysql_db_init_failed": "MySQL Datenbankinitialisierung fehlgeschlagen", - "mysql_db_initialized": "MySQL Datenbank erfolgreich initialisiert", + "mysql_db_initialized": "Die MySQL Datenbank wurde initialisiert", "network_check_mx_ko": "Es ist kein DNS MX Eintrag vorhanden", "network_check_smtp_ko": "Ausgehender Mailverkehr (SMTP Port 25) scheint in deinem Netzwerk blockiert zu sein", "network_check_smtp_ok": "Ausgehender Mailverkehr (SMTP Port 25) ist blockiert", @@ -149,7 +149,7 @@ "restore_action_required": "Du musst etwas zum Wiederherstellen auswählen", "restore_already_installed_app": "Es ist bereits eine App mit der ID '{app:s}' installiet", "restore_app_failed": "App '{app:s}' konnte nicht wiederhergestellt werden", - "restore_cleaning_failed": "Temporäres Wiederherstellungsverzeichnis konnte nicht geleert werden", + "restore_cleaning_failed": "Das temporäre Wiederherstellungsverzeichnis konnte nicht geleert werden", "restore_complete": "Wiederherstellung abgeschlossen", "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers:s}]", "restore_failed": "System kann nicht Wiederhergestellt werden", @@ -159,29 +159,29 @@ "restore_running_hooks": "Wiederherstellung wird gestartet...", "service_add_configuration": "Füge Konfigurationsdatei {file:s} hinzu", "service_add_failed": "Dienst '{service:s}' kann nicht hinzugefügt werden", - "service_added": "Service erfolgreich hinzugefügt", - "service_already_started": "Der Dienst '{service:s}' läutt bereits", + "service_added": "Der Service '{service:s}' wurde erfolgreich hinzugefügt", + "service_already_started": "Der Dienst '{service:s}' läuft bereits", "service_already_stopped": "Dienst '{service:s}' wurde bereits gestoppt", "service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden", "service_configuration_conflict": "Die Datei {file:s} wurde zwischenzeitlich verändert. Bitte übernehme die Änderungen manuell oder nutze die Option --force (diese wird alle Änderungen überschreiben).", "service_disable_failed": "Dienst '{service:s}' konnte nicht deaktiviert werden", "service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert", "service_enable_failed": "Dienst '{service:s}' konnte nicht aktiviert werden", - "service_enabled": "Dienst '{service:s}' erfolgreich aktiviert", + "service_enabled": "Der Dienst '{service:s}' wurde erfolgreich aktiviert", "service_no_log": "Für den Dienst '{service:s}' kann kein Log angezeigt werden", "service_remove_failed": "Dienst '{service:s}' konnte nicht entfernt werden", - "service_removed": "Dienst erfolgreich enternt", + "service_removed": "Der Dienst '{service:s}' wurde erfolgreich entfernt", "service_start_failed": "Dienst '{service:s}' konnte nicht gestartet werden", - "service_started": "der Dienst '{service:s}' wurde erfolgreich gestartet", + "service_started": "Der Dienst '{service:s}' wurde erfolgreich gestartet", "service_status_failed": "Status von '{service:s}' kann nicht festgestellt werden", "service_stop_failed": "Dienst '{service:s}' kann nicht gestoppt werden", - "service_stopped": "Dienst '{service:s}' wurde erfolgreich beendet", + "service_stopped": "Der Dienst '{service:s}' wurde erfolgreich beendet", "service_unknown": "Unbekannte Dienst '{service:s}'", "services_configured": "Konfiguration erfolgreich erstellt", "show_diff": "Es gibt folgende Änderungen:\n{diff:s}", - "ssowat_conf_generated": "Konfiguration von SSOwat erfolgreich", - "ssowat_conf_updated": "Persistente SSOwat Einstellung erfolgreich aktualisiert", - "system_upgraded": "System wurde erfolgreich aktualisiert", + "ssowat_conf_generated": "Die Konfiguration von SSOwat war erfolgreich", + "ssowat_conf_updated": "Die persistente SSOwat Einstellung wurde aktualisiert", + "system_upgraded": "Das System wurde aktualisiert", "system_username_exists": "Der Benutzername existiert bereits", "unbackup_app": "App '{app:s}' konnte nicht gespeichert werden", "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten", @@ -189,31 +189,31 @@ "unlimit": "Kein Kontingent", "unrestore_app": "App '{app:s}' kann nicht Wiederhergestellt werden", "update_cache_failed": "Konnte APT cache nicht aktualisieren", - "updating_apt_cache": "Liste der verfügbaren Pakete wird aktualisiert...", + "updating_apt_cache": "Die Liste der verfügbaren Pakete wird aktualisiert...", "upgrade_complete": "Upgrade vollständig", "upgrading_packages": "Pakete werden aktualisiert...", "upnp_dev_not_found": "Es konnten keine UPnP Geräte gefunden werden", - "upnp_disabled": "UPnP wurde erfolgreich deaktiviert", + "upnp_disabled": "UPnP wurde deaktiviert", "upnp_enabled": "UPnP wurde aktiviert", "upnp_port_open_failed": "UPnP Ports konnten nicht geöffnet werden", - "user_created": "Benutzer erfolgreich erstellt", + "user_created": "Der Benutzer wurde erstellt", "user_creation_failed": "Nutzer konnte nicht erstellt werden", - "user_deleted": "Benutzer wurde erfolgreich entfernt", + "user_deleted": "Der Benutzer wurde entfernt", "user_deletion_failed": "Nutzer konnte nicht gelöscht werden", "user_home_creation_failed": "Benutzer Home konnte nicht erstellt werden", "user_info_failed": "Nutzerinformationen können nicht angezeigt werden", - "user_unknown": "Unbekannter Benutzer", + "user_unknown": "Unbekannter Benutzer: {user:s}", "user_update_failed": "Benutzer kann nicht aktualisiert werden", - "user_updated": "Benutzer wurde erfolgreich aktualisiert", + "user_updated": "Der Benutzer wurde aktualisiert", "yunohost_already_installed": "YunoHost ist bereits installiert", "yunohost_ca_creation_failed": "Zertifikatsstelle konnte nicht erstellt werden", - "yunohost_configured": "YunoHost wurde erfolgreich konfiguriert", + "yunohost_configured": "YunoHost wurde konfiguriert", "yunohost_installing": "YunoHost wird installiert...", - "yunohost_not_installed": "Die YunoHost ist unvollständig. Bitte 'yunohost tools postinstall' ausführen.", + "yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", "app_not_properly_removed": "{app:s} wurde nicht ordnungsgemäß entfernt", "service_regenconf_failed": "Konnte die Konfiguration für folgende Dienste nicht neu erzeugen: {services}", "not_enough_disk_space": "Zu wenig Speicherplatz unter '{path:s}' verfügbar", - "backup_creation_failed": "Erzeugung des Backups fehlgeschlagen", + "backup_creation_failed": "Erstellen des Backups fehlgeschlagen", "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell", "package_not_installed": "Paket '{pkgname}' ist nicht installiert", "pattern_positive_number": "Muss eine positive Zahl sein", From fe4e64d467ab759b3f568abae07383a4cd8bdf5f Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Fri, 10 Feb 2017 21:39:10 +0100 Subject: [PATCH 0259/1066] [i18n] Translated using Weblate (German) Currently translated at 100.0% (270 of 270 strings) --- locales/de.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/locales/de.json b/locales/de.json index 7f01ab978..6898bcb92 100644 --- a/locales/de.json +++ b/locales/de.json @@ -128,7 +128,7 @@ "no_restore_script": "Es konnte kein Wiederherstellungsskript für '{app:s}' gefunden werden", "no_such_conf_file": "Datei {file:s}: konnte nicht kopiert werden, da diese nicht existiert", "packages_no_upgrade": "Es müssen keine Pakete aktualisiert werden", - "packages_upgrade_critical_later": "Wichtiges Paket ({packages:s}) wird später aktualisiert", + "packages_upgrade_critical_later": "Ein wichtiges Paket ({packages:s}) wird später aktualisiert", "packages_upgrade_failed": "Es konnten nicht alle Pakete aktualisiert werden", "path_removal_failed": "Pfad {:s} konnte nicht entfernt werden", "pattern_backup_archive_name": "Ein gültiger Dateiname kann nur aus alphanumerischen und -_. bestehen", @@ -142,9 +142,9 @@ "pattern_port": "Es muss ein valider Port (zwischen 0 und 65535) angegeben werden", "pattern_port_or_range": "Muss ein valider Port (z.B. 0-65535) oder ein Bereich (z.B. 100:200) sein", "pattern_username": "Darf nur aus klein geschriebenen alphanumerischen Zeichen und Unterstrichen bestehen", - "port_already_closed": "Port {port:d} wurde bereits für {ip_version:s} Verbindungen geschlossen", + "port_already_closed": "Der Port {port:d} wurde bereits für {ip_version:s} Verbindungen geschlossen", "port_already_opened": "Der Port {port:d} wird bereits von {ip_version:s} benutzt", - "port_available": "Port {port:d} ist verfügbar", + "port_available": "Der Port {port:d} ist verfügbar", "port_unavailable": "Der Port {port:d} ist nicht verfügbar", "restore_action_required": "Du musst etwas zum Wiederherstellen auswählen", "restore_already_installed_app": "Es ist bereits eine App mit der ID '{app:s}' installiet", @@ -158,25 +158,25 @@ "restore_running_app_script": "Wiederherstellung wird ausfeührt für App '{app:s}'...", "restore_running_hooks": "Wiederherstellung wird gestartet...", "service_add_configuration": "Füge Konfigurationsdatei {file:s} hinzu", - "service_add_failed": "Dienst '{service:s}' kann nicht hinzugefügt werden", + "service_add_failed": "Der Dienst '{service:s}' kann nicht hinzugefügt werden", "service_added": "Der Service '{service:s}' wurde erfolgreich hinzugefügt", "service_already_started": "Der Dienst '{service:s}' läuft bereits", "service_already_stopped": "Dienst '{service:s}' wurde bereits gestoppt", "service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden", "service_configuration_conflict": "Die Datei {file:s} wurde zwischenzeitlich verändert. Bitte übernehme die Änderungen manuell oder nutze die Option --force (diese wird alle Änderungen überschreiben).", - "service_disable_failed": "Dienst '{service:s}' konnte nicht deaktiviert werden", + "service_disable_failed": "Der Dienst '{service:s}' konnte nicht deaktiviert werden", "service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert", - "service_enable_failed": "Dienst '{service:s}' konnte nicht aktiviert werden", + "service_enable_failed": "Der Dienst '{service:s}' konnte nicht aktiviert werden", "service_enabled": "Der Dienst '{service:s}' wurde erfolgreich aktiviert", "service_no_log": "Für den Dienst '{service:s}' kann kein Log angezeigt werden", - "service_remove_failed": "Dienst '{service:s}' konnte nicht entfernt werden", + "service_remove_failed": "Der Dienst '{service:s}' konnte nicht entfernt werden", "service_removed": "Der Dienst '{service:s}' wurde erfolgreich entfernt", - "service_start_failed": "Dienst '{service:s}' konnte nicht gestartet werden", + "service_start_failed": "Der Dienst '{service:s}' konnte nicht gestartet werden", "service_started": "Der Dienst '{service:s}' wurde erfolgreich gestartet", - "service_status_failed": "Status von '{service:s}' kann nicht festgestellt werden", - "service_stop_failed": "Dienst '{service:s}' kann nicht gestoppt werden", + "service_status_failed": "Der Status von '{service:s}' kann nicht festgestellt werden", + "service_stop_failed": "Der Dienst '{service:s}' kann nicht gestoppt werden", "service_stopped": "Der Dienst '{service:s}' wurde erfolgreich beendet", - "service_unknown": "Unbekannte Dienst '{service:s}'", + "service_unknown": "Unbekannter Dienst '{service:s}'", "services_configured": "Konfiguration erfolgreich erstellt", "show_diff": "Es gibt folgende Änderungen:\n{diff:s}", "ssowat_conf_generated": "Die Konfiguration von SSOwat war erfolgreich", @@ -212,10 +212,10 @@ "yunohost_not_installed": "YunoHost ist nicht oder unvollständig installiert worden. Bitte 'yunohost tools postinstall' ausführen", "app_not_properly_removed": "{app:s} wurde nicht ordnungsgemäß entfernt", "service_regenconf_failed": "Konnte die Konfiguration für folgende Dienste nicht neu erzeugen: {services}", - "not_enough_disk_space": "Zu wenig Speicherplatz unter '{path:s}' verfügbar", + "not_enough_disk_space": "Zu wenig freier Speicherplatz unter '{path:s}' verfügbar", "backup_creation_failed": "Erstellen des Backups fehlgeschlagen", "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell", - "package_not_installed": "Paket '{pkgname}' ist nicht installiert", + "package_not_installed": "Das Paket '{pkgname}' ist nicht installiert", "pattern_positive_number": "Muss eine positive Zahl sein", "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", From 77046715ffc577d58d6696e29455a50937a6368d Mon Sep 17 00:00:00 2001 From: bricabraque Date: Sat, 11 Feb 2017 10:17:47 +0100 Subject: [PATCH 0260/1066] [i18n] Translated using Weblate (Italian) Currently translated at 19.6% (53 of 270 strings) --- locales/it.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 92a24365b..3e33edb5d 100644 --- a/locales/it.json +++ b/locales/it.json @@ -39,5 +39,25 @@ "app_no_upgrade": "Nessun app da aggiornare", "app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente", "app_not_properly_removed": "{app:s} non è stata correttamente rimossa", - "action_invalid": "L'azione '{action:s}' non è valida" + "action_invalid": "L'azione '{action:s}' non è valida", + "app_removed": "{app:s} è stata rimossa", + "app_sources_fetch_failed": "Non è possibile recuperare i file sorgenti", + "app_upgrade_failed": "Non è possibile aggiornare {app:s}", + "app_upgraded": "{app:s} è stata aggiornata", + "appslist_fetched": "La lista delle app è stata recuperata", + "appslist_removed": "La lista delle app è stata eliminata", + "app_package_need_update": "Il pacchetto della app deve esser aggiornato per seguire le modifiche di Yunohost", + "app_requirements_checking": "Controllo dei pacchetti necessari...", + "app_requirements_failed": "Non è possibile rispondere ai requisiti: {error}", + "app_requirements_unmeet": "Non sono soddisfatti i requisiti, il pacchetto {pkgname} ({version}) deve esser {spec}", + "appslist_unknown": "Lista sconosciuta", + "ask_current_admin_password": "Password dell'amministrazione attuale", + "ask_firstname": "Nome", + "ask_lastname": "Cognome", + "ask_list_to_remove": "Lista da eliminare", + "ask_main_domain": "Dominio principale", + "ask_new_admin_password": "Nuova password dell'amministrazione", + "backup_action_required": "Devi specificare qualcosa da salvare", + "backup_app_failed": "Non è possibile fare il backup della app '{app:s}'", + "backup_archive_app_not_found": "L'app '{app:s}' non è stata trovata nel archivio di backup" } From 2735de875fe0dfec4e65dae971a773bdf11b1898 Mon Sep 17 00:00:00 2001 From: Fabian Gruber Date: Sun, 12 Feb 2017 00:16:53 +0100 Subject: [PATCH 0261/1066] [i18n] Translated using Weblate (German) Currently translated at 100.0% (271 of 271 strings) --- locales/de.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 6898bcb92..4200d6c50 100644 --- a/locales/de.json +++ b/locales/de.json @@ -23,7 +23,7 @@ "app_upgraded": "{app:s} wurde erfolgreich aktualisiert", "appslist_fetched": "Appliste wurde erfolgreich heruntergelanden", "appslist_removed": "Appliste wurde erfolgreich entfernt", - "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden", + "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden: {error}", "appslist_unknown": "Unbekannte Appliste", "ask_current_admin_password": "Derzeitiges Verwaltungspasswort", "ask_email": "E-Mail Adresse", @@ -276,5 +276,6 @@ "service_regenconf_dry_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server {service} notwendig sind...", "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...", "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine misskonfigurierte Firewall/Router deines Servers dafür verantwortlich.", - "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut." + "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut.", + "appslist_retrieve_bad_format": "Die empfangene Datei ist keine gültige Appliste" } From 26bf985d6d25e7429a1998b65bcdb6aa1c8617e6 Mon Sep 17 00:00:00 2001 From: Lapineige Date: Tue, 14 Feb 2017 14:06:27 +0100 Subject: [PATCH 0262/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (272 of 272 strings) --- locales/fr.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index a609a1d77..c6f93a8c8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -32,7 +32,7 @@ "app_upgraded": "{app:s} a été mis à jour", "appslist_fetched": "La liste d'applications a été récupérée", "appslist_removed": "La liste d'applications a été supprimée", - "appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante", + "appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante: {error}", "appslist_unknown": "Liste d'applications inconnue", "ask_current_admin_password": "Mot de passe d'administration actuel", "ask_email": "Adresse courriel", @@ -82,7 +82,7 @@ "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine. ", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine", "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", @@ -279,5 +279,7 @@ "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", - "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement." + "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", + "appslist_retrieve_bad_format": "Le fichier récupéré n'est pas une liste d'applications valide", + "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte" } From 087cdb961c0c2dc550fd79dab20a6983a8a1b3e7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 25 Feb 2017 22:04:49 +0100 Subject: [PATCH 0263/1066] [enh] upgrade ciphers suit to more secure ones --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 854ff3faf..45961633f 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -26,7 +26,7 @@ server { ssl_session_cache shared:SSL:50m; ssl_prefer_server_ciphers on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers ALL:!aNULL:!eNULL:!LOW:!EXP:!RC4:!3DES:+HIGH:+MEDIUM; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; add_header Strict-Transport-Security "max-age=31536000;"; From 4b9e2ffbbc386e189ddfd9dd61d6cd637158511a Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 27 Feb 2017 16:52:35 +0100 Subject: [PATCH 0264/1066] [fix] Can't use common.sh on restore operation (#246) --- src/yunohost/backup.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 97a171dfb..a79df8604 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -502,7 +502,7 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, logger.warning(m18n.n('unrestore_app', app=app_instance_name)) continue - tmp_script = '/tmp/restore_' + app_instance_name + tmp_settings_dir = tmp_app_dir + '/settings' app_setting_path = '/etc/yunohost/apps/' + app_instance_name logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) try: @@ -512,8 +512,9 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, filesystem.chmod(app_setting_path, 0555, 0444, True) filesystem.chmod(app_setting_path + '/settings.yml', 0400) - # Copy restore script in a tmp file - subprocess.call(['install', '-Dm555', app_script, tmp_script]) + # Set correct right to the temporary settings folder + filesystem.chmod(tmp_settings_dir, 0550, 0550, True) + filesystem.chown(tmp_settings_dir, 'admin', None, True) # Prepare env. var. to pass to script env_dict = {} @@ -523,16 +524,12 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir # Execute app restore script - hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], + hook_exec(app_script, args=[tmp_app_bkp_dir, app_instance_name], raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) except: logger.exception(m18n.n('restore_app_failed', app=app_instance_name)) - # Copy remove script in a tmp file - filesystem.rm(tmp_script, force=True) app_script = tmp_app_dir + '/settings/scripts/remove' - tmp_script = '/tmp/remove_' + app_instance_name - subprocess.call(['install', '-Dm555', app_script, tmp_script]) # Setup environment for remove script env_dict_remove = {} @@ -542,7 +539,7 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, # Execute remove script # TODO: call app_remove instead - if hook_exec(tmp_script, args=[app_instance_name], + if hook_exec(app_script, args=[app_instance_name], env=env_dict_remove) != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) @@ -551,8 +548,6 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, shutil.rmtree(app_setting_path, ignore_errors=True) else: result['apps'].append(app_instance_name) - finally: - filesystem.rm(tmp_script, force=True) # Check if something has been restored if not result['hooks'] and not result['apps']: From ade8c59f0a8bcb21068eda0fe6a692fbb4be533a Mon Sep 17 00:00:00 2001 From: thardev Date: Mon, 27 Feb 2017 20:31:04 +0100 Subject: [PATCH 0265/1066] show fail2ban logs on admin web interface --- data/templates/yunohost/services.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 350faae37..97745890b 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -7,6 +7,9 @@ avahi-daemon: dnsmasq: status: service log: /var/log/daemon.log +fail2ban: + status: service + log: /var/log/fail2ban.log dovecot: status: service log: [/var/log/mail.log,/var/log/mail.err] From 7ade94e7e319fae635c1caede0e10f820c816ca8 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 2 Mar 2017 23:55:28 +0100 Subject: [PATCH 0266/1066] Fix ynh_app_dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Double // pour remplacer toutes les occurrences de _ Ajout d'une virgule entre les dépendances. --- data/helpers.d/package | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 9cafd3970..7264920ce 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -115,13 +115,13 @@ ynh_app_dependencies () { manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place fi version=$(sudo python3 -c "import sys, json;print(json.load(open(\"$manifest_path\"))['version'])") # Retrieve the version number in the manifest file. - dep_app=${app/_/-} # Replace all '_' by '-' + dep_app=${app//_/-} # Replace all '_' by '-' cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc Priority: optional Package: ${dep_app}-ynh-deps Version: ${version} -Depends: ${dependencies} +Depends: ${dependencies// /, } Architecture: all Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. From 4dfde86e1a8502667271eb23e764d936af9c0d6f Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 2 Mar 2017 23:57:02 +0100 Subject: [PATCH 0267/1066] Fix ynh_remove_app_dependencies too... --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 7264920ce..404c63cbc 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -136,6 +136,6 @@ EOF # # usage: ynh_remove_app_dependencies ynh_remove_app_dependencies () { - dep_app=${app/_/-} # Replace all '_' by '-' + dep_app=${app//_/-} # Replace all '_' by '-' ynh_package_autoremove ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. } From eb5a7607e464b791d079c02e764fbcd2d5f11560 Mon Sep 17 00:00:00 2001 From: Moul Date: Fri, 3 Mar 2017 08:34:01 +0100 Subject: [PATCH 0268/1066] [mod] dnsmasq conf: remove deprecated XMPP DNS record line. - https://serverfault.com/questions/582106/should-i-specify-the-srv-record-jabber-tcp --- data/templates/dnsmasq/domain.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/data/templates/dnsmasq/domain.tpl b/data/templates/dnsmasq/domain.tpl index 9966d1fdf..0dd87f5fd 100644 --- a/data/templates/dnsmasq/domain.tpl +++ b/data/templates/dnsmasq/domain.tpl @@ -4,4 +4,3 @@ txt-record={{ domain }},"v=spf1 mx a -all" mx-host={{ domain }},{{ domain }},5 srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5 srv-host=_xmpp-server._tcp.{{ domain }},{{ domain }},5269,0,5 -srv-host=_jabber._tcp.{{ domain }},{{ domain }},5269,0,5 From 20378fc2fcbfa44d392ae37ca4b628274cd26410 Mon Sep 17 00:00:00 2001 From: Moul Date: Fri, 3 Mar 2017 08:59:37 +0100 Subject: [PATCH 0269/1066] [fix] dnsmasq conf: remove 'resolv-file' line. - there is no file specified for this line. - dns resolution isn't working on some cases: - metronome could not works. - https://forum.yunohost.org/t/xmpp-cant-connect-to-conference-yunohost-org/2142 --- data/templates/dnsmasq/domain.tpl | 1 - 1 file changed, 1 deletion(-) diff --git a/data/templates/dnsmasq/domain.tpl b/data/templates/dnsmasq/domain.tpl index 9966d1fdf..85022290f 100644 --- a/data/templates/dnsmasq/domain.tpl +++ b/data/templates/dnsmasq/domain.tpl @@ -1,4 +1,3 @@ -resolv-file= address=/{{ domain }}/{{ ip }} txt-record={{ domain }},"v=spf1 mx a -all" mx-host={{ domain }},{{ domain }},5 From b0730d8463b5cf63be0b697534ac5270cac897cd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 7 Jul 2016 14:30:26 +0200 Subject: [PATCH 0270/1066] [enh] defaulting running hook_exec as root --- src/yunohost/hook.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 137232ed3..6bf515931 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -292,7 +292,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None): + chdir=None, env=None, user=None): """ Execute hook from a file with arguments @@ -303,6 +303,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, no_trace -- Do not print each command that will be executed chdir -- The directory from where the script will be executed env -- Dictionnary of environment variables to export + user -- User with which to run the command """ from moulinette.utils.process import call_async_output @@ -327,7 +328,11 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, cmd_script = path # Construct command to execute - command = ['sudo', '-n', '-u', 'admin', '-H', 'sh', '-c'] + if user is not None: + command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c'] + else: + command = ['sh', '-c'] + if no_trace: cmd = '/bin/bash "{script}" {args}' else: From 33e101b58840555ef49eeed122ac57f1ef90ae40 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 7 Jul 2016 22:39:04 +0200 Subject: [PATCH 0271/1066] [mod] change behavior, admin by default, as to explicitly set root as user --- src/yunohost/hook.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 6bf515931..0ce09b924 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -292,7 +292,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user=None): + chdir=None, env=None, user="admin"): """ Execute hook from a file with arguments @@ -328,10 +328,10 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, cmd_script = path # Construct command to execute - if user is not None: - command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c'] - else: + if user == "root": command = ['sh', '-c'] + else: + command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c'] if no_trace: cmd = '/bin/bash "{script}" {args}' From 879417895bf034c748d8eaa9a1a1788198cd71f7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 7 Jul 2016 22:39:14 +0200 Subject: [PATCH 0272/1066] [enh] use root for app related hook_exec --- src/yunohost/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 17223228d..4632f4ad7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -418,7 +418,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - if hook_exec(extracted_app_folder +'/scripts/upgrade', args=args_list, env=env_dict) != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) @@ -547,7 +547,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict) + args=args_list, env=env_dict, user="root") except (KeyboardInterrupt, EOFError): install_retcode = -1 except: @@ -564,7 +564,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Execute remove script remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove) + args=[app_instance_name], env=env_dict_remove, user="root") if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) @@ -632,7 +632,7 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict) == 0: + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) From 0122cbd43210560e40e151ce405f6b52ad9377f6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 5 Mar 2017 16:16:58 +0100 Subject: [PATCH 0273/1066] [mod] remove unused import --- src/yunohost/hook.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 0ce09b924..2a78f0801 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -307,7 +307,6 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, """ from moulinette.utils.process import call_async_output - from yunohost.app import _value_for_locale # Validate hook path if path[0] != '/': From e1a5074c756d0ef6b8c2133d22b829fbae75d45e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 5 Mar 2017 16:33:03 +0100 Subject: [PATCH 0274/1066] [fix] run missing backup scripts as root --- src/yunohost/backup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a79df8604..725bf2fda 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -234,7 +234,7 @@ def backup_create(name=None, description=None, output_directory=None, env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) + raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") except: logger.exception(m18n.n('backup_app_failed', app=app_instance_name)) # Cleaning app backup directory @@ -525,7 +525,7 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, # Execute app restore script hook_exec(app_script, args=[tmp_app_bkp_dir, app_instance_name], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict) + raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") except: logger.exception(m18n.n('restore_app_failed', app=app_instance_name)) @@ -540,7 +540,7 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, # Execute remove script # TODO: call app_remove instead if hook_exec(app_script, args=[app_instance_name], - env=env_dict_remove) != 0: + env=env_dict_remove, user="root") != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) From 76a7a36c62de350df9f9c57b26c9d7abd6af6585 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 5 Mar 2017 16:33:17 +0100 Subject: [PATCH 0275/1066] [enh] run hooks as root --- src/yunohost/hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 2a78f0801..b675caa2e 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -275,7 +275,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, hook_args = pre_callback(name=name, priority=priority, path=path, args=args) hook_exec(path, args=hook_args, chdir=chdir, env=env, - no_trace=no_trace, raise_on_error=True) + no_trace=no_trace, raise_on_error=True, user="root") except MoulinetteError as e: state = 'failed' logger.error(e.strerror, exc_info=1) From db601a412d83cb20795cddc931ddbf19948fa03e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 7 Jul 2016 10:36:37 +0200 Subject: [PATCH 0276/1066] [mod] try to clean a bit app_list code --- src/yunohost/app.py | 126 +++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 17223228d..948b1eba0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -157,17 +157,12 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w with_backup -- Return only apps with backup feature (force --installed filter) """ - if offset: offset = int(offset) - else: offset = 0 - if limit: limit = int(limit) - else: limit = 1000 + offset = int(offset) if offset else 0 + limit = int(limit) if limit else 1000 installed = with_backup or installed app_dict = {} - if raw: - list_dict = {} - else: - list_dict = [] + list_dict = {} if raw else [] try: applists = app_listlists()['lists'] @@ -178,10 +173,12 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w for applist in applists: with open(os.path.join(REPO_PATH, applist + '.json')) as json_list: - for app, info in json.loads(str(json_list.read())).items(): - if app not in app_dict: - info['repository'] = applist - app_dict[app] = info + for app, info in json.load(json_list).items(): + if app in app_dict: + continue + + info['repository'] = applist + app_dict[app] = info for app in os.listdir(APPS_SETTING_PATH): if app not in app_dict: @@ -192,63 +189,70 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w app_dict[app] = app_dict[original_app] continue with open( APPS_SETTING_PATH + app +'/manifest.json') as json_manifest: - app_dict[app] = {"manifest":json.loads(str(json_manifest.read()))} + app_dict[app] = {"manifest": json.loads(json_manifest)} + + app_dict[app]['repository'] = None - if len(app_dict) > (0 + offset) and limit > 0: - sorted_app_dict = {} - for sorted_keys in sorted(app_dict.keys())[offset:]: - sorted_app_dict[sorted_keys] = app_dict[sorted_keys] + if not (len(app_dict) > (0 + offset) and limit > 0): + return {'apps': list_dict} if not raw else list_dict - i = 0 - for app_id, app_info_dict in sorted_app_dict.items(): - if i < limit: - if (filter and ((filter in app_id) or (filter in app_info_dict['manifest']['name']))) or not filter: - app_installed = _is_installed(app_id) + sorted_app_dict = {} + for sorted_keys in sorted(app_dict.keys())[offset:]: + sorted_app_dict[sorted_keys] = app_dict[sorted_keys] - # Only installed apps filter - if installed and not app_installed: - continue + i = 0 + for app_id, app_info_dict in sorted_app_dict.items(): + if i >= limit: + break - # Filter only apps with backup and restore scripts - if with_backup and ( - not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/backup') or - not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/restore') - ): - continue + if not ((filter and ((filter in app_id) or (filter in app_info_dict['manifest']['name']))) or not filter): + continue - if raw: - app_info_dict['installed'] = app_installed - if app_installed: - app_info_dict['status'] = _get_app_status(app_id) + app_installed = _is_installed(app_id) - # dirty: we used to have manifest containing multi_instance value in form of a string - # but we've switched to bool, this line ensure retrocompatibility - app_info_dict["manifest"]["multi_instance"] = is_true(app_info_dict["manifest"].get("multi_instance", False)) + # Only installed apps filter + if installed and not app_installed: + continue - list_dict[app_id] = app_info_dict - else: - label = None - if app_installed: - app_info_dict_raw = app_info(app=app_id, raw=True) - label = app_info_dict_raw['settings']['label'] - list_dict.append({ - 'id': app_id, - 'name': app_info_dict['manifest']['name'], - 'label': label, - 'description': _value_for_locale( - app_info_dict['manifest']['description']), - # FIXME: Temporarly allow undefined license - 'license': app_info_dict['manifest'].get('license', - m18n.n('license_undefined')), - 'installed': app_installed - }) - i += 1 - else: - break - if not raw: - list_dict = { 'apps': list_dict } - return list_dict + # Filter only apps with backup and restore scripts + if with_backup and ( + not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/backup') or + not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/restore') + ): + continue + + if raw: + app_info_dict['installed'] = app_installed + if app_installed: + app_info_dict['status'] = _get_app_status(app_id) + + # dirty: we used to have manifest containing multi_instance value in form of a string + # but we've switched to bool, this line ensure retrocompatibility + app_info_dict["manifest"]["multi_instance"] = is_true(app_info_dict["manifest"].get("multi_instance", False)) + + list_dict[app_id] = app_info_dict + + else: + label = None + if app_installed: + app_info_dict_raw = app_info(app=app_id, raw=True) + label = app_info_dict_raw['settings']['label'] + + list_dict.append({ + 'id': app_id, + 'name': app_info_dict['manifest']['name'], + 'label': label, + 'description': _value_for_locale(app_info_dict['manifest']['description']), + # FIXME: Temporarly allow undefined license + 'license': app_info_dict['manifest'].get('license', m18n.n('license_undefined')), + 'installed': app_installed + }) + + i += 1 + + + return {'apps': list_dict} if not raw else list_dict def app_info(app, show_status=False, raw=False): From 4deaed1c7839d4f5472efdaf76dbe801457f75db Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2017 10:30:57 -0500 Subject: [PATCH 0277/1066] Trying to add comments and simplify some overly complicated parts --- src/yunohost/app.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 948b1eba0..a7f5f3d43 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -171,6 +171,7 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w app_fetchlist() applists = app_listlists()['lists'] + # Construct a dictionnary of apps, based on known app lists for applist in applists: with open(os.path.join(REPO_PATH, applist + '.json')) as json_list: for app, info in json.load(json_list).items(): @@ -180,42 +181,54 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w info['repository'] = applist app_dict[app] = info + # Get app list from the app settings directory for app in os.listdir(APPS_SETTING_PATH): if app not in app_dict: - # Look for forks + # Handle multi-instance case like wordpress__2 if '__' in app: original_app = app[:app.index('__')] if original_app in app_dict: app_dict[app] = app_dict[original_app] continue - with open( APPS_SETTING_PATH + app +'/manifest.json') as json_manifest: - app_dict[app] = {"manifest": json.loads(json_manifest)} + # FIXME : What if it's not !?!? + with open(os.path.join(APPS_SETTING_PATH, app, 'manifest.json')) as json_manifest: + app_dict[app] = {"manifest": json.load(json_manifest)} app_dict[app]['repository'] = None + # ??? if not (len(app_dict) > (0 + offset) and limit > 0): return {'apps': list_dict} if not raw else list_dict + # Build dict taking account of offset (ordered with sorted) sorted_app_dict = {} + print( sorted(app_dict.keys())[offset:]) for sorted_keys in sorted(app_dict.keys())[offset:]: sorted_app_dict[sorted_keys] = app_dict[sorted_keys] i = 0 for app_id, app_info_dict in sorted_app_dict.items(): + + print(app_id) + + # Apply limit if i >= limit: break - if not ((filter and ((filter in app_id) or (filter in app_info_dict['manifest']['name']))) or not filter): + # Apply filter if there's one + if (filter and + (filter not in app_id) and + (filter not in app_info_dict['manifest']['name'])): continue + # Ignore non-installed app if user wants only installed apps app_installed = _is_installed(app_id) - - # Only installed apps filter if installed and not app_installed: continue - # Filter only apps with backup and restore scripts + # Ignore apps which don't have backup/restore script if user wants + # only apps with backup features if with_backup and ( not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/backup') or not os.path.isfile(APPS_SETTING_PATH + app_id + '/scripts/restore') @@ -248,7 +261,7 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w 'license': app_info_dict['manifest'].get('license', m18n.n('license_undefined')), 'installed': app_installed }) - + i += 1 From cc9364f37ebfaf3de35820266b27821f7e7621fd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 7 Feb 2017 10:49:34 -0500 Subject: [PATCH 0278/1066] Trying to make offset / limit consistent --- src/yunohost/app.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a7f5f3d43..79857f075 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -201,20 +201,16 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w if not (len(app_dict) > (0 + offset) and limit > 0): return {'apps': list_dict} if not raw else list_dict - # Build dict taking account of offset (ordered with sorted) - sorted_app_dict = {} - print( sorted(app_dict.keys())[offset:]) - for sorted_keys in sorted(app_dict.keys())[offset:]: - sorted_app_dict[sorted_keys] = app_dict[sorted_keys] + # Sort app list + sorted_app_list = sorted(app_dict.keys()) + + # Take into account offset + sorted_app_list = sorted_app_list[offset:] i = 0 - for app_id, app_info_dict in sorted_app_dict.items(): + for app_id in sorted_app_list: - print(app_id) - - # Apply limit - if i >= limit: - break + app_info_dict = app_dict[app_id] # Apply filter if there's one if (filter and @@ -261,8 +257,11 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w 'license': app_info_dict['manifest'].get('license', m18n.n('license_undefined')), 'installed': app_installed }) - + + # Count listed apps and apply limit i += 1 + if i >= limit: + break return {'apps': list_dict} if not raw else list_dict From 02c974e8ca54e35d08b688388279e99d0d039b99 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 7 Feb 2017 20:47:04 +0100 Subject: [PATCH 0279/1066] [mod] remove useless addition --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 79857f075..8e67e5030 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -198,7 +198,7 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w app_dict[app]['repository'] = None # ??? - if not (len(app_dict) > (0 + offset) and limit > 0): + if not (len(app_dict) > offset and limit > 0): return {'apps': list_dict} if not raw else list_dict # Sort app list From 4db42fdeb7b453bf2d176d69155bcdc4041ed7c5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Mar 2017 17:21:40 +0100 Subject: [PATCH 0280/1066] [fix] if a service don't have a 'status' entry, don't list it --- src/yunohost/service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index c648a0e0e..e64c9cd89 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -201,11 +201,12 @@ def service_status(names=[]): m18n.n('service_unknown', service=name)) status = None - if 'status' not in services[name] or \ - services[name]['status'] == 'service': + if services[name].get('status') == 'service': status = 'service %s status' % name - else: + elif "status" in services[name]: status = str(services[name]['status']) + else: + continue runlevel = 5 if 'runlevel' in services[name].keys(): From 949e4de20aa0cf84fa9fb4b6c292b93a382d17eb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Mar 2017 17:21:58 +0100 Subject: [PATCH 0281/1066] [fix] nsswitch and udisks2 aren't used anymore --- data/templates/yunohost/services.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 97745890b..e17e53c22 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -50,10 +50,8 @@ yunohost-firewall: nslcd: status: service log: /var/log/syslog -nsswitch: - status: service -udisks2: - status: service +nsswitch: null +udisks2: null amavis: null postgrey: null spamassassin: null From 6d1c62d6191adb29b61b07d5a15546227aa934b4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Mar 2017 17:22:42 +0100 Subject: [PATCH 0282/1066] [fix] we don't use bind9, add null entry to remove it from old services.yml --- data/templates/yunohost/services.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index e17e53c22..5c25dcad8 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -51,6 +51,7 @@ nslcd: status: service log: /var/log/syslog nsswitch: null +bind9: null udisks2: null amavis: null postgrey: null From a18486df05f4898a5e9842229c73fbebe87b2d2a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Mar 2017 20:24:36 +0100 Subject: [PATCH 0283/1066] [enh] add other services to remove --- data/templates/yunohost/services.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 5c25dcad8..bbe67972e 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -52,7 +52,10 @@ nslcd: log: /var/log/syslog nsswitch: null bind9: null +tahoe-lafs: null +memcached: null udisks2: null +udisk-glue: null amavis: null postgrey: null spamassassin: null From 8482e48f9f27990a0eac76f07b24e4a89e7cc2d9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 7 Mar 2017 02:07:09 +0100 Subject: [PATCH 0284/1066] [fix] launch ssowatconf at the end of a broken install to avoid sso bad state --- src/yunohost/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 17223228d..9d1d38f1e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -573,6 +573,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): shutil.rmtree(app_setting_path) shutil.rmtree(extracted_app_folder) + app_ssowatconf(auth) + if install_retcode == -1: raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted')) From f490bf2e960e846267201bd2d49e85694a61eb88 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 7 Mar 2017 21:26:08 +0100 Subject: [PATCH 0285/1066] [love] adding thardev to contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 375c1cec6..1c5b6fd33 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -40,6 +40,7 @@ YunoHost core Contributors - Sebastien 'sebian' Badia - lmangani - Julien Vaubourg +- thardev YunoHost core Translators From df2f26c6075c81fdfb58c983fda9b9908f709e5c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 11 Mar 2017 03:11:39 +0100 Subject: [PATCH 0286/1066] [enh] Trigger exception during unit tests if string key aint defined (#261) --- src/yunohost/tests/conftest.py | 97 ++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 2845de4ee..f979d1b42 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -3,14 +3,101 @@ import moulinette sys.path.append("..") +############################################################################### +# Tweak moulinette init to have yunohost namespace # +############################################################################### + + old_init = moulinette.core.Moulinette18n.__init__ - - def monkey_path_i18n_init(self, package, default_locale="en"): old_init(self, package, default_locale) self.load_namespace("yunohost") - - moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init -moulinette.init() + +############################################################################### +# Tweak translator to raise exceptions if string keys are not defined # +############################################################################### + + +old_translate = moulinette.core.Translator.translate +def new_translate(self, key, *args, **kwargs): + + if key not in self._translations[self.default_locale].keys(): + raise KeyError("Unable to retrieve key %s for default locale !" % key) + + return old_translate(self, key, *args, **kwargs) +moulinette.core.Translator.translate = new_translate + +def new_m18nn(self, key, *args, **kwargs): + return self._namespace.translate(key, *args, **kwargs) +moulinette.core.Moulinette18n.n = new_m18nn + + +############################################################################### +# Init the moulinette to have the cli loggers stuff # +############################################################################### + + +def _init_moulinette(): + """Configure logging and initialize the moulinette""" + # Define loggers handlers + handlers = set(['tty']) + root_handlers = set(handlers) + + # Define loggers level + level = 'INFO' + tty_level = 'SUCCESS' + + # Custom logging configuration + logging = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'tty-debug': { + 'format': '%(relativeCreated)-4d %(fmessage)s' + }, + 'precise': { + 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + }, + }, + 'filters': { + 'action': { + '()': 'moulinette.utils.log.ActionFilter', + }, + }, + 'handlers': { + 'tty': { + 'level': tty_level, + 'class': 'moulinette.interfaces.cli.TTYHandler', + 'formatter': '', + }, + }, + 'loggers': { + 'yunohost': { + 'level': level, + 'handlers': handlers, + 'propagate': False, + }, + 'moulinette': { + 'level': level, + 'handlers': [], + 'propagate': True, + }, + 'moulinette.interface': { + 'level': level, + 'handlers': handlers, + 'propagate': False, + }, + }, + 'root': { + 'level': level, + 'handlers': root_handlers, + }, + } + + # Initialize moulinette + moulinette.init(logging_config=logging, _from_source=False) + + +_init_moulinette() From f851817bbf3c94cca638dd9b849bfb23080cad46 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 12 Mar 2017 20:58:45 -0400 Subject: [PATCH 0287/1066] Updating ciphers with recommendation from mozilla with modern compatibility --- data/templates/nginx/server.tpl.conf | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 45961633f..8466eeef3 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -24,16 +24,15 @@ server { ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + + # Ciphers with modern configuration + # Source : https://mozilla.github.io/server-side-tls/ssl-config-generator/ + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; add_header Strict-Transport-Security "max-age=31536000;"; - # Uncomment the following directive after DH generation - # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 - #ssl_dhparam /etc/ssl/private/dh2048.pem; - access_by_lua_file /usr/share/ssowat/access.lua; include conf.d/{{ domain }}.d/*.conf; From f2e656176e1b7df302caa7f2f3a8f01cc3dc4b77 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 13 Mar 2017 23:29:47 +0100 Subject: [PATCH 0288/1066] Failed if $1 not set Because we use set -u, this helper failed if the optional arg is not set --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index efc180d90..d6754bbfa 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -10,7 +10,7 @@ # # It's possible to use this helper several times, each config will added to same logrotate config file. ynh_use_logrotate () { - if [ -n "$1" ]; then + if [ "$#" -gt 0 ]; then if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile logfile=$1 # In this case, focus logrotate on the logfile else From 50cc3536acf2e2dee0dce70d0f81c97e47748d7a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 13 Mar 2017 23:51:35 +0100 Subject: [PATCH 0289/1066] [mod] remove offset/limit from app_list, they aren't used anymore --- data/actionsmap/yunohost.yml | 6 ------ src/yunohost/app.py | 18 +----------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 25eb6cf6d..ef951496c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -426,12 +426,6 @@ app: action_help: List apps api: GET /apps arguments: - -l: - full: --limit - help: Maximum number of app fetched - -o: - full: --offset - help: Starting number for app fetching -f: full: --filter help: Name filter of app_id or app_name diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8e67e5030..45579c22e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -144,7 +144,7 @@ def app_removelist(name): logger.success(m18n.n('appslist_removed')) -def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, with_backup=False): +def app_list(filter=None, raw=False, installed=False, with_backup=False): """ List apps @@ -157,8 +157,6 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w with_backup -- Return only apps with backup feature (force --installed filter) """ - offset = int(offset) if offset else 0 - limit = int(limit) if limit else 1000 installed = with_backup or installed app_dict = {} @@ -197,17 +195,9 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w app_dict[app]['repository'] = None - # ??? - if not (len(app_dict) > offset and limit > 0): - return {'apps': list_dict} if not raw else list_dict - # Sort app list sorted_app_list = sorted(app_dict.keys()) - # Take into account offset - sorted_app_list = sorted_app_list[offset:] - - i = 0 for app_id in sorted_app_list: app_info_dict = app_dict[app_id] @@ -258,12 +248,6 @@ def app_list(offset=None, limit=None, filter=None, raw=False, installed=False, w 'installed': app_installed }) - # Count listed apps and apply limit - i += 1 - if i >= limit: - break - - return {'apps': list_dict} if not raw else list_dict From 4be2720cb05a11587c14ab50a52ac25b1d3fa195 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 14 Mar 2017 00:35:28 +0100 Subject: [PATCH 0290/1066] [mod] implement ljf comment --- src/yunohost/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 45579c22e..0884ab7ce 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -173,11 +173,9 @@ def app_list(filter=None, raw=False, installed=False, with_backup=False): for applist in applists: with open(os.path.join(REPO_PATH, applist + '.json')) as json_list: for app, info in json.load(json_list).items(): - if app in app_dict: - continue - - info['repository'] = applist - app_dict[app] = info + if app not in app_dict: + info['repository'] = applist + app_dict[app] = info # Get app list from the app settings directory for app in os.listdir(APPS_SETTING_PATH): From 5fcb8cd439c3725beb46ada423ac046a812fde91 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 14 Mar 2017 16:34:07 +0100 Subject: [PATCH 0291/1066] Remove use of deprecated helper --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 404c63cbc..c9f0557cc 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -88,7 +88,7 @@ ynh_package_install_from_equivs () { ynh_package_update # Build and install the package - TMPDIR=$(ynh_mkdir_tmp) + TMPDIR=$(mktemp -d) # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg From 6df7a896ef96d02c9429a8f06e37f26c1255508f Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 14 Mar 2017 16:48:50 +0100 Subject: [PATCH 0292/1066] [enh] Use _get_maindomain helper. --- src/yunohost/app.py | 6 ++---- src/yunohost/dyndns.py | 5 ++--- src/yunohost/monitor.py | 5 ++--- src/yunohost/user.py | 5 ++--- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 68a4620ce..40231963e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -988,12 +988,10 @@ def app_ssowatconf(auth): """ - from yunohost.domain import domain_list + from yunohost.domain import domain_list, _get_maindomain from yunohost.user import user_list - with open('/etc/yunohost/current_host', 'r') as f: - main_domain = f.readline().rstrip() - + main_domain = _get_maindomain() domains = domain_list(auth)['domains'] users = {} diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 7553e417c..fca687b60 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -35,7 +35,7 @@ import subprocess from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.domain import get_public_ip +from yunohost.domain import get_public_ip, _get_maindomain logger = getActionLogger('yunohost.dyndns') @@ -78,8 +78,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None """ if domain is None: - with open('/etc/yunohost/current_host', 'r') as f: - domain = f.readline().rstrip() + domain = _get_maindomain() # Verify if domain is available try: diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 8fd8b7732..137e57b6e 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -40,7 +40,7 @@ from datetime import datetime from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.domain import get_public_ip +from yunohost.domain import get_public_ip, _get_maindomain logger = getActionLogger('yunohost.monitor') @@ -174,8 +174,7 @@ def monitor_network(units=None, human_readable=False): for u in units: if u == 'check': result[u] = {} - with open('/etc/yunohost/current_host', 'r') as f: - domain = f.readline().rstrip() + domain = _get_maindomain() cmd_check_smtp = os.system('/bin/nc -z -w1 yunohost.org 25') if cmd_check_smtp == 0: smtp_check = m18n.n('network_check_smtp_ok') diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 9de9595f4..7c8776063 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -106,7 +106,7 @@ def user_create(auth, username, firstname, lastname, mail, password, """ import pwd - from yunohost.domain import domain_list + from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf @@ -163,8 +163,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): - with open('/etc/yunohost/current_host') as f: - main_domain = f.readline().rstrip() + main_domain = _get_maindomain() aliases = [ 'root@' + main_domain, 'admin@' + main_domain, From 2bb32c26b64f274380adafaefbf877667ac3e092 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 14 Mar 2017 18:05:28 +0100 Subject: [PATCH 0293/1066] Add app setting --- data/helpers.d/package | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/package b/data/helpers.d/package index c9f0557cc..c87abe5f3 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -128,6 +128,7 @@ Description: Fake package for ${app} (YunoHost app) dependencies EOF ynh_package_install_from_equivs ./${dep_app}-ynh-deps.control \ || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies + ynh_app_setting_set $app apt_dependencies $dependencies } # Remove fake package and its dependencies From 08760df0ecc8abe9906c6baea37f8154591a277b Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 14 Mar 2017 16:56:16 +0100 Subject: [PATCH 0294/1066] [fix] Regenerate SSOwat conf during main_domain operation. #672 --- src/yunohost/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8470a7b27..07db05877 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -185,6 +185,9 @@ def tools_maindomain(auth, new_domain=None): else: logger.info(out) + # Generate SSOwat configuration file + app_ssowatconf(auth) + # Regen configurations try: with open('/etc/yunohost/installed', 'r') as f: @@ -308,9 +311,6 @@ def tools_postinstall(domain, password, ignore_dyndns=False): domain_add(auth, domain, dyndns) tools_maindomain(auth, domain) - # Generate SSOwat configuration file - app_ssowatconf(auth) - # Change LDAP admin password tools_adminpw(auth, password) From 47dffca74413565943f102301438cdd6b3414087 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 15 Mar 2017 23:51:12 +0100 Subject: [PATCH 0295/1066] Allow underscore in backup name Allow underscore in backup name, to allow backup with the name of multi-instance apps. If there are no reason to avoid this character. --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ef951496c..c37a2c2f1 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -678,7 +678,7 @@ backup: help: Name of the backup archive extra: pattern: &pattern_backup_archive_name - - !!str ^[\w\-\.]{1,30}(? Date: Thu, 16 Mar 2017 04:58:34 +0100 Subject: [PATCH 0296/1066] Nouveau helper ynh_normalize_url_path (#234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Nouveau helper ynh_check_path Nouveau helper pour vérifier et corriger la syntaxe du path. Et ça permet de passer le test "incorrect_path" de package check * [fix] Bad comment * path en argument et normalize * Make commande more obvious * Do not use path * Adding behavior for / in the documentation --- data/helpers.d/network | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/network b/data/helpers.d/network index 42616d513..750a49cf6 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -1,3 +1,27 @@ +# Normalize the url path syntax +# Handle the slash at the beginning of path and its absence at ending +# Return a normalized url path +# +# example: url_path=$(ynh_normalize_url_path $url_path) +# ynh_normalize_url_path example -> /example +# ynh_normalize_url_path /example -> /example +# ynh_normalize_url_path /example/ -> /example +# ynh_normalize_url_path / -> / +# +# usage: ynh_normalize_url_path path_to_normalize +# | arg: url_path_to_normalize - URL path to normalize before using it +ynh_normalize_url_path () { + path_url=$1 + test -n "$path_url" || ynh_die "ynh_normalize_url_path expect a URL path as first argument and received nothing." + if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / + path_url="/$path_url" # Add / at begin of path variable + fi + if [ "${path_url:${#path_url}-1}" == "/" ] && [ ${#path_url} -gt 1 ]; then # If the last character is a / and that not the only character. + path_url="${path_url:0:${#path_url}-1}" # Delete the last character + fi + echo $path_url +} + # Find a free port and return it # # example: port=$(ynh_find_port 8080) @@ -12,4 +36,4 @@ ynh_find_port () { port=$((port+1)) # Else, pass to next port done echo $port -} +} \ No newline at end of file From 5f3fcefc882c66ccb7cd60b05fef6cde3d4f81d0 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 16 Mar 2017 18:35:57 +0100 Subject: [PATCH 0297/1066] Prevent to rewrite the previous control file --- data/helpers.d/package | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index c87abe5f3..2bf258c53 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -116,7 +116,11 @@ ynh_app_dependencies () { fi version=$(sudo python3 -c "import sys, json;print(json.load(open(\"$manifest_path\"))['version'])") # Retrieve the version number in the manifest file. dep_app=${app//_/-} # Replace all '_' by '-' - cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build + + if ynh_package_is_installed "${dep_app}-ynh-deps"; then + echo "A package named ${dep_app}-ynh-deps is already installed" >&2 + else + cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc Priority: optional Package: ${dep_app}-ynh-deps @@ -126,9 +130,10 @@ Architecture: all Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. EOF - ynh_package_install_from_equivs ./${dep_app}-ynh-deps.control \ - || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies - ynh_app_setting_set $app apt_dependencies $dependencies + ynh_package_install_from_equivs ./${dep_app}-ynh-deps.control \ + || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies + ynh_app_setting_set $app apt_dependencies $dependencies + fi } # Remove fake package and its dependencies From 5b9092d6df5c6985979d353a99dd8f5bacef40c8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 17 Mar 2017 04:07:17 +0100 Subject: [PATCH 0298/1066] Rename ynh_app_dependencies to ynh_install_app_dependencies --- data/helpers.d/package | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 2bf258c53..368e61cd6 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -104,11 +104,12 @@ ynh_package_install_from_equivs () { ynh_package_is_installed "$pkgname" } -# Install dependencies with a equivs control file +# Define and install dependencies with a equivs control file +# This helper can/should only be called once per app # -# usage: ynh_app_dependencies dep [dep [...]] +# usage: ynh_install_app_dependencies dep [dep [...]] # | arg: dep - the package name to install in dependence -ynh_app_dependencies () { +ynh_install_app_dependencies () { dependencies=$@ manifest_path="../manifest.json" if [ ! -e "$manifest_path" ]; then From 9d44e1d84bc5ad13d054265058a084ffafc0ebcb Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 17 Mar 2017 16:16:36 +0100 Subject: [PATCH 0299/1066] [enh] New helper ynh_abort_if_errors (#245) * New helper ynh_check_error Helpers to implemente trap and set -eu. Simply to use, you only add `ynh_check_error` in your script. * Change helper's name * Propagate new name in comments --- data/helpers.d/system | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 data/helpers.d/system diff --git a/data/helpers.d/system b/data/helpers.d/system new file mode 100644 index 000000000..5f2ad385b --- /dev/null +++ b/data/helpers.d/system @@ -0,0 +1,43 @@ +# Manage a fail of the script +# +# Print a warning to inform that the script was failed +# Execute the ynh_clean_setup function if used in the app script +# +# usage of ynh_clean_setup function +# This function provide a way to clean some residual of installation that not managed by remove script. +# To use it, simply add in your script: +# ynh_clean_setup () { +# instructions... +# } +# This function is optionnal. +# +# Usage: ynh_exit_properly is used only by the helper ynh_abort_if_errors. +# You must not use it directly. +ynh_exit_properly () { + exit_code=$? + if [ "$exit_code" -eq 0 ]; then + exit 0 # Exit without error if the script ended correctly + fi + + trap '' EXIT # Ignore new exit signals + set +eu # Do not exit anymore if a command fail or if a variable is empty + + echo -e "!!\n $app's script has encountered an error. Its execution was cancelled.\n!!" >&2 + + if type -t ynh_clean_setup > /dev/null; then # Check if the function exist in the app script. + ynh_clean_setup # Call the function to do specific cleaning for the app. + fi + + ynh_die # Exit with error status +} + +# Exit if an error occurs during the execution of the script. +# +# Stop immediatly the execution if an error occured or if a empty variable is used. +# The execution of the script is derivate to ynh_exit_properly function before exit. +# +# Usage: ynh_abort_if_errors +ynh_abort_if_errors () { + set -eu # Exit if a command fail, and if a variable is used unset. + trap ynh_exit_properly EXIT # Capturing exit signals on shell script +} From 8cba71b51444c3119c6b9771e2a6eee9b389a1fa Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 20 Mar 2017 00:45:33 +0100 Subject: [PATCH 0300/1066] [fix] Apply cipher suite into webadmin nginx conf --- data/templates/nginx/plain/yunohost_admin.conf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index bcd99493b..57abac7ee 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -19,9 +19,12 @@ server { ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + + # Ciphers with modern configuration + # Source : https://mozilla.github.io/server-side-tls/ssl-config-generator/ + ssl_protocols TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers ALL:!aNULL:!eNULL:!LOW:!EXP:!RC4:!3DES:+HIGH:+MEDIUM; add_header Strict-Transport-Security "max-age=31536000;"; From 50188ae265808139538ea90c6f2854f97d21a2b7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 20 Mar 2017 01:49:27 +0100 Subject: [PATCH 0301/1066] [fix] only remove a service if it is setted to null --- data/hooks/conf_regen/01-yunohost | 2 +- data/templates/yunohost/services.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 96b62fe67..f8bef0614 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -65,7 +65,7 @@ with open('/etc/yunohost/services.yml') as f: updated = False for service, conf in new_services.items(): # remove service with empty conf - if not conf: + if conf is None: if service in services: print("removing '{0}' from services".format(service)) del services[service] diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index bbe67972e..514cf5258 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -50,7 +50,7 @@ yunohost-firewall: nslcd: status: service log: /var/log/syslog -nsswitch: null +nsswitch: {} bind9: null tahoe-lafs: null memcached: null From ce472696f52d6548b97491f56dcfe6599188ca2d Mon Sep 17 00:00:00 2001 From: opi Date: Wed, 22 Mar 2017 08:31:47 +0100 Subject: [PATCH 0302/1066] [enh] ip6.yunohost is now served through HTTPS. --- data/hooks/conf_regen/43-dnsmasq | 2 +- src/yunohost/domain.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index 6947fb634..aab2b599f 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -17,7 +17,7 @@ do_pre_regen() { # retrieve variables ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) ynh_validate_ip4 "$ipv4" || ipv4='127.0.0.1' - ipv6=$(curl -s -6 http://ip6.yunohost.org 2>/dev/null || true) + ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) ynh_validate_ip6 "$ipv6" || ipv6='' domain_list=$(sudo yunohost domain list --output-as plain --quiet) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index ded8bb27a..37636229e 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -280,8 +280,7 @@ def get_public_ip(protocol=4): if protocol == 4: url = 'https://ip.yunohost.org' elif protocol == 6: - # FIXME: Let's Encrypt does not support IPv6-only hosts yet - url = 'http://ip6.yunohost.org' + url = 'https://ip6.yunohost.org' else: raise ValueError("invalid protocol version") try: From 856699445e85df20ca60754e019e12401902dfa6 Mon Sep 17 00:00:00 2001 From: Moul Date: Thu, 23 Mar 2017 10:01:46 +0000 Subject: [PATCH 0303/1066] Update changelog for 2.6.1 release --- debian/changelog | 131 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0945c5ad0..0cfe85d8a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,134 @@ +yunohost (2.6.1) testing; urgency=low + + [ Maniack Crudelis ] + * Hack dégueux pour éviter d'écrire dans le log cli + * [enh] New helpers for equivs use + * [enh] New helpers for logrotate + * Update package + * Restore use of subshell + + [ Trollken ] + * [i18n] Translated using Weblate (Portuguese) + + [ rokaz ] + * [i18n] Translated using Weblate (Spanish) + * [i18n] Translated using Weblate (French) + + [ Jean-Baptiste Holcroft ] + * [i18n] Translated using Weblate (French) + + [ rokaz ] + * [i18n] Translated using Weblate (English) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + + [ bricabraque ] + * [i18n] Translated using Weblate (Italian) + + [ Trollken ] + * [i18n] Translated using Weblate (Portuguese) + + [ rokaz ] + * [i18n] Translated using Weblate (Spanish) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + * [i18n] Translated using Weblate (German) + + [ bricabraque ] + * [i18n] Translated using Weblate (Italian) + + [ Fabian Gruber ] + * [i18n] Translated using Weblate (German) + + [ Lapineige ] + * [i18n] Translated using Weblate (French) + + [ Laurent Peuch ] + * [enh] upgrade ciphers suit to more secure ones + + [ ljf (zamentur) ] + * [fix] Can't use common.sh on restore operation (#246) + + [ thardev ] + * show fail2ban logs on admin web interface + + [ Maniack Crudelis ] + * Fix ynh_app_dependencies + * Fix ynh_remove_app_dependencies too... + + [ Moul ] + * [mod] dnsmasq conf: remove deprecated XMPP DNS record line. + * [fix] dnsmasq conf: remove 'resolv-file' line. - there is no file specified for this line. - dns resolution isn't working on some cases: - metronome could not works. - https://forum.yunohost.org/t/xmpp-cant-connect-to-conference-yunohost-org/2142 + + [ Laurent Peuch ] + * [enh] defaulting running hook_exec as root + * [mod] change behavior, admin by default, as to explicitly set root as user + * [enh] use root for app related hook_exec + * [mod] remove unused import + * [fix] run missing backup scripts as root + * [enh] run hooks as root + * [mod] try to clean a bit app_list code + + [ Alexandre Aubin ] + * Trying to add comments and simplify some overly complicated parts + * Trying to make offset / limit consistent + + [ Laurent Peuch ] + * [mod] remove useless addition + * [fix] if a service don't have a 'status' entry, don't list it + * [fix] nsswitch and udisks2 aren't used anymore + * [fix] we don't use bind9, add null entry to remove it from old services.yml + * [enh] add other services to remove + * [fix] launch ssowatconf at the end of a broken install to avoid sso bad state + + [ opi ] + * [love] adding thardev to contributors + + [ Alexandre Aubin ] + * [enh] Trigger exception during unit tests if string key aint defined (#261) + * Updating ciphers with recommendation from mozilla with modern compatibility + + [ Maniack Crudelis ] + * Failed if $1 not set + + [ Laurent Peuch ] + * [mod] remove offset/limit from app_list, they aren't used anymore + * [mod] implement ljf comment + + [ Maniack Crudelis ] + * Remove use of deprecated helper + + [ opi ] + * [enh] Use _get_maindomain helper. + + [ Maniack Crudelis ] + * Add app setting + + [ opi ] + * [fix] Regenerate SSOwat conf during main_domain operation. #672 + + [ Maniack Crudelis ] + * Nouveau helper ynh_normalize_url_path (#234) + * Prevent to rewrite the previous control file + + [ Alexandre Aubin ] + * Rename ynh_app_dependencies to ynh_install_app_dependencies + + [ Maniack Crudelis ] + * [enh] New helper ynh_abort_if_errors (#245) + + [ ljf ] + * [fix] Apply cipher suite into webadmin nginx conf + + [ Laurent Peuch ] + * [fix] only remove a service if it is setted to null + + [ Moul ] + + -- Moul Thu, 23 Mar 2017 09:53:06 +0000 + yunohost (2.6.0) testing; urgency=low Important changes From bb11168c31064d6be6cba3b8f2c48bf3a70f02e8 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Sun, 26 Mar 2017 21:07:13 +0200 Subject: [PATCH 0304/1066] Fix for missing YunoHost tiles (#276) This PR: - brings back [this PR](https://github.com/YunoHost/yunohost-config-nginx/pull/3/files) which seems to have stayed behind when migrating from `yunohost-config-nginx`. It fixes the missing tile for Jirafeau app (and maybe others). - adds a fix to serve `ynhpanel.*` files whatever the app ningx rules are (some apps nginx conf files block them, like duniter or nextcloud). --- data/templates/nginx/plain/yunohost_panel.conf.inc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/templates/nginx/plain/yunohost_panel.conf.inc b/data/templates/nginx/plain/yunohost_panel.conf.inc index 0ca8b02aa..34afe136d 100644 --- a/data/templates/nginx/plain/yunohost_panel.conf.inc +++ b/data/templates/nginx/plain/yunohost_panel.conf.inc @@ -1,2 +1,8 @@ +# Insert YunoHost panel sub_filter ''; sub_filter_once on; +# Apply to other mime types than text/html +sub_filter_types application/xhtml+xml; +# Prevent YunoHost panel files from being blocked by specific app rules +location ~ ynhpanel\.(js|json|css) { +} From 5dc86e6dbfca7ac28f9f5b756e14deaa6de63de8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Mar 2017 01:33:20 +0200 Subject: [PATCH 0305/1066] Mention limit of 30 characters in message --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 62acc4e10..f5db98595 100644 --- a/locales/en.json +++ b/locales/en.json @@ -153,7 +153,7 @@ "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", "path_removal_failed": "Unable to remove path {:s}", - "pattern_backup_archive_name": "Must be a valid filename with alphanumeric and -_. characters only", + "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", "pattern_email": "Must be a valid email address (e.g. someone@domain.org)", "pattern_firstname": "Must be a valid first name", From 4a729ba03512d95e0452c5ce3ca1f5f618851ae7 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 28 Mar 2017 01:36:27 +0200 Subject: [PATCH 0306/1066] [enh] Add libnss-mdns as Debian dependency. (#279) --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 47f0e61b2..c261546dd 100644 --- a/debian/control +++ b/debian/control @@ -22,7 +22,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl - , dnsmasq, openssl, avahi-daemon + , dnsmasq, openssl, avahi-daemon, libnss-mdns , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , haveged From 86f0978dfb4c43237e10fd55653bc664ff46eb8d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 29 Mar 2017 02:39:00 +0200 Subject: [PATCH 0307/1066] Cleaner postinstall logs during CA creation (#250) --- locales/en.json | 1 + src/yunohost/tools.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index f5db98595..9c3d1ce76 100644 --- a/locales/en.json +++ b/locales/en.json @@ -242,6 +242,7 @@ "user_updated": "The user has been updated", "yunohost_already_installed": "YunoHost is already installed", "yunohost_ca_creation_failed": "Unable to create certificate authority", + "yunohost_ca_creation_success": "The local certification authority has been created.", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 07db05877..5f1e0bb0f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -291,21 +291,31 @@ def tools_postinstall(domain, password, ignore_dyndns=False): # Create SSL CA service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - command_list = [ + commands = [ 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.ca.cnf ' % (domain, ssl_dir), + 'sed -i s/yunohost.org/%s/g %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, 'update-ca-certificates' ] - for command in command_list: - if os.system(command) != 0: + for command in commands: + p = subprocess.Popen( + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + out, _ = p.communicate() + + if p.returncode != 0: + logger.warning(out) raise MoulinetteError(errno.EPERM, m18n.n('yunohost_ca_creation_failed')) + else: + logger.debug(out) + + logger.success(m18n.n('yunohost_ca_creation_success')) # New domain config domain_add(auth, domain, dyndns) From c31e4c5b0d7972f7ce02e35c32218ee268e78ced Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 2 Apr 2017 23:52:11 +0200 Subject: [PATCH 0308/1066] New helper ynh_replace_string (#280) * New helper ynh_substitute_char Sed abstraction, and a way to use something other than sed. I added also an escape of the delimiter used by sed, to prevent a fail in this case. * Renaming to ynh_replace_string * Renaming variable for clarity --- data/helpers.d/string | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/data/helpers.d/string b/data/helpers.d/string index 1a848d239..772681fb9 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -9,3 +9,18 @@ ynh_string_random() { | tr -c -d 'A-Za-z0-9' \ | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' } + +# Substitute/replace a string by another in a file +# +# usage: ynh_replace_string match_string replace_string target_file +# | arg: match_string - String to be searched and replaced in the file +# | arg: replace_string - String that will replace matches +# | arg: target_file - File in which the string will be replaced. +ynh_replace_string () { + delimit=@ + match_string=${1//${delimit}/"\\${delimit}"} # Escape the delimiter if it's in the string. + replace_string=${2//${delimit}/"\\${delimit}"} + workfile=$3 + + sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" +} From 712e69d8302bf01e3b9677f71ad8763e3a99f1ec Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 6 Apr 2017 15:33:56 +0200 Subject: [PATCH 0309/1066] New helper ynh_local_curl (#288) * New helper ynh_curl_abstract Just an abstraction around curl command. To prevent forgot arguments and simplify this difficult command. * [enh] Switch args of ynh_curl_abstract * [enh] Rename ynh_curl_abstract into ynh_local_curl * Splitting the various key=value into several arguments * Comment to clarify bash command * Adding comment about $domain and $path_url --- data/helpers.d/utils | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 165a394d3..6bc1e39d1 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -24,3 +24,31 @@ ynh_get_plain_key() { fi done } + +# Curl abstraction to help with POST requests to local pages (such as installation forms) +# +# $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?)) +# +# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2" +# +# usage: ynh_local_curl "page_uri" "key1=value1" "key2=value2" ... +# | arg: page_uri - Path (relative to $path_url) of the page where POST data will be sent +# | arg: key1=value1 - (Optionnal) POST key and corresponding value +# | arg: key2=value2 - (Optionnal) Another POST key and corresponding value +# | arg: ... - (Optionnal) More POST keys and values +ynh_local_curl () { + # Define url of page to curl + full_page_url=https://localhost$path_url$1 + + # Concatenate all other arguments with '&' to prepare POST data + POST_data="" + for arg in "${@:2}" + do + POST_data="${POST_data}${arg}&" + done + # (Remove the last character, which is an unecessary '&') + POST_data=${POST_data::-1} + + # Curl the URL + curl -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 --data "$POST_data" "$full_page_url" 2>&1 +} From c954429ccad6d2ffe744aeb12e029deeebb086d7 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 6 Apr 2017 15:34:17 +0200 Subject: [PATCH 0310/1066] [fix] Avoid to remove a apt package accidentally (#292) * [fix] Avoid to remove a apt package accidentally * [fix] No need --no-remove option with apt update cmd --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 368e61cd6..b292a6c8e 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -43,7 +43,7 @@ ynh_package_update() { # usage: ynh_package_install name [name [...]] # | arg: name - the package name to install ynh_package_install() { - ynh_apt -o Dpkg::Options::=--force-confdef \ + ynh_apt --no-remove -o Dpkg::Options::=--force-confdef \ -o Dpkg::Options::=--force-confold install $@ } From a4c487a0aad96e1fe69edec6cc917f2e130f247f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 6 Apr 2017 22:21:25 +0200 Subject: [PATCH 0311/1066] [enh] Refactor applist management (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [mod] directly use python to retreive json list * [enh] app_fetchlist fetch all app_list by default * [fix] name variable doesn't exists here * [fix] re returns None when there is not matchs * [enh] app_fetchlist fetch all app_list by default * Some cleaning for better readability * Simpler variable name * Prepare a function that register lists to be fetched * Skeletong for applist system migration * Add implementation of migration system with tests * Refactorize app_fetchlist * Misc fixes + adding test for single app fetching * Fixing a few issues + test removelist * Adding fetchlist and cron install during postinstall * Adding debug messages * Adding particular exception for SSL connection error * Update actionmap help * We don't use urlretrieve * Clean tests, some description were bad * [mod] some cleaning * Moving to a .json file to store lists url + adjusting tests * Adding missing string in locale * Moving exception to logger.error when fetching fails * Adding name of applist in error messages * Fixing cron job stuff + adding proper tests * Using None instead of -1 for applist lastupdate * Handling exceptions when writing applist files * More exception handling... * [mod] pep8 * Updating test for migration of conflicting lists * More general error when return code is not 200 * [enh] Improve app_fetchlist help. * [fix] Use appslist instead of applist. * [fix] Consistent user string for translation. --- data/actionsmap/yunohost.yml | 14 +- locales/en.json | 15 +- src/yunohost/app.py | 341 +++++++++++++++++++----- src/yunohost/tests/test_appslist.py | 389 ++++++++++++++++++++++++++++ src/yunohost/tools.py | 11 +- 5 files changed, 693 insertions(+), 77 deletions(-) create mode 100644 src/yunohost/tests/test_appslist.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index c37a2c2f1..8d549ebb3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -391,28 +391,28 @@ app: ### app_fetchlist() fetchlist: - action_help: Fetch application list from app server + action_help: Fetch application lists from app servers, or register a new one. api: PUT /appslists arguments: - -u: - full: --url - help: URL of remote JSON list (default https://app.yunohost.org/official.json) -n: full: --name - help: Name of the list (default yunohost) + help: Name of the list to fetch (fetches all registered lists if empty) extra: pattern: &pattern_listname - !!str ^[a-z0-9_]+$ - "pattern_listname" + -u: + full: --url + help: URL of a new application list to register. To be specified with -n. ### app_listlists() listlists: - action_help: List fetched lists + action_help: List registered application lists api: GET /appslists ### app_removelist() removelist: - action_help: Remove list from the repositories + action_help: Remove and forget about a given application list api: DELETE /appslists arguments: name: diff --git a/locales/en.json b/locales/en.json index 9c3d1ce76..b0dad75b4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -28,11 +28,16 @@ "app_unsupported_remote_type": "Unsupported remote type used for the app", "app_upgrade_failed": "Unable to upgrade {app:s}", "app_upgraded": "{app:s} has been upgraded", - "appslist_fetched": "The app list has been fetched", - "appslist_removed": "The app list has been removed", - "appslist_retrieve_error": "Unable to retrieve the remote app list: {error}", - "appslist_retrieve_bad_format": "Retrieved file is not a valid app list", - "appslist_unknown": "Unknown app list", + "appslist_fetched": "The application list {appslist:s} has been fetched", + "appslist_removed": "The application list {appslist:s} has been removed", + "appslist_unknown": "Application list {appslist:s} unknown.", + "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", + "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not a valid app list", + "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", + "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", + "appslist_migrating": "Migrating application list {appslist:s} ...", + "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", + "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", "ask_current_admin_password": "Current administration password", "ask_email": "Email address", "ask_firstname": "First name", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 226fbdb93..66e5785da 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -34,6 +34,9 @@ import urlparse import errno import subprocess import requests +import glob +import pwd +import grp from collections import OrderedDict from moulinette.core import MoulinetteError @@ -44,11 +47,12 @@ from yunohost.utils import packages logger = getActionLogger('yunohost.app') -REPO_PATH = '/var/cache/yunohost/repo' -APPS_PATH = '/usr/share/yunohost/apps' -APPS_SETTING_PATH= '/etc/yunohost/apps/' -INSTALL_TMP = '/var/cache/yunohost' -APP_TMP_FOLDER = INSTALL_TMP + '/from_file' +REPO_PATH = '/var/cache/yunohost/repo' +APPS_PATH = '/usr/share/yunohost/apps' +APPS_SETTING_PATH = '/etc/yunohost/apps/' +INSTALL_TMP = '/var/cache/yunohost' +APP_TMP_FOLDER = INSTALL_TMP + '/from_file' +APPSLISTS_JSON = '/etc/yunohost/appslists.json' re_github_repo = re.compile( r'^(http[s]?://|git@)github.com[/:]' @@ -65,66 +69,122 @@ def app_listlists(): """ List fetched lists - """ - list_list = [] - try: - for filename in os.listdir(REPO_PATH): - if '.json' in filename: - list_list.append(filename[:len(filename)-5]) - except OSError: - raise MoulinetteError(1, m18n.n('no_appslist_found')) - return { 'lists' : list_list } + # Migrate appslist system if needed + # XXX move to a migration when those are implemented + if _using_legacy_appslist_system(): + _migrate_appslist_system() + + # Get the list + appslist_list = _read_appslist_list() + + return appslist_list def app_fetchlist(url=None, name=None): """ - Fetch application list from app server + Fetch application list(s) from app server. By default, fetch all lists. Keyword argument: - name -- Name of the list (default yunohost) - url -- URL of remote JSON list (default https://app.yunohost.org/official.json) - + name -- Name of the list + url -- URL of remote JSON list """ - # Create app path if not exists + # If needed, create folder where actual appslists are stored if not os.path.exists(REPO_PATH): os.makedirs(REPO_PATH) - if url is None: - url = 'https://app.yunohost.org/official.json' - name = 'yunohost' - elif name is None: - raise MoulinetteError(errno.EINVAL, - m18n.n('custom_appslist_name_required')) + # Migrate appslist system if needed + # XXX move that to a migration once they are finished + if _using_legacy_appslist_system(): + _migrate_appslist_system() - # Download file - try: - applist_request = requests.get(url, timeout=30) - except Exception as e: - raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error', error=str(e))) + # Read the list of appslist... + appslists = _read_appslist_list() - if (applist_request.status_code != 200): - raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_error', error="404, not found")) + # Determine the list of appslist to be fetched + appslists_to_be_fetched = [] - # Validate app list format - # TODO / Possible improvement : better validation for app list (check that - # json fields actually look like an app list and not any json file) - applist = applist_request.text - try: - json.loads(applist) - except ValueError, e: - raise MoulinetteError(errno.EBADR, m18n.n('appslist_retrieve_bad_format')) + # If a url and and a name is given, try to register new list, + # the fetch only this list + if url is not None: + if name: + _register_new_appslist(url, name) + # Refresh the appslists dict + appslists = _read_appslist_list() + appslists_to_be_fetched = [name] + else: + raise MoulinetteError(errno.EINVAL, + m18n.n('custom_appslist_name_required')) - # Write app list to file - list_file = '%s/%s.json' % (REPO_PATH, name) - with open(list_file, "w") as f: - f.write(applist) + # If a name is given, look for an appslist with that name and fetch it + elif name is not None: + if name not in appslists.keys(): + raise MoulinetteError(errno.EINVAL, + m18n.n('appslist_unknown', appslist=name)) + else: + appslists_to_be_fetched = [name] - # Setup a cron job to re-fetch the list at midnight - open("/etc/cron.d/yunohost-applist-%s" % name, "w").write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) + # Otherwise, fetch all lists + else: + appslists_to_be_fetched = appslists.keys() - logger.success(m18n.n('appslist_fetched')) + # Fetch all appslists to be fetched + for name in appslists_to_be_fetched: + + url = appslists[name]["url"] + + logger.debug("Attempting to fetch list %s at %s" % (name, url)) + + # Download file + try: + appslist_request = requests.get(url, timeout=30) + except requests.exceptions.SSLError: + logger.error(m18n.n('appslist_retrieve_error', + appslist=name, + error="SSL connection error")) + continue + except Exception as e: + logger.error(m18n.n('appslist_retrieve_error', + appslist=name, + error=str(e))) + continue + if appslist_request.status_code != 200: + logger.error(m18n.n('appslist_retrieve_error', + appslist=name, + error="Server returned code %s " % + str(appslist_request.status_code))) + continue + + # Validate app list format + # TODO / Possible improvement : better validation for app list (check + # that json fields actually look like an app list and not any json + # file) + appslist = appslist_request.text + try: + json.loads(appslist) + except ValueError, e: + logger.error(m18n.n('appslist_retrieve_bad_format', + appslist=name)) + continue + + # Write app list to file + list_file = '%s/%s.json' % (REPO_PATH, name) + try: + with open(list_file, "w") as f: + f.write(appslist) + except Exception as e: + raise MoulinetteError(errno.EIO, + "Error while writing appslist %s: %s" % + (name, str(e))) + + now = int(time.time()) + appslists[name]["lastUpdate"] = now + + logger.success(m18n.n('appslist_fetched', appslist=name)) + + # Write updated list of appslist + _write_appslist_list(appslists) def app_removelist(name): @@ -135,13 +195,22 @@ def app_removelist(name): name -- Name of the list to remove """ - try: - os.remove('%s/%s.json' % (REPO_PATH, name)) - os.remove("/etc/cron.d/yunohost-applist-%s" % name) - except OSError: - raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown')) + appslists = _read_appslist_list() - logger.success(m18n.n('appslist_removed')) + # Make sure we know this appslist + if name not in appslists.keys(): + raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) + + # Remove json + json_path = '%s/%s.json' % (REPO_PATH, name) + if os.path.exists(json_path): + os.remove(json_path) + + # Forget about this appslist + del appslists[name] + _write_appslist_list(appslists) + + logger.success(m18n.n('appslist_removed', appslist=name)) def app_list(filter=None, raw=False, installed=False, with_backup=False): @@ -162,19 +231,18 @@ def app_list(filter=None, raw=False, installed=False, with_backup=False): app_dict = {} list_dict = {} if raw else [] - try: - applists = app_listlists()['lists'] - applists[0] - except (IOError, IndexError): - app_fetchlist() - applists = app_listlists()['lists'] + appslists = _read_appslist_list() - # Construct a dictionnary of apps, based on known app lists - for applist in applists: - with open(os.path.join(REPO_PATH, applist + '.json')) as json_list: - for app, info in json.load(json_list).items(): + for appslist in appslists.keys(): + + json_path = "%s/%s.json" % (REPO_PATH, appslist) + if not os.path.exists(json_path): + app_fetchlist(name=appslist) + + with open(json_path) as json_list: + for app, info in json.loads(str(json_list.read())).items(): if app not in app_dict: - info['repository'] = applist + info['repository'] = appslist app_dict[app] = info # Get app list from the app settings directory @@ -1657,6 +1725,151 @@ def _parse_app_instance_name(app_instance_name): app_instance_nb = int(match.groupdict().get('appinstancenb')) if match.groupdict().get('appinstancenb') is not None else 1 return (appid, app_instance_nb) + +def _using_legacy_appslist_system(): + """ + Return True if we're using the old fetchlist scheme. + This is determined by the presence of some cron job yunohost-appslist-foo + """ + + return glob.glob("/etc/cron.d/yunohost-appslist-*") != [] + + +def _migrate_appslist_system(): + """ + Migrate from the legacy fetchlist system to the new one + """ + legacy_crons = glob.glob("/etc/cron.d/yunohost-appslist-*") + + for cron_path in legacy_crons: + appslist_name = os.path.basename(cron_path).replace("yunohost-appslist-", "") + logger.info(m18n.n('appslist_migrating', appslist=appslist_name)) + + # Parse appslist url in cron + cron_file_content = open(cron_path).read().strip() + appslist_url_parse = re.search("-u (https?://[^ ]+)", cron_file_content) + + # Abort if we did not find an url + if not appslist_url_parse or not appslist_url_parse.groups(): + # Bkp the old cron job somewhere else + bkp_file = "/etc/yunohost/%s.oldlist.bkp" % appslist_name + os.rename(cron_path, bkp_file) + # Notice the user + logger.warning(m18n.n('appslist_could_not_migrate', + appslist=appslist_name, + bkp_file=bkp_file)) + # Otherwise, register the list and remove the legacy cron + else: + appslist_url = appslist_url_parse.groups()[0] + try: + _register_new_appslist(appslist_url, appslist_name) + # Might get an exception if two legacy cron jobs conflict + # in terms of url... + except Exception as e: + logger.error(str(e)) + # Bkp the old cron job somewhere else + bkp_file = "/etc/yunohost/%s.oldlist.bkp" % appslist_name + os.rename(cron_path, bkp_file) + # Notice the user + logger.warning(m18n.n('appslist_could_not_migrate', + appslist=appslist_name, + bkp_file=bkp_file)) + else: + os.remove(cron_path) + + +def _install_appslist_fetch_cron(): + + cron_job_file = "/etc/cron.daily/yunohost-fetch-appslists" + + logger.debug("Installing appslist fetch cron job") + + with open(cron_job_file, "w") as f: + f.write('#!/bin/bash\n\nyunohost app fetchlist > /dev/null 2>&1\n') + + _set_permissions(cron_job_file, "root", "root", 0755) + + +# FIXME - Duplicate from certificate.py, should be moved into a common helper +# thing... +def _set_permissions(path, user, group, permissions): + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + + os.chown(path, uid, gid) + os.chmod(path, permissions) + + +def _read_appslist_list(): + """ + Read the json corresponding to the list of appslists + """ + + # If file does not exists yet, return empty dict + if not os.path.exists(APPSLISTS_JSON): + return {} + + # Read file content + with open(APPSLISTS_JSON, "r") as f: + appslists_json = f.read() + + # Parse json, throw exception if what we got from file is not a valid json + try: + appslists = json.loads(appslists_json) + except ValueError: + raise MoulinetteError(errno.EBADR, + m18n.n('appslist_corrupted_json', filename=APPSLISTS_JSON)) + + return appslists + + +def _write_appslist_list(appslist_lists): + """ + Update the json containing list of appslists + """ + + # Write appslist list + try: + with open(APPSLISTS_JSON, "w") as f: + json.dump(appslist_lists, f) + except Exception as e: + raise MoulinetteError(errno.EIO, + "Error while writing list of appslist %s: %s" % + (APPSLISTS_JSON, str(e))) + + +def _register_new_appslist(url, name): + """ + Add a new appslist to be fetched regularly. + Raise an exception if url or name conflicts with an existing list. + """ + + appslist_list = _read_appslist_list() + + # Check if name conflicts with an existing list + if name in appslist_list: + raise MoulinetteError(errno.EEXIST, + m18n.n('appslist_name_already_tracked', name=name)) + + # Check if url conflicts with an existing list + known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()] + + if url in known_appslist_urls: + raise MoulinetteError(errno.EEXIST, + m18n.n('appslist_url_already_tracked', url=url)) + + logger.debug("Registering new appslist %s at %s" % (name, url)) + + appslist_list[name] = { + "url": url, + "lastUpdate": None + } + + _write_appslist_list(appslist_list) + + _install_appslist_fetch_cron() + + def is_true(arg): """ Convert a string into a boolean diff --git a/src/yunohost/tests/test_appslist.py b/src/yunohost/tests/test_appslist.py new file mode 100644 index 000000000..4750e32a8 --- /dev/null +++ b/src/yunohost/tests/test_appslist.py @@ -0,0 +1,389 @@ +import os +import pytest +import requests +import requests_mock +import glob +import time + +from moulinette.core import MoulinetteError + +from yunohost.app import app_fetchlist, app_removelist, app_listlists, _using_legacy_appslist_system, _migrate_appslist_system, _register_new_appslist + +URL_OFFICIAL_APP_LIST = "https://app.yunohost.org/official.json" +REPO_PATH = '/var/cache/yunohost/repo' +APPSLISTS_JSON = '/etc/yunohost/appslists.json' + + +def setup_function(function): + + # Clear all appslist + files = glob.glob(REPO_PATH+"/*") + for f in files: + os.remove(f) + + # Clear appslist crons + files = glob.glob("/etc/cron.d/yunohost-appslist-*") + for f in files: + os.remove(f) + + if os.path.exists("/etc/cron.daily/yunohost-fetch-appslists"): + os.remove("/etc/cron.daily/yunohost-fetch-appslists") + + if os.path.exists(APPSLISTS_JSON): + os.remove(APPSLISTS_JSON) + + +def teardown_function(function): + pass + + +def cron_job_is_there(): + r = os.system("run-parts -v --test /etc/cron.daily/ | grep yunohost-fetch-appslists") + return r == 0 + + +############################################################################### +# Test listing of appslists and registering of appslists # +############################################################################### + + +def test_appslist_list_empty(): + """ + Calling app_listlists() with no registered list should return empty dict + """ + + assert app_listlists() == {} + + +def test_appslist_list_register(): + """ + Register a new list + """ + + # Assume we're starting with an empty app list + assert app_listlists() == {} + + # Register a new dummy list + _register_new_appslist("https://lol.com/appslist.json", "dummy") + + appslist_dict = app_listlists() + assert "dummy" in appslist_dict.keys() + assert appslist_dict["dummy"]["url"] == "https://lol.com/appslist.json" + + assert cron_job_is_there() + + +def test_appslist_list_register_conflict_name(): + """ + Attempt to register a new list with conflicting name + """ + + _register_new_appslist("https://lol.com/appslist.json", "dummy") + with pytest.raises(MoulinetteError): + _register_new_appslist("https://lol.com/appslist2.json", "dummy") + + appslist_dict = app_listlists() + + assert "dummy" in appslist_dict.keys() + assert "dummy2" not in appslist_dict.keys() + + +def test_appslist_list_register_conflict_url(): + """ + Attempt to register a new list with conflicting url + """ + + _register_new_appslist("https://lol.com/appslist.json", "dummy") + with pytest.raises(MoulinetteError): + _register_new_appslist("https://lol.com/appslist.json", "plopette") + + appslist_dict = app_listlists() + + assert "dummy" in appslist_dict.keys() + assert "plopette" not in appslist_dict.keys() + + +############################################################################### +# Test fetching of appslists # +############################################################################### + + +def test_appslist_fetch(): + """ + Do a fetchlist and test the .json got updated. + """ + assert app_listlists() == {} + + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + + with requests_mock.Mocker() as m: + + # Mock the server response with a valid (well, empty, yep) json + m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ }') + + official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"] + app_fetchlist() + new_official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"] + + assert new_official_lastUpdate > official_lastUpdate + + +def test_appslist_fetch_single_appslist(): + """ + Register several lists but only fetch one. Check only one got updated. + """ + + assert app_listlists() == {} + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + _register_new_appslist("https://lol.com/appslist.json", "dummy") + + time.sleep(1) + + with requests_mock.Mocker() as m: + + # Mock the server response with a valid (well, empty, yep) json + m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ }') + + official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"] + dummy_lastUpdate = app_listlists()["dummy"]["lastUpdate"] + app_fetchlist(name="yunohost") + new_official_lastUpdate = app_listlists()["yunohost"]["lastUpdate"] + new_dummy_lastUpdate = app_listlists()["dummy"]["lastUpdate"] + + assert new_official_lastUpdate > official_lastUpdate + assert new_dummy_lastUpdate == dummy_lastUpdate + + +def test_appslist_fetch_unknownlist(): + """ + Attempt to fetch an unknown list + """ + + assert app_listlists() == {} + + with pytest.raises(MoulinetteError): + app_fetchlist(name="swag") + + +def test_appslist_fetch_url_but_no_name(): + """ + Do a fetchlist with url given, but no name given + """ + + with pytest.raises(MoulinetteError): + app_fetchlist(url=URL_OFFICIAL_APP_LIST) + + +def test_appslist_fetch_badurl(): + """ + Do a fetchlist with a bad url + """ + + app_fetchlist(url="https://not.a.valid.url/plop.json", name="plop") + + +def test_appslist_fetch_badfile(): + """ + Do a fetchlist and mock a response with a bad json + """ + assert app_listlists() == {} + + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + + with requests_mock.Mocker() as m: + + m.register_uri("GET", URL_OFFICIAL_APP_LIST, text='{ not json lol }') + + app_fetchlist() + + +def test_appslist_fetch_404(): + """ + Do a fetchlist and mock a 404 response + """ + assert app_listlists() == {} + + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + + with requests_mock.Mocker() as m: + + m.register_uri("GET", URL_OFFICIAL_APP_LIST, status_code=404) + + app_fetchlist() + + +def test_appslist_fetch_sslerror(): + """ + Do a fetchlist and mock an SSL error + """ + assert app_listlists() == {} + + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + + with requests_mock.Mocker() as m: + + m.register_uri("GET", URL_OFFICIAL_APP_LIST, + exc=requests.exceptions.SSLError) + + app_fetchlist() + + +def test_appslist_fetch_timeout(): + """ + Do a fetchlist and mock a timeout + """ + assert app_listlists() == {} + + _register_new_appslist(URL_OFFICIAL_APP_LIST, "yunohost") + + with requests_mock.Mocker() as m: + + m.register_uri("GET", URL_OFFICIAL_APP_LIST, + exc=requests.exceptions.ConnectTimeout) + + app_fetchlist() + + +############################################################################### +# Test remove of appslist # +############################################################################### + + +def test_appslist_remove(): + """ + Register a new appslist, then remove it + """ + + # Assume we're starting with an empty app list + assert app_listlists() == {} + + # Register a new dummy list + _register_new_appslist("https://lol.com/appslist.json", "dummy") + app_removelist("dummy") + + # Should end up with no list registered + assert app_listlists() == {} + + +def test_appslist_remove_unknown(): + """ + Attempt to remove an unknown list + """ + + with pytest.raises(MoulinetteError): + app_removelist("dummy") + + +############################################################################### +# Test migration from legacy appslist system # +############################################################################### + + +def add_legacy_cron(name, url): + with open("/etc/cron.d/yunohost-appslist-%s" % name, "w") as f: + f.write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) + + +def test_appslist_check_using_legacy_system_testFalse(): + """ + If no legacy cron job is there, the check should return False + """ + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert _using_legacy_appslist_system() is False + + +def test_appslist_check_using_legacy_system_testTrue(): + """ + If there's a legacy cron job, the check should return True + """ + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + add_legacy_cron("yunohost", "https://app.yunohost.org/official.json") + assert _using_legacy_appslist_system() is True + + +def test_appslist_system_migration(): + """ + Test that legacy cron jobs get migrated correctly when calling app_listlists + """ + + # Start with no legacy cron, no appslist registered + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert app_listlists() == {} + assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") + + # Add a few legacy crons + add_legacy_cron("yunohost", "https://app.yunohost.org/official.json") + add_legacy_cron("dummy", "https://swiggitty.swaggy.lol/yolo.json") + + # Migrate + assert _using_legacy_appslist_system() is True + _migrate_appslist_system() + assert _using_legacy_appslist_system() is False + + # No legacy cron job should remain + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + + # Check they are in app_listlists anyway + appslist_dict = app_listlists() + assert "yunohost" in appslist_dict.keys() + assert appslist_dict["yunohost"]["url"] == "https://app.yunohost.org/official.json" + assert "dummy" in appslist_dict.keys() + assert appslist_dict["dummy"]["url"] == "https://swiggitty.swaggy.lol/yolo.json" + + assert cron_job_is_there() + + +def test_appslist_system_migration_badcron(): + """ + Test the migration on a bad legacy cron (no url found inside cron job) + """ + + # Start with no legacy cron, no appslist registered + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert app_listlists() == {} + assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") + + # Add a "bad" legacy cron + add_legacy_cron("wtflist", "ftp://the.fuck.is.this") + + # Migrate + assert _using_legacy_appslist_system() is True + _migrate_appslist_system() + assert _using_legacy_appslist_system() is False + + # No legacy cron should remain, but it should be backuped in /etc/yunohost + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert os.path.exists("/etc/yunohost/wtflist.oldlist.bkp") + + # Appslist should still be empty + assert app_listlists() == {} + + +def test_appslist_system_migration_conflict(): + """ + Test migration of conflicting cron job (in terms of url) + """ + + # Start with no legacy cron, no appslist registered + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert app_listlists() == {} + assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") + + # Add a few legacy crons + add_legacy_cron("yunohost", "https://app.yunohost.org/official.json") + add_legacy_cron("dummy", "https://app.yunohost.org/official.json") + + # Migrate + assert _using_legacy_appslist_system() is True + _migrate_appslist_system() + assert _using_legacy_appslist_system() is False + + # No legacy cron job should remain + assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + + # Only one among "dummy" and "yunohost" should be listed + appslist_dict = app_listlists() + assert (len(appslist_dict.keys()) == 1) + assert ("dummy" in appslist_dict.keys()) or ("yunohost" in appslist_dict.keys()) + + assert cron_job_is_there() diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5f1e0bb0f..8a9cb0664 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -38,7 +38,7 @@ import apt.progress from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger -from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list +from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import dyndns_subscribe from yunohost.firewall import firewall_upnp @@ -327,6 +327,15 @@ def tools_postinstall(domain, password, ignore_dyndns=False): # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) + # Setup the default official app list with cron job + try: + app_fetchlist(name="yunohost", + url="https://app.yunohost.org/official.json") + except Exception as e: + logger.warning(str(e)) + + _install_appslist_fetch_cron() + os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time From f8ace3153adebc600b9fd6124168a16dae55551c Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Sat, 8 Apr 2017 11:30:21 +0200 Subject: [PATCH 0312/1066] new text proposal "appslist_retrieve_bad_format" Fix: https://dev.yunohost.org/issues/895 --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index b0dad75b4..f5ceac24a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -32,7 +32,7 @@ "appslist_removed": "The application list {appslist:s} has been removed", "appslist_unknown": "Application list {appslist:s} unknown.", "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", - "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not a valid app list", + "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", "appslist_migrating": "Migrating application list {appslist:s} ...", From 804daf63bf4a536ee2070a1b01b69092b053c160 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Sun, 9 Apr 2017 16:03:02 +0200 Subject: [PATCH 0313/1066] rewrite proposal for certmanager_http_check_timeout hairpinning is the answer to the problem, not the problem itself. We should probably rewrite the whole sentence, but to keep it simple, let's just add it is an issue related to hairpinning. --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index f5ceac24a..09a2422ca 100644 --- a/locales/en.json +++ b/locales/en.json @@ -274,7 +274,7 @@ "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", - "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning or the firewall/router ahead of your server is misconfigured.", + "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" } From 674d63953072550744c3e1c9cb6d6b40ee00b21e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Apr 2017 16:46:57 +0200 Subject: [PATCH 0314/1066] [enh] Adding new port availability checker (#266) * Adding new port availability checker in tools * [fix] move import at file's beginning. * Moving back import inside function * Using boolean instead of Yes/No * Using built-in depreciation mechanism in Moulinette * We're now using boolean instead of yes/no * Renaming to port-available * Returning directly a boolean --- data/actionsmap/yunohost.yml | 11 +++++++++++ src/yunohost/app.py | 13 ++++++------- src/yunohost/tools.py | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8d549ebb3..502d0b7d5 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -550,6 +550,7 @@ app: checkport: action_help: Check availability of a local port api: GET /tools/checkport + deprecated: true arguments: port: help: Port to check @@ -1345,6 +1346,16 @@ tools: help: Show private data (domain, IP) action: store_true + ### tools_port_available() + port-available: + action_help: Check availability of a local port + api: GET /tools/portavailable + arguments: + port: + help: Port to check + extra: + pattern: *pattern_port + ############################# # Hook # diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 66e5785da..622e5fb77 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -29,7 +29,6 @@ import shutil import yaml import time import re -import socket import urlparse import errno import subprocess @@ -955,12 +954,12 @@ def app_checkport(port): port -- Port to check """ - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(1) - s.connect(("localhost", int(port))) - s.close() - except socket.error: + + # This import cannot be moved on top of file because it create a recursive + # import... + from yunohost.tools import tools_portavailable + availability = tools_portavailable(port) + if availability["available"]: logger.success(m18n.n('port_available', port=int(port))) else: raise MoulinetteError(errno.EINVAL, diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8a9cb0664..805eb2c45 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -31,6 +31,7 @@ import errno import logging import subprocess import pwd +import socket from collections import OrderedDict import apt @@ -574,3 +575,22 @@ def tools_diagnosis(auth, private=False): diagnosis['private']['domains'] = domain_list(auth)['domains'] return diagnosis + + +def tools_port_available(port): + """ + Check availability of a local port + + Keyword argument: + port -- Port to check + + """ + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect(("localhost", int(port))) + s.close() + except socket.error: + return True + else: + return False From f646fdf2725eafa84f59499172dba0f12db55205 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Apr 2017 16:47:51 +0200 Subject: [PATCH 0315/1066] [fix] Split checkurl into two functions : availability + booking (#267) * Splitting checkurl into two functions, one to check availability, the other for booking * [fix] move import at file's beginning. * Rename bookurl to registerurl * Set registerurl as a PUT request for the api * urlavailable returns a boolean now * Revert moving import to top of file :/ * Have domain and path as separate arguments * Flagging checkurl as deprecated in the actionmap * Adding unit tests for registerurl and related * Using built-in deprectation mechanism of Moulinette * Using - separator in names + moving url-available to domain * Returning directly a bool in url-available --- data/actionsmap/yunohost.yml | 32 +++++++++++++++ locales/en.json | 3 ++ src/yunohost/app.py | 36 +++++++++++++++++ src/yunohost/domain.py | 57 ++++++++++++++++++++++++++ src/yunohost/tests/test_appurl.py | 67 +++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 src/yunohost/tests/test_appurl.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 502d0b7d5..f44647ef5 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -368,6 +368,21 @@ domain: help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. action: store_true + ### domain_url_available() + url-available: + action_help: Check availability of a web path + api: GET /domain/urlavailable + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domain: + help: The domain for the web path (e.g. your.domain.tld) + extra: + pattern: *pattern_domain + path: + help: The path to check (e.g. /coffee) + ### domain_info() # info: @@ -563,6 +578,7 @@ app: checkurl: action_help: Check availability of a web path api: GET /tools/checkurl + deprecated: True configuration: authenticate: all authenticator: ldap-anonymous @@ -573,6 +589,22 @@ app: full: --app help: Write domain & path to app settings for further checks + ### app_register_url() + register-url: + action_help: Book/register a web path for a given app + api: PUT /tools/registerurl + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + app: + help: App which will use the web path + domain: + help: The domain on which the app should be registered (e.g. your.domain.tld) + path: + help: The path to be registered (e.g. /coffee) + + ### app_initdb() initdb: action_help: Create database and initialize it with optionnal attached script diff --git a/locales/en.json b/locales/en.json index 09a2422ca..41c3c2098 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,6 +4,7 @@ "admin_password_change_failed": "Unable to change password", "admin_password_changed": "The administration password has been changed", "app_already_installed": "{app:s} is already installed", + "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", "app_argument_required": "Argument '{name:s}' is required", @@ -13,6 +14,7 @@ "app_install_files_invalid": "Invalid installation files", "app_location_already_used": "An app is already installed in this location", "app_location_install_failed": "Unable to install the app in this location", + "app_location_unavailable": "This url is not available or conflicts with an already installed app", "app_manifest_invalid": "Invalid app manifest", "app_no_upgrade": "No app to upgrade", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", @@ -119,6 +121,7 @@ "hook_name_unknown": "Unknown hook name '{name:s}'", "installation_complete": "Installation complete", "installation_failed": "Installation failed", + "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "ldap_initialized": "LDAP has been initialized", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 622e5fb77..f3f945d1e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -966,6 +966,42 @@ def app_checkport(port): m18n.n('port_unavailable', port=int(port))) +def app_register_url(auth, app, domain, path): + """ + Book/register a web path for a given app + + Keyword argument: + app -- App which will use the web path + domain -- The domain on which the app should be registered (e.g. your.domain.tld) + path -- The path to be registered (e.g. /coffee) + """ + + # This line can't be moved on top of file, otherwise it creates an infinite + # loop of import with tools.py... + from domain import domain_url_available, _normalize_domain_path + + domain, path = _normalize_domain_path(domain, path) + + # We cannot change the url of an app already installed simply by changing + # the settings... + # FIXME should look into change_url once it's merged + + installed = app in app_list(installed=True, raw=True).keys() + if installed: + settings = _get_app_settings(app) + if "path" in settings.keys() and "domain" in settings.keys(): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_already_installed_cant_change_url')) + + # Check the url is available + if not domain_url_available(auth, domain, path): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_location_unavailable')) + + app_setting(app, 'domain', value=domain) + app_setting(app, 'path', value=path) + + def app_checkurl(auth, url, app=None): """ Check availability of a web path diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 37636229e..e869df6d0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -275,6 +275,45 @@ def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=Fal return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) +def domain_url_available(auth, domain, path): + """ + Check availability of a web path + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + """ + + domain, path = _normalize_domain_path(domain, path) + + # Abort if domain is unknown + if domain not in domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + + # This import cannot be put on top of file because it would create a + # recursive import... + from yunohost.app import app_map + + # Fetch apps map + apps_map = app_map(raw=True) + + # Loop through all apps to check if path is taken by one of them + available = True + if domain in apps_map: + # Loop through apps + for p, a in apps_map[domain].items(): + if path == p: + available = False + break + # We also don't want conflicts with other apps starting with + # same name + elif path.startswith(p) or p.startswith(path): + available = False + break + + return available + + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" if protocol == 4: @@ -300,3 +339,21 @@ def _get_maindomain(): def _set_maindomain(domain): with open('/etc/yunohost/current_host', 'w') as f: f.write(domain) + + +def _normalize_domain_path(domain, path): + + # We want url to be of the format : + # some.domain.tld/foo + + # Remove http/https prefix if it's there + if domain.startswith("https://"): + domain = domain[len("https://"):] + elif domain.startswith("http://"): + domain = domain[len("http://"):] + + # Remove trailing slashes + domain = domain.rstrip("/") + path = "/" + path.strip("/") + + return domain, path diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py new file mode 100644 index 000000000..dc1dbc29b --- /dev/null +++ b/src/yunohost/tests/test_appurl.py @@ -0,0 +1,67 @@ +import pytest + +from moulinette.core import MoulinetteError, init_authenticator + +from yunohost.app import app_install, app_remove +from yunohost.domain import _get_maindomain, domain_url_available, _normalize_domain_path + +# Instantiate LDAP Authenticator +auth_identifier = ('ldap', 'ldap-anonymous') +auth_parameters = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} +auth = init_authenticator(auth_identifier, auth_parameters) + + +# Get main domain +maindomain = _get_maindomain() + + +def setup_function(function): + + try: + app_remove(auth, "register_url_app") + except: + pass + +def teardown_function(function): + + try: + app_remove(auth, "register_url_app") + except: + pass + + +def test_normalize_domain_path(): + + assert _normalize_domain_path("https://yolo.swag/", "macnuggets") == ("yolo.swag", "/macnuggets") + assert _normalize_domain_path("http://yolo.swag", "/macnuggets/") == ("yolo.swag", "/macnuggets") + assert _normalize_domain_path("yolo.swag/", "macnuggets/") == ("yolo.swag", "/macnuggets") + + +def test_urlavailable(): + + # Except the maindomain/macnuggets to be available + assert domain_url_available(auth, maindomain, "/macnuggets") + + # We don't know the domain yolo.swag + with pytest.raises(MoulinetteError): + assert domain_url_available(auth, "yolo.swag", "/macnuggets") + + +def test_registerurl(): + + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + + assert not domain_url_available(auth, maindomain, "/urlregisterapp") + + # Try installing at same location + with pytest.raises(MoulinetteError): + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + + +def test_registerurl_baddomain(): + + with pytest.raises(MoulinetteError): + app_install(auth, "./tests/apps/register_url_app_ynh", + args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp")) From 5820f79772ac4e4977c2e6bdcf6b9e35f7f5c47f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Apr 2017 16:51:03 +0200 Subject: [PATCH 0316/1066] [fix] Properly define app upgradability / Fix app part of tools update (#255) * Adding a 'upgradable' attribute directly in app_info Conflicts: src/yunohost/app.py src/yunohost/tools.py * Fixing a few weird stuff from cherry-picking --- locales/en.json | 1 + src/yunohost/app.py | 33 ++++++++++++++++++++++++--------- src/yunohost/tools.py | 35 ++++++++++++----------------------- 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/locales/en.json b/locales/en.json index 41c3c2098..9b5acd87f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -30,6 +30,7 @@ "app_unsupported_remote_type": "Unsupported remote type used for the app", "app_upgrade_failed": "Unable to upgrade {app:s}", "app_upgraded": "{app:s} has been upgraded", + "app_already_up_to_date": "{app:s} is already up to date", "appslist_fetched": "The application list {appslist:s} has been fetched", "appslist_removed": "The application list {appslist:s} has been removed", "appslist_unknown": "Application list {appslist:s} unknown.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f3f945d1e..f25d35b72 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -332,6 +332,19 @@ def app_info(app, show_status=False, raw=False): if raw: ret = app_list(filter=app, raw=True)[app] ret['settings'] = _get_app_settings(app) + + # Determine upgradability + local_update_time = ret['settings'].get('update_time', ret['settings']['install_time']) + + if 'lastUpdate' not in ret or 'git' not in ret: + upgradable = "url_required" + elif ret['lastUpdate'] > local_update_time: + upgradable = "yes" + else: + upgradable = "no" + + ret['upgradable'] = upgradable + return ret app_setting_path = APPS_SETTING_PATH + app @@ -425,16 +438,19 @@ def app_upgrade(auth, app=[], url=None, file=None): upgraded_apps = [] + apps = app + user_specified_list = True # If no app is specified, upgrade all apps - if not app: + if not apps: if not url and not file: - app = [app["id"] for app in app_list(installed=True)["apps"]] + apps = [app["id"] for app in app_list(installed=True)["apps"]] + user_specified_list = False elif not isinstance(app, list): - app = [app] + apps = [app] logger.info("Upgrading apps %s", ", ".join(app)) - for app_instance_name in app: + for app_instance_name in apps: installed = _is_installed(app_instance_name) if not installed: raise MoulinetteError(errno.ENOPKG, @@ -445,18 +461,18 @@ def app_upgrade(auth, app=[], url=None, file=None): app_dict = app_info(app_instance_name, raw=True) - locale_update_time = app_dict['settings'].get('update_time', app_dict['settings']['install_time']) - if file: manifest, extracted_app_folder = _extract_app_from_file(file) elif url: manifest, extracted_app_folder = _fetch_app_from_git(url) - elif 'lastUpdate' not in app_dict or 'git' not in app_dict: + elif app_dict["upgradable"] == "url_required": logger.warning(m18n.n('custom_app_url_required', app=app_instance_name)) continue - elif app_dict['lastUpdate'] > locale_update_time: + elif app_dict["upgradable"] == "yes": manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) else: + if user_specified_list: + logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) continue # Check requirements @@ -516,7 +532,6 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) - def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 805eb2c45..f5fc2fc01 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -356,6 +356,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): ignore_packages -- Ignore apt cache update and changelog """ + # "packages" will list upgradable packages packages = [] if not ignore_packages: cache = apt.Cache() @@ -365,8 +366,6 @@ def tools_update(ignore_apps=False, ignore_packages=False): if not cache.update(): raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) - logger.info(m18n.n('done')) - cache.open(None) cache.upgrade(True) @@ -377,37 +376,27 @@ def tools_update(ignore_apps=False, ignore_packages=False): 'fullname': pkg.fullname, 'changelog': pkg.get_changelog() }) + logger.info(m18n.n('done')) + # "apps" will list upgradable packages apps = [] if not ignore_apps: try: app_fetchlist() except MoulinetteError: + # FIXME : silent exception !? pass - app_list = os.listdir(APPS_SETTING_PATH) - if len(app_list) > 0: - for app_id in app_list: - if '__' in app_id: - original_app_id = app_id[:app_id.index('__')] - else: - original_app_id = app_id - current_app_dict = app_info(app_id, raw=True) - new_app_dict = app_info(original_app_id, raw=True) + app_list_installed = os.listdir(APPS_SETTING_PATH) + for app_id in app_list_installed: - # Custom app - if new_app_dict is None or 'lastUpdate' not in new_app_dict or 'git' not in new_app_dict: - continue + app_dict = app_info(app_id, raw=True) - if (new_app_dict['lastUpdate'] > current_app_dict['lastUpdate']) \ - or ('update_time' not in current_app_dict['settings'] \ - and (new_app_dict['lastUpdate'] > current_app_dict['settings']['install_time'])) \ - or ('update_time' in current_app_dict['settings'] \ - and (new_app_dict['lastUpdate'] > current_app_dict['settings']['update_time'])): - apps.append({ - 'id': app_id, - 'label': current_app_dict['settings']['label'] - }) + if app_dict["upgradable"] == "yes": + apps.append({ + 'id': app_id, + 'label': app_dict['settings']['label'] + }) if len(apps) == 0 and len(packages) == 0: logger.info(m18n.n('packages_no_upgrade')) From 7718ed60004d1cd34066f8467e375aa71f287adb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 18 Apr 2017 00:37:21 +0200 Subject: [PATCH 0317/1066] [fix] Properly manage resolv.conf, dns resolvers and dnsmasq (#290) * Adding resolvconf as dependency * Adding new templates for dnsmasq * Adding libnss-myhostname as dependency * tableflip.gif * Moar tableflip.gif :/ * Tweaking dns resolvers used by dnsmasq * Adding Aquilenet + moving FDN back to top * Clarifying how the resolv.dnsmasq.conf template is used * Moving nsswitch force regen back to postinstall --- data/hooks/conf_regen/43-dnsmasq | 9 +++++ data/templates/dnsmasq/plain/dnsmasq.conf | 6 ++++ data/templates/dnsmasq/plain/etcdefault | 33 +++++++++++++++++++ .../dnsmasq/plain/resolv.dnsmasq.conf | 31 +++++++++++++++++ data/templates/nsswitch/nsswitch.conf | 2 +- debian/control | 2 +- src/yunohost/tools.py | 4 +++ 7 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 data/templates/dnsmasq/plain/dnsmasq.conf create mode 100644 data/templates/dnsmasq/plain/etcdefault create mode 100644 data/templates/dnsmasq/plain/resolv.dnsmasq.conf diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index aab2b599f..e298f7eaa 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -13,6 +13,15 @@ do_pre_regen() { # create directory for pending conf dnsmasq_dir="${pending_dir}/etc/dnsmasq.d" mkdir -p "$dnsmasq_dir" + etcdefault_dir="${pending_dir}/etc/default" + mkdir -p "$etcdefault_dir" + + # add general conf files + cp plain/etcdefault ${pending_dir}/etc/default/dnsmasq + cp plain/dnsmasq.conf ${pending_dir}/etc/dnsmasq.conf + + # add resolver file + cat plain/resolv.dnsmasq.conf | grep nameserver | shuf > ${pending_dir}/etc/resolv.dnsmasq.conf # retrieve variables ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) diff --git a/data/templates/dnsmasq/plain/dnsmasq.conf b/data/templates/dnsmasq/plain/dnsmasq.conf new file mode 100644 index 000000000..12a14048a --- /dev/null +++ b/data/templates/dnsmasq/plain/dnsmasq.conf @@ -0,0 +1,6 @@ +domain-needed +expand-hosts + +listen-address=127.0.0.1 +resolv-file=/etc/resolv.dnsmasq.conf +cache-size=256 diff --git a/data/templates/dnsmasq/plain/etcdefault b/data/templates/dnsmasq/plain/etcdefault new file mode 100644 index 000000000..e62dbbf67 --- /dev/null +++ b/data/templates/dnsmasq/plain/etcdefault @@ -0,0 +1,33 @@ +# This file has five functions: +# 1) to completely disable starting dnsmasq, +# 2) to set DOMAIN_SUFFIX by running `dnsdomainname` +# 3) to select an alternative config file +# by setting DNSMASQ_OPTS to --conf-file= +# 4) to tell dnsmasq to read the files in /etc/dnsmasq.d for +# more configuration variables. +# 5) to stop the resolvconf package from controlling dnsmasq's +# idea of which upstream nameservers to use. +# For upgraders from very old versions, all the shell variables set +# here in previous versions are still honored by the init script +# so if you just keep your old version of this file nothing will break. + +#DOMAIN_SUFFIX=`dnsdomainname` +#DNSMASQ_OPTS="--conf-file=/etc/dnsmasq.alt" + +# Whether or not to run the dnsmasq daemon; set to 0 to disable. +ENABLED=1 + +# By default search this drop directory for configuration options. +# Libvirt leaves a file here to make the system dnsmasq play nice. +# Comment out this line if you don't want this. The dpkg-* are file +# endings which cause dnsmasq to skip that file. This avoids pulling +# in backups made by dpkg. +CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new + +# If the resolvconf package is installed, dnsmasq will use its output +# rather than the contents of /etc/resolv.conf to find upstream +# nameservers. Uncommenting this line inhibits this behaviour. +# Note that including a "resolv-file=" line in +# /etc/dnsmasq.conf is not enough to override resolvconf if it is +# installed: the line below must be uncommented. +IGNORE_RESOLVCONF=yes diff --git a/data/templates/dnsmasq/plain/resolv.dnsmasq.conf b/data/templates/dnsmasq/plain/resolv.dnsmasq.conf new file mode 100644 index 000000000..bc36ef365 --- /dev/null +++ b/data/templates/dnsmasq/plain/resolv.dnsmasq.conf @@ -0,0 +1,31 @@ +# This file will be used to generate /etc/resolv.dnsmasq.conf +# To avoid that every instance rely on the first server as primary +# server, this list is *shuffled* during every regen-conf of dnsmasq +# In the possibility where the first nameserver is down, dnsmasq +# will automatically switch to the next as primary server. + +# List taken from +# http://diyisp.org/dokuwiki/doku.php?id=technical:dnsresolver + +# (FR) FDN +nameserver 80.67.169.12 +nameserver 80.67.169.40 +# (FR) LDN +nameserver 80.67.188.188 +# (FR) ARN +nameserver 89.234.141.66 +# (FR) gozmail / grifon +nameserver 89.234.186.18 +# (DE) FoeBud / Digital Courage +nameserver 85.214.20.141 +# (FR) Aquilenet [added manually, following comments from @sachaz] +nameserver 141.255.128.100 +nameserver 141.255.128.101 +# (DE) CCC Berlin +nameserver 213.73.91.35 +# (DE) Ideal-Hosting +nameserver 84.200.69.80 +nameserver 84.200.70.40 +# (DK) censurfridns +nameserver 91.239.100.100 +nameserver 89.233.43.71 diff --git a/data/templates/nsswitch/nsswitch.conf b/data/templates/nsswitch/nsswitch.conf index cf5b45256..b55e01b02 100644 --- a/data/templates/nsswitch/nsswitch.conf +++ b/data/templates/nsswitch/nsswitch.conf @@ -9,7 +9,7 @@ group: compat ldap shadow: compat ldap gshadow: files -hosts: files mdns4_minimal [NOTFOUND=return] dns +hosts: files myhostname mdns4_minimal [NOTFOUND=return] dns networks: files protocols: db files diff --git a/debian/control b/debian/control index c261546dd..3be8b917e 100644 --- a/debian/control +++ b/debian/control @@ -22,7 +22,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl - , dnsmasq, openssl, avahi-daemon, libnss-mdns + , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , haveged diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f5fc2fc01..cf3152dce 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -164,6 +164,9 @@ def tools_maindomain(auth, new_domain=None): logger.warning("%s" % e, exc_info=1) raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) + # Clear nsswitch cache for hosts to make sure hostname is resolved ... + subprocess.call(['nscd', '-i', 'hosts']) + # Set hostname pretty_hostname = "(YunoHost/%s)" % new_domain commands = [ @@ -319,6 +322,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.success(m18n.n('yunohost_ca_creation_success')) # New domain config + service_regen_conf(['nsswitch'], force=True) domain_add(auth, domain, dyndns) tools_maindomain(auth, domain) From 1516f48699e81c3ed36b3481d472b4e8ee9965e4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 18 Apr 2017 00:38:40 +0200 Subject: [PATCH 0318/1066] [enh] Allow applications to ship a script to change its url (#185) * [enh] add app_change_url * [fix] avoid unecessary io and reuse already existing variable * [fix] bad comment * [fix] another bad comment * [fix] I need to be able to call yunohost during change_url scripts * [mod] global variables are now uppercased * [mod] compress condition * [enh] don't change_url if old/new domain_path are identical * [mod] i18n * [enh] ensure that nginx doesn't failed at the end of change_url * [fix] forgot to call this damn m18n * [mod] m18n * [enh] ask and requires new domain/path for change_url * [fix] missing translation key * [mod] ordering * [mod] lisibility * [enh] avoid common mistakes * [fix] check_output is annoying * [fix] locale: typo. * Adding changeurl unit test draft * [mod] remove useless imports * [mod] style * [mod] change_url -> changeurl * Moving comment about checkurl near checkurl call * Normalize new path and domain format * Adding test about trying to changeurl to same url * Internationalizing change app success message * Removing 'trimed' stuff * Moving check for change_url script at beginning of function * Use _run_service_command to reload nginx * Changing changeurl back to change-url, gomennasai :s --- data/actionsmap/yunohost.yml | 25 +++++++ locales/en.json | 5 ++ src/yunohost/app.py | 101 ++++++++++++++++++++++++++- src/yunohost/tests/test_changeurl.py | 61 ++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/tests/test_changeurl.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index f44647ef5..39c62398c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -544,6 +544,31 @@ app: full: --file help: Folder or tarball for upgrade + ### app_change_url() + change-url: + action_help: Change app's URL + api: PUT /apps//changeurl + configuration: + authenticate: all + authenticator: ldap-anonymous + lock: false + arguments: + app: + help: Target app instance name + -d: + full: --domain + help: New app domain on which the application will be moved + extra: + ask: ask_main_domain + pattern: *pattern_domain + required: True + -p: + full: --path + help: New path at which the application will be moved + extra: + ask: ask_path + required: True + ### app_setting() setting: action_help: Set or get an app setting value diff --git a/locales/en.json b/locales/en.json index 9b5acd87f..2e85d6d4b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -8,6 +8,11 @@ "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", "app_argument_required": "Argument '{name:s}' is required", + "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", + "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", + "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", + "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app is incompatible with your YunoHost version", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f25d35b72..10f617228 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -41,7 +41,7 @@ from collections import OrderedDict from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_log +from yunohost.service import service_log, _run_service_command from yunohost.utils import packages logger = getActionLogger('yunohost.app') @@ -419,6 +419,105 @@ def app_map(app=None, raw=False, user=None): return result +def app_change_url(auth, app, domain, path): + """ + Modify the URL at which an application is installed. + + Keyword argument: + app -- Taget app instance name + domain -- New app domain on which the application will be moved + path -- New path at which the application will be move + + """ + from yunohost.hook import hook_exec + + installed = _is_installed(app) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app)) + + if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_no_change_url_script", app_name=app)) + + old_domain = app_setting(app, "domain") + old_path = app_setting(app, "path") + + # Normalize path and domain format + domain = domain.strip().lower() + old_path = '/' + old_path.strip("/").strip() + '/' + path = '/' + path.strip("/").strip() + '/' + + if (domain, path) == (old_domain, old_path): + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path)) + + # WARNING / FIXME : checkurl will modify the settings + # (this is a non intuitive behavior that should be changed) + # (or checkurl renamed in reserve_url) + app_checkurl(auth, '%s%s' % (domain, path), app) + + manifest = json.load(open(os.path.join(APPS_SETTING_PATH, app, "manifest.json"))) + + # Retrieve arguments list for change_url script + # TODO: Allow to specify arguments + args_odict = _parse_args_from_manifest(manifest, 'change_url', auth=auth) + args_list = args_odict.values() + args_list.append(app) + + # Prepare env. var. to pass to script + env_dict = _make_environment_dict(args_odict) + app_id, app_instance_nb = _parse_app_instance_name(app) + env_dict["YNH_APP_ID"] = app_id + env_dict["YNH_APP_INSTANCE_NAME"] = app + env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + + env_dict["YNH_APP_OLD_DOMAIN"] = old_domain + env_dict["YNH_APP_OLD_PATH"] = old_path.rstrip("/") + env_dict["YNH_APP_NEW_DOMAIN"] = domain + env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + + if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): + shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) + + shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"), + os.path.join(APP_TMP_FOLDER, "scripts")) + + # Execute App change_url script + os.system('chown -R admin: %s' % INSTALL_TMP) + os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) + os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) + + # XXX journal + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict) != 0: + logger.error("Failed to change '%s' url." % app) + + # restore values modified by app_checkurl + # see begining of the function + app_setting(app, "domain", value=old_domain) + app_setting(app, "path", value=old_path) + + return + + # this should idealy be done in the change_url script but let's avoid common mistakes + app_setting(app, 'domain', value=domain) + app_setting(app, 'path', value=path) + + app_ssowatconf(auth) + + # avoid common mistakes + if _run_service_command("reload", "nginx") == False: + # grab nginx errors + # the "exit 0" is here to avoid check_output to fail because 'nginx -t' + # will return != 0 since we are in a failed state + nginx_errors = subprocess.check_output("nginx -t; exit 0", + stderr=subprocess.STDOUT, + shell=True).rstrip() + + raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors)) + + logger.success(m18n.n("app_change_url_success", + app=app, domain=domain, path=path)) + + def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py new file mode 100644 index 000000000..506060cbb --- /dev/null +++ b/src/yunohost/tests/test_changeurl.py @@ -0,0 +1,61 @@ +import pytest +import time +import requests + +from moulinette.core import init_authenticator +from yunohost.app import app_install, app_change_url, app_remove, app_map +from yunohost.domain import _get_maindomain + +from moulinette.core import MoulinetteError + +# Instantiate LDAP Authenticator +AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') +AUTH_PARAMETERS = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} + +auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) + +# Get main domain +maindomain = _get_maindomain() + + +def setup_function(function): + pass + + +def teardown_function(function): + app_remove(auth, "change_url_app") + + +def install_changeurl_app(path): + app_install(auth, "./tests/apps/change_url_app_ynh", + args="domain=%s&path=%s" % (maindomain, path)) + + +def check_changeurl_app(path): + appmap = app_map(raw=True) + + assert path + "/" in appmap[maindomain].keys() + + assert appmap[maindomain][path + "/"]["id"] == "change_url_app" + + r = requests.get("https://%s%s/" % (maindomain, path)) + assert r.status_code == 200 + + +def test_appchangeurl(): + install_changeurl_app("/changeurl") + check_changeurl_app("/changeurl") + + app_change_url(auth, "change_url_app", maindomain, "/newchangeurl") + + # For some reason the nginx reload can take some time to propagate ...? + time.sleep(2) + + check_changeurl_app("/newchangeurl") + +def test_appchangeurl_sameurl(): + install_changeurl_app("/changeurl") + check_changeurl_app("/changeurl") + + with pytest.raises(MoulinetteError): + app_change_url(auth, "change_url_app", maindomain, "changeurl") From c502b8c348c03acb9d1fbc8a1280ea2fd7732f6a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 24 Apr 2017 13:38:11 +0200 Subject: [PATCH 0319/1066] [fix] Add random delay to app fetchlist cron job (#297) * Add random delay to app fetchlist cron job * Split the sleep and yunohost command on two lines --- src/yunohost/app.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 10f617228..2f80d0ac2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1933,8 +1933,15 @@ def _install_appslist_fetch_cron(): logger.debug("Installing appslist fetch cron job") + cron_job = [] + cron_job.append("#!/bin/bash") + # We add a random delay between 0 and 60 min to avoid every instance fetching + # the appslist at the same time every night + cron_job.append("(sleep $((RANDOM%3600));") + cron_job.append("yunohost app fetchlist > /dev/null 2>&1) &") + with open(cron_job_file, "w") as f: - f.write('#!/bin/bash\n\nyunohost app fetchlist > /dev/null 2>&1\n') + f.write('\n'.join(cron_job)) _set_permissions(cron_job_file, "root", "root", 0755) From 3eb160707a2b7cf0f47dbd3f5bf72f6b5e6c1779 Mon Sep 17 00:00:00 2001 From: fummelmann Date: Thu, 23 Feb 2017 11:25:04 +0100 Subject: [PATCH 0320/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (272 of 272 strings) --- locales/es.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/es.json b/locales/es.json index 0d2bed3cc..b30f8a4d3 100644 --- a/locales/es.json +++ b/locales/es.json @@ -31,7 +31,7 @@ "app_upgraded": "{app:s} ha sido actualizada", "appslist_fetched": "Lista de aplicaciones ha sido descargada", "appslist_removed": "La lista de aplicaciones ha sido eliminada", - "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones", + "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones: {error}", "appslist_unknown": "Lista de aplicaciones desconocida", "ask_current_admin_password": "Contraseña administrativa actual", "ask_email": "Dirección de correo electrónico", @@ -81,7 +81,7 @@ "domain_dyndns_invalid": "Dominio no válido para usar con DynDNS", "domain_dyndns_root_unknown": "Dominio raíz de DynDNS desconocido", "domain_exists": "El dominio ya existe", - "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminar el dominio. ", + "domain_uninstall_app_first": "Una o más aplicaciones están instaladas en este dominio. Debe desinstalarlas antes de eliminar el dominio", "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zona del DNS ya existe", "domain_zone_not_found": "No se ha encontrado el archivo de zona del DNS para el dominio [:s]", @@ -271,5 +271,7 @@ "certmanager_domain_not_resolved_locally": "Su servidor Yunohost no consigue resolver el dominio {domain:s}. Esto puede suceder si modificó su registro DNS. Si es el caso, espere unas horas que se propague la modificación. Si el problema persiste, considere añadir {domain:s} a /etc/hosts. (Si sabe usted lo que está haciendo, use --no-checks para deshabilitar estas verificaciones.)", "certmanager_acme_not_configured_for_domain": "El certificado para el dominio {domain:s} no parece instalado correctamente. Ejecute primero cert-install para este dominio.", "certmanager_http_check_timeout": "Plazo expirado, el servidor no ha podido contactarse a si mismo a través de HTTP usando su dirección IP pública (dominio {domain:s} con ip {ip:s}). Puede ser debido a hairpinning o a una mala configuración del cortafuego/router al que está conectado su servidor.", - "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde." + "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde.", + "appslist_retrieve_bad_format": "El archivo recuperado no es una lista de aplicaciones válida", + "domain_hostname_failed": "Error al establecer nuevo nombre de host" } From ed4fc0f7b9a70f78cf80133a10ff751083fc77a9 Mon Sep 17 00:00:00 2001 From: fummelmann Date: Thu, 23 Feb 2017 11:16:52 +0100 Subject: [PATCH 0321/1066] [i18n] Translated using Weblate (German) Currently translated at 100.0% (272 of 272 strings) --- locales/de.json | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/locales/de.json b/locales/de.json index 4200d6c50..462bc9be9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,20 +1,20 @@ { "action_invalid": "Ungültige Aktion '{action:s}'", - "admin_password": "Verwaltungspasswort", + "admin_password": "Administrator-Passwort", "admin_password_change_failed": "Passwort kann nicht geändert werden", - "admin_password_changed": "Das Verwaltungspasswort wurde erfolgreich geändert", + "admin_password_changed": "Das Administrator-Passwort wurde erfolgreich geändert", "app_already_installed": "{app:s} ist schon installiert", - "app_argument_choice_invalid": "Invalide Auswahl für Argument '{name:s}'. Muss einer der folgenden Werte sein {choices:s}", + "app_argument_choice_invalid": "Ungültige Auswahl für Argument '{name:s}'. Es muss einer der folgenden Werte sein {choices:s}", "app_argument_invalid": "Das Argument '{name:s}' hat einen falschen Wert: {error:s}", "app_argument_required": "Argument '{name:s}' wird benötigt", "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", - "app_id_invalid": "Falsche App ID", + "app_id_invalid": "Falsche App-ID", "app_install_files_invalid": "Ungültige Installationsdateien", "app_location_already_used": "Eine andere App ist bereits an diesem Ort installiert", - "app_location_install_failed": "Die App kann an diesem Ort nicht installiert werden", - "app_manifest_invalid": "Ungültiges App Manifest", + "app_location_install_failed": "Die App kann nicht an diesem Ort installiert werden", + "app_manifest_invalid": "Ungültiges App-Verzeichnis", "app_no_upgrade": "Keine Aktualisierungen für Apps verfügbar", - "app_not_installed": "{app:s} ist nicht intalliert", + "app_not_installed": "{app:s} ist nicht installiert", "app_recent_version_required": "Für {:s} benötigt eine aktuellere Version von moulinette", "app_removed": "{app:s} wurde erfolgreich entfernt", "app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden", @@ -25,11 +25,11 @@ "appslist_removed": "Appliste wurde erfolgreich entfernt", "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden: {error}", "appslist_unknown": "Unbekannte Appliste", - "ask_current_admin_password": "Derzeitiges Verwaltungspasswort", - "ask_email": "E-Mail Adresse", + "ask_current_admin_password": "Derzeitiges Administrator-Kennwort", + "ask_email": "E-Mail-Adresse", "ask_firstname": "Vorname", "ask_lastname": "Nachname", - "ask_list_to_remove": "Liste entfernen", + "ask_list_to_remove": "zu entfernende Liste", "ask_main_domain": "Hauptdomain", "ask_new_admin_password": "Neues Verwaltungskennwort", "ask_password": "Passwort", @@ -66,7 +66,7 @@ "domain_dyndns_invalid": "Domain nicht mittels DynDNS nutzbar", "domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain", "domain_exists": "Die Domain existiert bereits", - "domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte deinstalliere zuerst die App, bevor du die Domain löschst.. ", + "domain_uninstall_app_first": "Mindestens eine App ist noch für diese Domain installiert. Bitte deinstalliere zuerst die App, bevor du die Domain löschst", "domain_unknown": "Unbekannte Domain", "domain_zone_exists": "DNS Zonen Datei existiert bereits", "domain_zone_not_found": "DNS Zonen Datei kann nicht für Domäne {:s} gefunden werden", @@ -219,7 +219,7 @@ "pattern_positive_number": "Muss eine positive Zahl sein", "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", - "app_incompatible": "Die Anwendung ist mit deiner YunoHost Version inkompatibel", + "app_incompatible": "Die Anwendung ist nicht mit deiner YunoHost-Version kompatibel", "app_not_correctly_installed": "{app:s} scheint nicht richtig installiert worden zu sein", "app_requirements_checking": "Überprüfe notwendige Pakete...", "app_requirements_failed": "Anforderungen werden nicht erfüllt: {error}", @@ -277,5 +277,6 @@ "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...", "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine misskonfigurierte Firewall/Router deines Servers dafür verantwortlich.", "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut.", - "appslist_retrieve_bad_format": "Die empfangene Datei ist keine gültige Appliste" + "appslist_retrieve_bad_format": "Die empfangene Datei ist keine gültige Appliste", + "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen" } From 89589ebaa7fa80284c953854008be8046547b6bb Mon Sep 17 00:00:00 2001 From: bricabraque Date: Fri, 24 Feb 2017 22:32:14 +0100 Subject: [PATCH 0322/1066] [i18n] Translated using Weblate (Italian) Currently translated at 92.2% (251 of 272 strings) --- locales/it.json | 262 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 226 insertions(+), 36 deletions(-) diff --git a/locales/it.json b/locales/it.json index 3e33edb5d..ebf4fd5db 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,63 +1,253 @@ { - "app_already_installed": "{app:s} è già installato", + "app_already_installed": "{app:s} è già installata", "app_extraction_failed": "Impossibile estrarre i file di installazione", - "app_not_installed": "{app:s} non è installato", + "app_not_installed": "{app:s} non è installata", "app_unknown": "Applicazione sconosciuta", "ask_email": "Indirizzo email", "ask_password": "Password", - "backup_archive_name_exists": "Il nome dell'archivio del backup esiste già", + "backup_archive_name_exists": "Il nome dell'archivio del backup è già esistente", "backup_created": "Backup completo", "backup_invalid_archive": "Archivio di backup non valido", - "backup_output_directory_not_empty": "Directory di output non è vuota", - "backup_running_app_script": "Esecuzione script di backup dell'applicazione '{app:s}'...", - "domain_created": "Dominio creato con successo", - "domain_dyndns_invalid": "Dominio non valido da utilizzare con DynDNS", - "domain_exists": "Dominio esiste già", - "ldap_initialized": "LDAP inizializzato con successo", - "pattern_email": "Deve essere un indirizzo e-mail valido (es someone@domain.org)", - "pattern_mailbox_quota": "Deve essere una dimensione con un suffisso b/k/M/G/T o 0 per disabilitare la quota", - "port_already_opened": "Port {port:d} è già aperto per {ip_version:s} connessioni", - "port_unavailable": "Porta {port:d} non è disponibile", - "service_add_failed": "Impossibile aggiungere servizio '{service:s}'", + "backup_output_directory_not_empty": "La directory di output non è vuota", + "backup_running_app_script": "Esecuzione del script di backup dell'applicazione '{app:s}'...", + "domain_created": "Il dominio è stato creato", + "domain_dyndns_invalid": "Il dominio non è valido per essere usato con DynDNS", + "domain_exists": "Il dominio è già esistente", + "ldap_initialized": "LDAP è stato inizializzato", + "pattern_email": "L'indirizzo email deve essere valido (es. someone@domain.org)", + "pattern_mailbox_quota": "La dimensione deve avere un suffisso b/k/M/G/T o 0 per disattivare la quota", + "port_already_opened": "La porta {port:d} è già aperta per {ip_version:s} connessioni", + "port_unavailable": "La porta {port:d} non è disponibile", + "service_add_failed": "Impossibile aggiungere il servizio '{service:s}'", "service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'", - "service_disabled": "Servizio '{service:s}' disattivato con successo", + "service_disabled": "Il servizio '{service:s}' è stato disattivato", "service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'", - "service_removed": "Servizio rimosso con successo", - "service_stop_failed": "Impossibile arrestare il servizio '{service:s}'", - "system_username_exists": "Nome utente esiste già negli utenti del sistema", - "unrestore_app": "Applicazione '{app:s}' non verrà ripristinato", + "service_removed": "Il servizio '{service:s}' è stato rimosso", + "service_stop_failed": "Impossibile fermare il servizio '{service:s}'", + "system_username_exists": "il nome utente esiste già negli utenti del sistema", + "unrestore_app": "L'applicazione '{app:s}' non verrà ripristinata", "upgrading_packages": "Aggiornamento dei pacchetti...", - "user_deleted": "Utente cancellato con successo", + "user_deleted": "L'utente è stato cancellato", "admin_password": "Password dell'amministrazione", - "admin_password_change_failed": "Non è possibile cambiare la password", + "admin_password_change_failed": "Impossibile cambiare la password", "admin_password_changed": "La password dell'amministrazione è stata cambiata", "app_incompatible": "L'app non è compatibile con la tua versione di Yunohost", "app_install_files_invalid": "Non sono validi i file di installazione", - "app_location_already_used": "Una app è già installata in questa postazione", - "app_location_install_failed": "Non è possibile installare l'app in questa postazione", - "app_manifest_invalid": "", - "app_no_upgrade": "Nessun app da aggiornare", + "app_location_already_used": "Un'app è già installata in questa posizione", + "app_location_install_failed": "Impossibile installare l'applicazione in questa posizione", + "app_manifest_invalid": "Manifesto dell'applicazione non valido", + "app_no_upgrade": "Nessun applicazione da aggiornare", "app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente", "app_not_properly_removed": "{app:s} non è stata correttamente rimossa", "action_invalid": "L'azione '{action:s}' non è valida", "app_removed": "{app:s} è stata rimossa", - "app_sources_fetch_failed": "Non è possibile recuperare i file sorgenti", - "app_upgrade_failed": "Non è possibile aggiornare {app:s}", + "app_sources_fetch_failed": "Impossibile riportare i file sorgenti", + "app_upgrade_failed": "Impossibile aggiornare {app:s}", "app_upgraded": "{app:s} è stata aggiornata", - "appslist_fetched": "La lista delle app è stata recuperata", - "appslist_removed": "La lista delle app è stata eliminata", - "app_package_need_update": "Il pacchetto della app deve esser aggiornato per seguire le modifiche di Yunohost", + "appslist_fetched": "La lista delle applicazioni è stata recuperata", + "appslist_removed": "La lista delle applicazioni è stata rimossa", + "app_package_need_update": "Il pacchetto dell'app deve esser aggiornato per seguire le modifiche di Yunohost", "app_requirements_checking": "Controllo dei pacchetti necessari...", - "app_requirements_failed": "Non è possibile rispondere ai requisiti: {error}", + "app_requirements_failed": "Impossibile rispondere ai requisiti: {error}", "app_requirements_unmeet": "Non sono soddisfatti i requisiti, il pacchetto {pkgname} ({version}) deve esser {spec}", - "appslist_unknown": "Lista sconosciuta", - "ask_current_admin_password": "Password dell'amministrazione attuale", + "appslist_unknown": "Lista di applicazioni sconosciuta", + "ask_current_admin_password": "Password attuale dell'amministrazione", "ask_firstname": "Nome", "ask_lastname": "Cognome", - "ask_list_to_remove": "Lista da eliminare", + "ask_list_to_remove": "Lista da rimuovere", "ask_main_domain": "Dominio principale", "ask_new_admin_password": "Nuova password dell'amministrazione", "backup_action_required": "Devi specificare qualcosa da salvare", - "backup_app_failed": "Non è possibile fare il backup della app '{app:s}'", - "backup_archive_app_not_found": "L'app '{app:s}' non è stata trovata nel archivio di backup" + "backup_app_failed": "Non è possibile fare il backup dell'applicazione '{app:s}'", + "backup_archive_app_not_found": "L'applicazione '{app:s}' non è stata trovata nel archivio di backup", + "app_argument_choice_invalid": "Scelta non valida per l'argomento '{name:s}', deve essere uno di {choices:s}", + "app_argument_invalid": "Valore non valido per '{name:s}': {error:s}", + "app_argument_required": "L'argomento '{name:s}' è requisito", + "app_id_invalid": "Identificativo dell'applicazione non valido", + "app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato", + "appslist_retrieve_error": "Non è possibile riportare la lista remota delle applicazioni: {error}", + "appslist_retrieve_bad_format": "Il file recuperato non è una lista di applicazioni valida", + "backup_archive_broken_link": "Non è possibile accedere al archivio di backup (link rotto verso {path:s})", + "backup_archive_hook_not_exec": "Il hook '{hook:s}' non è stato eseguito in questo backup", + "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto", + "backup_archive_open_failed": "Non è possibile aprire l'archivio di backup", + "backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup", + "backup_creating_archive": "Creazione del archivio di backup...", + "backup_creation_failed": "La creazione del backup è fallita", + "backup_delete_error": "Impossibile cancellare '{path:s}'", + "backup_deleted": "Il backup è stato cancellato", + "backup_extracting_archive": "Estrazione del archivio di backup...", + "backup_hook_unknown": "Hook di backup '{hook:s}' sconosciuto", + "backup_nothings_done": "Non c'è niente da salvare", + "backup_output_directory_forbidden": "Directory di output vietata. I backup non possono esser creati nelle sotto-cartelle /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives", + "backup_output_directory_required": "Devi fornire una directory di output per il backup", + "backup_running_hooks": "Esecuzione dei hook di backup...", + "custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}", + "custom_appslist_name_required": "Devi fornire un nome per la lista di applicazioni personalizzata", + "diagnostic_debian_version_error": "Impossibile riportare la versione di Debian: {error}", + "diagnostic_kernel_version_error": "Impossibile riportare la versione del kernel: {error}", + "diagnostic_monitor_disk_error": "Impossibile controllare i dischi: {error}", + "diagnostic_monitor_network_error": "Impossibile controllare la rete: {error}", + "diagnostic_monitor_system_error": "Impossibile controllare il sistema: {error}", + "diagnostic_no_apps": "Nessuna applicazione installata", + "dnsmasq_isnt_installed": "dnsmasq non sembra installato, impartisci il comando 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_creation_failed": "Impossibile creare un dominio", + "domain_deleted": "Il dominio è stato cancellato", + "domain_deletion_failed": "Impossibile cancellare il dominio", + "domain_dyndns_already_subscribed": "Hai già sottoscritto un dominio DynDNS", + "domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto", + "domain_hostname_failed": "La definizione del nuovo hostname è fallita", + "domain_uninstall_app_first": "Una o più applicazioni sono installate su questo dominio. Disinstalla loro prima di procedere alla cancellazione di un dominio", + "domain_unknown": "Dominio sconosciuto", + "domain_zone_exists": "Il file di zona DNS è già esistente", + "domain_zone_not_found": "Il file di zona DNS non è stato trovato per il dominio {:s}", + "done": "Terminato", + "domains_available": "Domini disponibili:", + "downloading": "Scaricamento...", + "dyndns_cron_installed": "Il cronjob DynDNS è stato installato", + "dyndns_cron_remove_failed": "Impossibile rimuovere il cronjob DynDNS", + "dyndns_cron_removed": "Il cronjob DynDNS è stato rimosso", + "dyndns_ip_update_failed": "Impossibile aggiornare l'indirizzo IP in DynDNS", + "dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato in DynDNS", + "dyndns_key_generating": "La chiave DNS sta generando, potrebbe richiedere del tempo...", + "dyndns_key_not_found": "La chiave DNS non è stata trovata per il dominio", + "dyndns_no_domain_registered": "Nessuno dominio è stato registrato con DynDNS", + "dyndns_registered": "Il dominio DynDNS è stato registrato", + "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}", + "dyndns_unavailable": "Il sottodominio DynDNS non è disponibile", + "executing_command": "Esecuzione del comando '{command:s}'...", + "executing_script": "Esecuzione dello script '{script:s}'...", + "extracting": "Estrazione...", + "field_invalid": "Campo '{:s}' non valido", + "firewall_reload_failed": "Impossibile ricaricare il firewall", + "firewall_reloaded": "Il firewall è stato ricaricato", + "firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.", + "format_datetime_short": "%m/%d/%Y %I:%M %p", + "hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}", + "hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}", + "hook_name_unknown": "Nome di hook '{name:s}' sconosciuto", + "installation_complete": "Installazione finita", + "installation_failed": "Installazione fallita", + "ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta", + "iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta", + "ldap_init_failed_to_create_admin": "L'inizializzazione LDAP non è riuscita a creare un utente admin", + "license_undefined": "Indeterminato", + "mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail:s}'", + "mail_domain_unknown": "Dominio d'indirizzo mail '{domain:s}' sconosciuto", + "mail_forward_remove_failed": "Impossibile rimuovere la mail inoltrata '{mail:s}'", + "mailbox_used_space_dovecot_down": "Il servizio di posta elettronica Dovecot deve essere attivato se vuoi riportare lo spazio usato dalla posta elettronica", + "maindomain_change_failed": "Impossibile cambiare il dominio principale", + "maindomain_changed": "Il dominio principale è stato cambiato", + "monitor_disabled": "Il monitoraggio del sistema è stato disattivato", + "monitor_enabled": "Il monitoraggio del sistema è stato attivato", + "monitor_glances_con_failed": "Impossibile collegarsi al server Glances", + "monitor_not_enabled": "Il monitoraggio del server non è attivato", + "monitor_period_invalid": "Periodo di tempo non valido", + "monitor_stats_file_not_found": "I file statistici non sono stati trovati", + "monitor_stats_no_update": "Nessuna statistica di monitoraggio da aggiornare", + "monitor_stats_period_unavailable": "Nessuna statistica disponibile per il periodo", + "mountpoint_unknown": "Punto di mount sconosciuto", + "mysql_db_creation_failed": "La creazione del database MySQL è fallita", + "mysql_db_init_failed": "L'inizializzazione del database MySQL è fallita", + "mysql_db_initialized": "Il database MySQL è stato inizializzato", + "new_domain_required": "Devi fornire il nuovo dominio principale", + "no_appslist_found": "Nessuna lista di applicazioni trovata", + "no_internet_connection": "Il server non è collegato a Internet", + "no_ipv6_connectivity": "La connessione IPv6 non è disponibile", + "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path:s}'", + "package_not_installed": "Il pacchetto '{pkgname}' non è installato", + "package_unknown": "Pacchetto '{pkgname}' sconosciuto", + "packages_no_upgrade": "Nessuno pacchetto da aggiornare", + "packages_upgrade_critical_later": "I pacchetti critici {packages:s} verranno aggiornati più tardi", + "packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti", + "path_removal_failed": "Impossibile rimuovere il percorso {:s}", + "pattern_backup_archive_name": "Deve essere un nome di file valido con caratteri alfanumerici e -_. soli", + "pattern_domain": "Deve essere un nome di dominio valido (es. il-mio-dominio.org)", + "pattern_firstname": "Deve essere un nome valido", + "pattern_lastname": "Deve essere un cognome valido", + "pattern_listname": "Caratteri alfanumerici e trattini bassi soli", + "pattern_password": "Deve contenere almeno 3 caratteri", + "pattern_port": "Deve essere un numero di porta valido (es. 0-65535)", + "pattern_port_or_range": "Deve essere un numero di porta valido (es. 0-65535) o una fascia di porte valida (es. 100:200)", + "pattern_positive_number": "Deve essere un numero positivo", + "pattern_username": "Caratteri minuscoli alfanumerici o trattini bassi soli", + "port_already_closed": "La porta {port:d} è già chiusa per le connessioni {ip_version:s}", + "port_available": "La porta {port:d} è disponibile", + "restore_action_required": "Devi specificare qualcosa da ripristinare", + "restore_already_installed_app": "Un'applicazione è già installata con l'identificativo '{app:s}'", + "restore_app_failed": "Impossibile ripristinare l'applicazione '{app:s}'", + "restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino", + "restore_complete": "Ripristino completo", + "restore_confirm_yunohost_installed": "Sei sicuro di volere ripristinare un sistema già installato? {answers:s}", + "restore_failed": "Impossibile ripristinare il sistema", + "user_update_failed": "Impossibile aggiornare l'utente", + "network_check_smtp_ko": "La posta in uscita (SMTP porta 25) sembra bloccata dalla tua rete", + "network_check_smtp_ok": "La posta in uscita (SMTP porta 25) non è bloccata", + "no_restore_script": "Nessuno script di ripristino trovato per l'applicazone '{app:s}'", + "package_unexpected_error": "Un'errore inaspettata si è verificata durante il trattamento del pacchetto '{pkgname}'", + "restore_hook_unavailable": "Il hook di ripristino '{hook:s}' non è disponibile sul tuo sistema", + "restore_nothings_done": "Non è stato ripristinato nulla", + "restore_running_app_script": "Esecuzione dello script di ripristino dell'applcicazione '{app:s}'...", + "restore_running_hooks": "Esecuzione dei hook di ripristino...", + "service_added": "Il servizio '{service:s}' è stato aggiunto", + "service_already_started": "Il servizio '{service:s}' è già stato avviato", + "service_already_stopped": "Il servizio '{service:s}' è già stato fermato", + "service_conf_file_backed_up": "Il file di configurazione '{conf}' è stato salvato in '{backup}'", + "service_conf_file_copy_failed": "Impossibile copiare il nuovo file di configurazione '{new}' in '{conf}'", + "service_conf_file_manually_modified": "Il file di configurazione '{conf}' è stato modificato manualmente e non verrà aggiornato", + "service_conf_file_manually_removed": "Il file di configurazione '{conf}' è stato rimosso manualmente e non verrà creato", + "service_conf_file_not_managed": "Il file di configurazione '{conf}' non è ancora amministrato e non verrà aggiornato", + "service_conf_file_remove_failed": "Impossibile rimuovere il file di configurazione '{conf}'", + "service_conf_file_removed": "Il file di configurazione '{conf}' è stato rimosso", + "service_conf_file_updated": "Il file di configurazione '{conf}' è stato aggiornato", + "service_conf_up_to_date": "La configurazione è già aggiornata per il servizio '{service}'", + "service_conf_updated": "La configurazione è stata aggiornata per il servizio '{service}'", + "service_conf_would_be_updated": "La configurazione sarebbe stata aggiornata per il servizio '{service}'", + "service_disable_failed": "Impossibile disattivare il servizio '{service:s}'", + "service_enable_failed": "Impossibile attivare il servizio '{service:s}'", + "service_enabled": "Il servizio '{service:s}' è stato attivato", + "service_no_log": "Nessuno registro da visualizzare per il servizio '{service:s}'", + "service_regenconf_dry_pending_applying": "Verificazione della configurazione in attesa che sarebbe stata applicata per il servizio '{service}'...", + "service_regenconf_failed": "Impossibile rigenerare la configurazione per il/i servizio/i: {services}", + "service_regenconf_pending_applying": "Applicazione della configurazione in attesa per il servizio '{service}'...", + "service_start_failed": "Impossibile avviare il servizio '{service:s}'", + "service_started": "Il servizio '{service:s}' è stato avviato", + "service_status_failed": "Impossibile determinare lo stato del servizio '{service:s}'", + "service_stopped": "Il servizio '{service:s}' è stato fermato", + "service_unknown": "Servizio '{service:s}' sconosciuto", + "ssowat_conf_generated": "La configurazione SSOwat è stata generata", + "ssowat_conf_updated": "La configurazione SSOwat è stata aggiornata", + "ssowat_persistent_conf_read_error": "Un'errore si è verificata durante la lettura della configurazione persistente SSOwat: {error:s}. Modifica il file persistente /etc/ssowat/conf.json per correggere la sintassi JSON", + "ssowat_persistent_conf_write_error": "Un'errore si è verificata durante la registrazione della configurazione persistente SSOwat: {error:s}. Modifica il file persistente /etc/ssowat/conf.json per correggere la sintassi JSON", + "system_upgraded": "Il sistema è stato aggiornato", + "unbackup_app": "L'applicazione '{app:s}' non verrà salvata", + "unexpected_error": "Un'errore inaspettata si è verificata", + "unit_unknown": "Unità '{unit:s}' sconosciuta", + "unlimit": "Nessuna quota", + "update_cache_failed": "Impossibile aggiornare la cache APT", + "updating_apt_cache": "Aggiornamento della lista dei pacchetti disponibili...", + "upgrade_complete": "Aggiornamento completo", + "upnp_dev_not_found": "Nessuno supporto UPnP trovato", + "upnp_disabled": "UPnP è stato disattivato", + "upnp_enabled": "UPnP è stato attivato", + "upnp_port_open_failed": "Impossibile aprire le porte UPnP", + "user_created": "L'utente è stato creato", + "user_creation_failed": "Impossibile creare l'utente", + "user_deletion_failed": "Impossibile cancellare l'utente", + "user_home_creation_failed": "Impossibile creare la home directory del utente", + "user_info_failed": "Impossibile riportare le informazioni del utente", + "user_unknown": "Utente sconosciuto: {user:s}", + "user_updated": "L'utente è stato aggiornato", + "yunohost_already_installed": "YunoHost è già installato", + "yunohost_ca_creation_failed": "Impossibile creare una certificate authority", + "yunohost_configured": "YunoHost è stato configurato", + "yunohost_installing": "Installazione di YunoHost...", + "yunohost_not_installed": "YunoHost non è o non corretamente installato. Esegui 'yunohost tools postinstall'", + "domain_cert_gen_failed": "Impossibile generare il certificato", + "certmanager_attempt_to_replace_valid_cert": "Stai provando a sovrascrivere un certificato buono e valido per il dominio {domain:s}! (Usa --force per ignorare)", + "certmanager_domain_unknown": "Dominio {domain:s} sconosciuto", + "certmanager_domain_cert_not_selfsigned": "Il ceritifcato per il dominio {domain:s} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa --force)", + "certmanager_certificate_fetching_or_enabling_failed": "L'attivazione del nuovo certificato per {domain:s} sembra fallita in qualche modo...", + "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain:s} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!", + "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è a scadere! Usa --force per ignorare" } From 82de25f6fc0b607beeb79853e78c58fe8f8aa751 Mon Sep 17 00:00:00 2001 From: bricabraque Date: Sat, 4 Mar 2017 16:33:04 +0100 Subject: [PATCH 0323/1066] [i18n] Translated using Weblate (Italian) Currently translated at 92.6% (252 of 272 strings) --- locales/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index ebf4fd5db..be94afa09 100644 --- a/locales/it.json +++ b/locales/it.json @@ -249,5 +249,6 @@ "certmanager_domain_cert_not_selfsigned": "Il ceritifcato per il dominio {domain:s} non è auto-firmato. Sei sicuro di volere sostituirlo? (Usa --force)", "certmanager_certificate_fetching_or_enabling_failed": "L'attivazione del nuovo certificato per {domain:s} sembra fallita in qualche modo...", "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain:s} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!", - "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è a scadere! Usa --force per ignorare" + "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è a scadere! Usa --force per ignorare", + "certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx" } From 067137ea8277efb0a27e5c2a9aa43774b6b0950a Mon Sep 17 00:00:00 2001 From: Jean Date: Fri, 31 Mar 2017 12:05:30 +0200 Subject: [PATCH 0324/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 100.0% (273 of 273 strings) --- locales/es.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/es.json b/locales/es.json index b30f8a4d3..d0ed51914 100644 --- a/locales/es.json +++ b/locales/es.json @@ -151,7 +151,7 @@ "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", "path_removal_failed": "No se pudo eliminar la ruta {:s}", - "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos y los guiones -_", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos, los guiones -_ y el punto.", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida (por ejemplo, alguien@dominio.org)", "pattern_firstname": "Debe ser un nombre válido", @@ -273,5 +273,6 @@ "certmanager_http_check_timeout": "Plazo expirado, el servidor no ha podido contactarse a si mismo a través de HTTP usando su dirección IP pública (dominio {domain:s} con ip {ip:s}). Puede ser debido a hairpinning o a una mala configuración del cortafuego/router al que está conectado su servidor.", "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde.", "appslist_retrieve_bad_format": "El archivo recuperado no es una lista de aplicaciones válida", - "domain_hostname_failed": "Error al establecer nuevo nombre de host" + "domain_hostname_failed": "Error al establecer nuevo nombre de host", + "yunohost_ca_creation_success": "Se ha creado la autoridad de certificación local." } From 482410233a11ecccb9c3165a726844ff701c61e9 Mon Sep 17 00:00:00 2001 From: Jean Date: Tue, 28 Mar 2017 11:51:26 +0200 Subject: [PATCH 0325/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (273 of 273 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index c6f93a8c8..e11b0c646 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -281,5 +281,6 @@ "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", "appslist_retrieve_bad_format": "Le fichier récupéré n'est pas une liste d'applications valide", - "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte" + "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", + "yunohost_ca_creation_success": "L’autorité locale de certification a été créée." } From b1119c8289d9890e4eeddf8a9331bae75aabf75f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Mon, 3 Apr 2017 22:19:02 +0200 Subject: [PATCH 0326/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (273 of 273 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index e11b0c646..28c84e729 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -153,7 +153,7 @@ "packages_upgrade_critical_later": "Les paquets critiques ({packages:s}) seront mis à jour ultérieurement", "packages_upgrade_failed": "Impossible de mettre à jour tous les paquets", "path_removal_failed": "Impossible de supprimer le chemin {:s}", - "pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement", + "pattern_backup_archive_name": "Doit être un nom de fichier valide composé uniquement de caractères alphanumériques et de -_.", "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)", "pattern_email": "Doit être une adresse courriel valide (ex. : pseudo@domain.org)", "pattern_firstname": "Doit être un prénom valide", From 8687f9ef46503ce8c33a621130e103a023e5bbc7 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Fri, 7 Apr 2017 13:26:00 +0200 Subject: [PATCH 0327/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (278 of 278 strings) --- locales/fr.json | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 28c84e729..6144ce553 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -30,10 +30,10 @@ "app_unsupported_remote_type": "Le type distant utilisé par l'application n'est pas supporté", "app_upgrade_failed": "Impossible de mettre à jour {app:s}", "app_upgraded": "{app:s} a été mis à jour", - "appslist_fetched": "La liste d'applications a été récupérée", - "appslist_removed": "La liste d'applications a été supprimée", - "appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante: {error}", - "appslist_unknown": "Liste d'applications inconnue", + "appslist_fetched": "La liste d’applications {appslist:s} a été récupérée", + "appslist_removed": "La liste d’applications {appslist:s} a été supprimée", + "appslist_retrieve_error": "Impossible de récupérer la liste d’applications distante {appslist:s} : {error:s}", + "appslist_unknown": "La liste d’applications {appslist:s} est inconnue.", "ask_current_admin_password": "Mot de passe d'administration actuel", "ask_email": "Adresse courriel", "ask_firstname": "Prénom", @@ -280,7 +280,12 @@ "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", - "appslist_retrieve_bad_format": "Le fichier récupéré n'est pas une liste d'applications valide", + "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’application {appslist:s} n’est pas une liste d’applications valide", "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", - "yunohost_ca_creation_success": "L’autorité locale de certification a été créée." + "yunohost_ca_creation_success": "L’autorité locale de certification a été créée.", + "appslist_name_already_tracked": "Une liste d’applications ayant le nom {name:s} est déjà enregistrée.", + "appslist_url_already_tracked": "Une liste d’applications ayant l’URL {url:s} est déjà enregistrée.", + "appslist_migrating": "Migration de la liste d’applications {appslist:s}…", + "appslist_could_not_migrate": "Impossible de migrer la liste d’applications {appslist:s} ! L’URL ne peut pas être analysée… L’ancienne tâche planifiée a été gardée dans {bkp_file:s}.", + "appslist_corrupted_json": "Impossible de charger la liste d’applications. Il semble que {filename:s} est corrompu." } From 793baf9484055df175371188552bddc329f010db Mon Sep 17 00:00:00 2001 From: Philip Gatzka Date: Sat, 8 Apr 2017 20:05:11 +0200 Subject: [PATCH 0328/1066] [i18n] Translated using Weblate (German) Currently translated at 95.6% (266 of 278 strings) --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 462bc9be9..b2f7cc631 100644 --- a/locales/de.json +++ b/locales/de.json @@ -40,7 +40,7 @@ "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits", "backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name:s}' gefunden", "backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen", - "backup_cleaning_failed": "Verzeichnis der temporären Sicherungsdaten konnte nicht geleert werden", + "backup_cleaning_failed": "Temporäres Sicherungsverzeichnis konnte nicht geleert werden", "backup_created": "Datensicherung komplett", "backup_creating_archive": "Datensicherung wird erstellt...", "backup_delete_error": "Pfad '{path:s}' konnte nicht gelöscht werden", From 78c88dee3207c70026092a06934cb7281ffdba8b Mon Sep 17 00:00:00 2001 From: Jeroen Keerl Date: Sat, 8 Apr 2017 20:00:16 +0200 Subject: [PATCH 0329/1066] [i18n] Translated using Weblate (Dutch) Currently translated at 48.2% (134 of 278 strings) --- locales/nl.json | 79 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/locales/nl.json b/locales/nl.json index c2bfed31e..166df89ff 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,9 +1,9 @@ { "action_invalid": "Ongeldige actie '{action:s}'", - "admin_password": "Administration password", - "admin_password_changed": "Het admin-wachtwoord is gewijzigd", + "admin_password": "Administrator wachtwoord", + "admin_password_changed": "Het administratie wachtwoord is gewijzigd", "app_already_installed": "{app:s} is al geïnstalleerd", - "app_argument_invalid": "'{name:s}' bevat geldige waarde: {error:s}", + "app_argument_invalid": "'{name:s}' bevat ongeldige waarde: {error:s}", "app_argument_required": "Het '{name:s}' moet ingevuld worden", "app_extraction_failed": "Kan installatiebestanden niet uitpakken", "app_id_invalid": "Ongeldige app-id", @@ -12,24 +12,24 @@ "app_location_install_failed": "Kan app niet installeren op deze locatie", "app_manifest_invalid": "Ongeldig app-manifest", "app_no_upgrade": "Geen apps op te upgraden", - "app_not_installed": "{app:s} is niet geinstalleerd", + "app_not_installed": "{app:s} is niet geïnstalleerd", "app_recent_version_required": "{:s} vereist een nieuwere versie van moulinette", "app_removed": "{app:s} succesvol verwijderd", "app_sources_fetch_failed": "Kan bronbestanden niet ophalen", "app_unknown": "Onbekende app", - "app_upgrade_failed": "Kan niet alle apps updaten", - "app_upgraded": "{app:s} succesvol geüpgrade", - "appslist_fetched": "App-lijst succesvol aangemaakt.", - "appslist_removed": "App-lijst succesvol verwijderd", - "appslist_unknown": "Onbekende app-lijst", + "app_upgrade_failed": "Kan app {app:s} niet updaten", + "app_upgraded": "{app:s} succesvol geüpgraded", + "appslist_fetched": "App-lijst {appslist:s} succesvol opgehaald", + "appslist_removed": "App-lijst {appslist:s} succesvol verwijderd", + "appslist_unknown": "App-lijst {appslist:s} is onbekend.", "ask_current_admin_password": "Huidig administratorwachtwoord", "ask_email": "Email-adres", "ask_firstname": "Voornaam", "ask_lastname": "Achternaam", "ask_new_admin_password": "Nieuw administratorwachtwoord", "ask_password": "Wachtwoord", - "backup_archive_name_exists": "Backuparchief bestaat al", - "backup_cleaning_failed": "Kan tijdelijke backup directory niet leeg maken", + "backup_archive_name_exists": "Een backuparchief met dezelfde naam bestaat al", + "backup_cleaning_failed": "Kan tijdelijke backup map niet leeg maken", "backup_creating_archive": "Backup wordt gestart...", "backup_invalid_archive": "Ongeldig backup archief", "backup_output_directory_not_empty": "Doelmap is niet leeg", @@ -42,15 +42,15 @@ "domain_creation_failed": "Kan domein niet aanmaken", "domain_deleted": "Domein succesvol verwijderd", "domain_deletion_failed": "Kan domein niet verwijderen", - "domain_dyndns_already_subscribed": "Dit domein is al geregistreed bij DynDNS", + "domain_dyndns_already_subscribed": "U heeft reeds een domein bij DynDNS geregistreerd", "domain_dyndns_invalid": "Het domein is ongeldig voor DynDNS", "domain_dyndns_root_unknown": "Onbekend DynDNS root domein", "domain_exists": "Domein bestaat al", - "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijderd.", + "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijdert", "domain_unknown": "Onbekend domein", "domain_zone_exists": "DNS zone bestand bestaat al", "domain_zone_not_found": "DNS zone bestand niet gevonden voor domein: {:s}", - "done": "Voltooid.", + "done": "Voltooid", "downloading": "Downloaden...", "dyndns_cron_remove_failed": "De cron-job voor DynDNS kon niet worden verwijderd", "dyndns_ip_update_failed": "Kan het IP adres niet updaten bij DynDNS", @@ -61,15 +61,15 @@ "extracting": "Uitpakken...", "installation_complete": "Installatie voltooid", "installation_failed": "Installatie gefaald", - "ldap_initialized": "LDAP staat klaar voor gebruik", - "license_undefined": "undefined", - "mail_alias_remove_failed": "Kan mail alias niet verwijderen '{mail:s}'", + "ldap_initialized": "LDAP is klaar voor gebruik", + "license_undefined": "Niet gedefinieerd", + "mail_alias_remove_failed": "Kan mail-alias '{mail:s}' niet verwijderen", "monitor_stats_no_update": "Er zijn geen recente monitoringstatistieken bij te werken", "mysql_db_creation_failed": "Aanmaken MySQL database gefaald", "mysql_db_init_failed": "Initialiseren MySQL database gefaald", - "mysql_db_initialized": "MySQL database succesvol geïnitialiseerd", + "mysql_db_initialized": "MySQL database is succesvol geïnitialiseerd", "network_check_smtp_ko": "Uitgaande mail (SMPT port 25) wordt blijkbaar geblokkeerd door uw het netwerk", - "no_appslist_found": "Geen app-lijsten gevonden", + "no_appslist_found": "Geen app-lijst gevonden", "no_internet_connection": "Server is niet verbonden met het internet", "no_ipv6_connectivity": "IPv6-stack is onbeschikbaar", "path_removal_failed": "Kan pad niet verwijderen {:s}", @@ -82,7 +82,7 @@ "port_available": "Poort {port:d} is beschikbaar", "port_unavailable": "Poort {port:d} is niet beschikbaar", "restore_app_failed": "De app '{app:s}' kon niet worden terug gezet", - "restore_hook_unavailable": "De restauration hook '{hook:s}' is niet beschikbaar op dit systeem", + "restore_hook_unavailable": "De herstel-hook '{hook:s}' is niet beschikbaar op dit systeem", "service_add_failed": "Kan service '{service:s}' niet toevoegen", "service_already_started": "Service '{service:s}' draait al", "service_cmd_exec_failed": "Kan '{command:s}' niet uitvoeren", @@ -98,12 +98,45 @@ "upgrade_complete": "Upgrade voltooid", "upgrading_packages": "Pakketten worden geüpdate...", "upnp_dev_not_found": "Geen UPnP apparaten gevonden", - "upnp_disabled": "UPnP successvol uitgeschakeld", + "upnp_disabled": "UPnP succesvol uitgeschakeld", "upnp_enabled": "UPnP succesvol ingeschakeld", "upnp_port_open_failed": "Kan UPnP poorten niet openen", "user_deleted": "Gebruiker werd verwijderd", "user_home_creation_failed": "Kan de map voor deze gebruiker niet aanmaken", - "user_unknown": "Gebruikersnaam is onbekend", + "user_unknown": "Gebruikersnaam {user:s} is onbekend", "user_update_failed": "Kan gebruiker niet bijwerken", - "yunohost_configured": "YunoHost configuratie is OK" + "yunohost_configured": "YunoHost configuratie is OK", + "admin_password_change_failed": "Wachtwoord kan niet veranderd worden", + "app_argument_choice_invalid": "Ongeldige keuze voor argument '{name:s}'. Het moet een van de volgende keuzes zijn {choices:s}", + "app_incompatible": "Deze applicatie is incompatibel met uw YunoHost versie", + "app_not_correctly_installed": "{app:s} schijnt niet juist geïnstalleerd te zijn", + "app_not_properly_removed": "{app:s} werd niet volledig verwijderd", + "app_package_need_update": "Het is noodzakelijk om het app pakket te updaten, in navolging van veranderingen aan YunoHost", + "app_requirements_checking": "Controleer noodzakelijke pakketten...", + "app_requirements_failed": "Er wordt niet aan de aanvorderingen voldaan: {error}", + "app_requirements_unmeet": "Er wordt niet aan de aanvorderingen voldaan, het pakket {pkgname} ({version}) moet {spec} zijn", + "app_unsupported_remote_type": "Niet ondersteund besturings type voor de app", + "appslist_retrieve_error": "Niet mogelijk om de externe applicatie lijst op te halen {appslist:s}: {error:s}", + "appslist_retrieve_bad_format": "Opgehaald bestand voor applicatie lijst {appslist:s} is geen geldige applicatie lijst", + "appslist_name_already_tracked": "Er is reeds een geregistreerde applicatie lijst met de naam {name:s}.", + "appslist_url_already_tracked": "Er is reeds een geregistreerde applicatie lijst met de url {url:s}.", + "appslist_migrating": "Migreer applicatielijst {appslist:s} ...", + "appslist_could_not_migrate": "Kon applicatielijst {appslist:s} niet migreren! Niet in staat om de url te verwerken... De oude cron job is opgeslagen onder {bkp_file:s}.", + "appslist_corrupted_json": "Kon de applicatielijst niet laden. Het schijnt, dat {filename:s} beschadigd is.", + "ask_list_to_remove": "Te verwijderen lijst", + "ask_main_domain": "Hoofd-domein", + "backup_action_required": "U moet iets om op te slaan uitkiezen", + "backup_app_failed": "Kon geen backup voor app '{app:s}' aanmaken", + "backup_archive_app_not_found": "App '{app:s}' kon niet in het backup archief gevonden worden", + "backup_archive_broken_link": "Het backup archief kon niet geopend worden (Ongeldig verwijs naar {path:s})", + "backup_archive_hook_not_exec": "Hook '{hook:s}' kon voor deze backup niet uitgevoerd worden", + "backup_archive_name_unknown": "Onbekend lokaal backup archief namens '{name:s}' gevonden", + "backup_archive_open_failed": "Kan het backup archief niet openen", + "backup_created": "Backup aangemaakt", + "backup_creation_failed": "Aanmaken van backup mislukt", + "backup_delete_error": "Kon pad '{path:s}' niet verwijderen", + "backup_deleted": "Backup werd verwijderd", + "backup_extracting_archive": "Backup archief uitpakken...", + "backup_hook_unknown": "backup hook '{hook:s}' onbekend", + "backup_nothings_done": "Niets om op te slaan" } From b6b66f231f92bc44ecc65b26e6f5ffc24f5455db Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Fri, 7 Apr 2017 13:38:35 +0200 Subject: [PATCH 0330/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (278 of 278 strings) --- locales/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 6144ce553..4d7b3e323 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -111,7 +111,7 @@ "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", - "hook_list_by_invalid": "La variable de tri de la liste des connexions est invalide", + "hook_list_by_invalid": "La propriété de tri des actions est invalide", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", "installation_failed": "Échec de l'installation", @@ -140,7 +140,7 @@ "network_check_smtp_ko": "Le trafic courriel sortant (port 25 SMTP) semble bloqué par votre réseau", "network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n'est pas bloqué", "new_domain_required": "Vous devez spécifier le nouveau domaine principal", - "no_appslist_found": "Aucune liste d'applications trouvée", + "no_appslist_found": "Aucune liste d’applications n’a été trouvée", "no_internet_connection": "Le serveur n'est pas connecté à Internet", "no_ipv6_connectivity": "La connectivité IPv6 n'est pas disponible", "no_restore_script": "Le script de sauvegarde n'a pas été trouvé pour l'application « {app:s} »", @@ -280,7 +280,7 @@ "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", - "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’application {appslist:s} n’est pas une liste d’applications valide", + "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas une liste d’applications valide", "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", "yunohost_ca_creation_success": "L’autorité locale de certification a été créée.", "appslist_name_already_tracked": "Une liste d’applications ayant le nom {name:s} est déjà enregistrée.", From 0088483835f36ed90b891d01539bbdbfa9daf356 Mon Sep 17 00:00:00 2001 From: Jeroen Keerl Date: Sat, 8 Apr 2017 20:15:55 +0200 Subject: [PATCH 0331/1066] [i18n] Translated using Weblate (German) Currently translated at 98.9% (275 of 278 strings) --- locales/de.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/locales/de.json b/locales/de.json index b2f7cc631..c51beebe1 100644 --- a/locales/de.json +++ b/locales/de.json @@ -21,8 +21,8 @@ "app_unknown": "Unbekannte App", "app_upgrade_failed": "{app:s} konnte nicht aktualisiert werden", "app_upgraded": "{app:s} wurde erfolgreich aktualisiert", - "appslist_fetched": "Appliste wurde erfolgreich heruntergelanden", - "appslist_removed": "Appliste wurde erfolgreich entfernt", + "appslist_fetched": "Appliste {appslist:s} wurde erfolgreich heruntergelanden", + "appslist_removed": "Appliste {appslist:s} wurde erfolgreich entfernt", "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden: {error}", "appslist_unknown": "Unbekannte Appliste", "ask_current_admin_password": "Derzeitiges Administrator-Kennwort", @@ -131,7 +131,7 @@ "packages_upgrade_critical_later": "Ein wichtiges Paket ({packages:s}) wird später aktualisiert", "packages_upgrade_failed": "Es konnten nicht alle Pakete aktualisiert werden", "path_removal_failed": "Pfad {:s} konnte nicht entfernt werden", - "pattern_backup_archive_name": "Ein gültiger Dateiname kann nur aus alphanumerischen und -_. bestehen", + "pattern_backup_archive_name": "Ein gültiger Dateiname kann nur aus maximal 30 alphanumerischen sowie -_. Zeichen bestehen", "pattern_domain": "Muss ein gültiger Domainname sein (z.B. meine-domain.org)", "pattern_email": "Muss eine gültige E-Mail Adresse sein (z.B. someone@domain.org)", "pattern_firstname": "Muss ein gültiger Vorname sein", @@ -278,5 +278,11 @@ "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine misskonfigurierte Firewall/Router deines Servers dafür verantwortlich.", "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut.", "appslist_retrieve_bad_format": "Die empfangene Datei ist keine gültige Appliste", - "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen" + "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen", + "appslist_name_already_tracked": "Es gibt bereits eine registrierte App-Liste mit Namen {name:s}.", + "appslist_url_already_tracked": "Es gibt bereits eine registrierte Anwendungsliste mit dem URL {url:s}.", + "appslist_migrating": "Migriere Anwendungsliste {appslist:s} ...", + "appslist_could_not_migrate": "Konnte Anwendungsliste {appslist:s} nicht migrieren. Konnte URL nicht verarbeiten... Der alte Cron-Job wurde unter {bkp_file:s} beibehalten.", + "appslist_corrupted_json": "Konnte die Anwendungslisten. Es scheint, dass {filename:s} beschädigt ist.", + "yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt." } From 6e3b67a843ee92b3397e2fe9a8b74cde524ddfcf Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Mon, 10 Apr 2017 17:44:52 +0200 Subject: [PATCH 0332/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (278 of 278 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 4d7b3e323..4fdd2bbb3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -280,7 +280,7 @@ "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", - "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas une liste d’applications valide", + "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas valide", "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", "yunohost_ca_creation_success": "L’autorité locale de certification a été créée.", "appslist_name_already_tracked": "Une liste d’applications ayant le nom {name:s} est déjà enregistrée.", From 883fe46d8924b75aa924d425f5bd77e16d758e56 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 18 Apr 2017 09:24:11 +0200 Subject: [PATCH 0333/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (287 of 287 strings) --- locales/fr.json | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 4fdd2bbb3..83230955e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -282,10 +282,19 @@ "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas valide", "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", - "yunohost_ca_creation_success": "L’autorité locale de certification a été créée.", - "appslist_name_already_tracked": "Une liste d’applications ayant le nom {name:s} est déjà enregistrée.", - "appslist_url_already_tracked": "Une liste d’applications ayant l’URL {url:s} est déjà enregistrée.", + "yunohost_ca_creation_success": "L’autorité de certification locale a été créée.", + "appslist_name_already_tracked": "Il y a déjà une liste d’applications enregistrée avec le nom {name:s}.", + "appslist_url_already_tracked": "Il y a déjà une liste d’applications enregistrée avec l’URL {url:s}.", "appslist_migrating": "Migration de la liste d’applications {appslist:s}…", - "appslist_could_not_migrate": "Impossible de migrer la liste d’applications {appslist:s} ! L’URL ne peut pas être analysée… L’ancienne tâche planifiée a été gardée dans {bkp_file:s}.", - "appslist_corrupted_json": "Impossible de charger la liste d’applications. Il semble que {filename:s} est corrompu." + "appslist_could_not_migrate": "Impossible de migrer la liste {appslist:s} ! Impossible d’exploiter l’URL… L’ancienne tâche cron a été conservée dans {bkp_file:s}.", + "appslist_corrupted_json": "Impossible de charger la liste d’applications. Il semble que {filename:s} soit corrompu.", + "app_already_installed_cant_change_url": "Cette application est déjà installée. L’URL ne peut pas être changé simplement par cette fonction. Regardez avec « app changeurl » si c’est disponible.", + "app_change_no_change_url_script": "L’application {app_name:s} ne prend pas encore en charge le changement d’URL, vous pourriez avoir besoin de la mettre à jour.", + "app_change_url_failed_nginx_reload": "Le redémarrage de nginx a échoué. Voici la sortie de « nginx -t » :\n{nginx_errors:s}", + "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin sont identiques pour {domain:s}{path:s}, aucune action.", + "app_change_url_no_script": "L’application {app_name:s} ne prend pas encore en charge le changement d’URL. Vous devriez peut-être la mettre à jour.", + "app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}", + "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante", + "app_already_up_to_date": "{app:s} est déjà à jour", + "invalid_url_format": "Format d’URL non valide" } From dc720cc2ce01ce5a2e5956febcf14820f6502964 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 24 Apr 2017 09:21:44 -0400 Subject: [PATCH 0334/1066] Update changelog for 2.6.2 release --- debian/changelog | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0cfe85d8a..81b816e39 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,37 @@ +yunohost (2.6.2) testing; urgency=low + + New Features + ------------ + + * [enh] Allow applications to ship a script to change its url (#185) + * New helper ynh_replace_string (#280) + * New helper ynh_local_curl (#288) + + Fixes + ----- + + * Fix for missing YunoHost tiles (#276) + * [fix] Properly define app upgradability / Fix app part of tools update (#255) + * [fix] Properly manage resolv.conf, dns resolvers and dnsmasq (#290) + * [fix] Add random delay to app fetchlist cron job (#297) + + Improvements + ------------- + + * [fix] Avoid to remove a apt package accidentally (#292) + * [enh] Refactor applist management (#160) + * [enh] Add libnss-mdns as Debian dependency. (#279) + * [enh] ip6.yunohost is now served through HTTPS. + * [enh] Adding new port availability checker (#266) + * [fix] Split checkurl into two functions : availability + booking (#267) + * [enh] Cleaner postinstall logs during CA creation (#250) + * Allow underscore in backup name + * Rewrite text for "appslist_retrieve_bad_format" + * Rewrite text for "certmanager_http_check_timeout" + * Updated Spanish, German, Italian, French, German and Dutch translations + + -- Alexandre Aubin Mon, 24 Apr 2017 09:07:51 -0400 + yunohost (2.6.1) testing; urgency=low [ Maniack Crudelis ] From 12b7c438a9449fa2ae59478ba485a5d50963d404 Mon Sep 17 00:00:00 2001 From: opi Date: Tue, 25 Apr 2017 15:37:14 +0200 Subject: [PATCH 0335/1066] [love] Add missing contributors & translators. --- CONTRIBUTORS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1c5b6fd33..0a9ac7527 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -41,6 +41,7 @@ YunoHost core Contributors - lmangani - Julien Vaubourg - thardev +- zimo2001 YunoHost core Translators @@ -52,23 +53,32 @@ If you want to help translation, please visit https://translate.yunohost.org/pro ### Dutch - DUBWiSE +- Jeroen Keerl - marut ### English - Bugsbane +- rokaz ### French - aoz roon - Genma - Jean-Baptiste Holcroft +- Jean P. - Jérôme Lebleu +- Lapineige +- paddy + ### German - david.bartke +- Fabian Gruber - Felix Bartels +- Jeroen Keerl +- martin kistner - Philip Gatzka ### Hindi @@ -77,11 +87,13 @@ If you want to help translation, please visit https://translate.yunohost.org/pro ### Italian +- bricabrac - Thomas Bille ### Portuguese - Deleted User +- Trollken ### Spanish From 558323f6f22b483d5c478062e78d3956e1b2081c Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 26 Apr 2017 19:51:51 +0200 Subject: [PATCH 0336/1066] [enh] Fix curl helper without POST data - Allow to use this helper without any POST data - Keep curl more quiet --- data/helpers.d/utils | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 6bc1e39d1..a7c8bbff9 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -46,9 +46,12 @@ ynh_local_curl () { do POST_data="${POST_data}${arg}&" done - # (Remove the last character, which is an unecessary '&') - POST_data=${POST_data::-1} + if [ -n "$POST_data" ] + then + # Add --data arg and remove the last character, which is an unecessary '&' + POST_data="--data \"${POST_data::-1}\"" + fi # Curl the URL - curl -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 --data "$POST_data" "$full_page_url" 2>&1 + curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" } From 6cc237dccae3c2fda29e120191c1e5db9177c6ec Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 28 Apr 2017 17:37:34 +0200 Subject: [PATCH 0337/1066] Changing nginx ciphers to intermediate compatiblity (#298) * Changing nginx ciphers to intermediate compatiblity * [fix] Move commented ciphers list after, and add explanation * [fix] Move commented ciphers list after, and add explanation --- .../templates/nginx/plain/yunohost_admin.conf | 19 +++++++++++++++---- data/templates/nginx/server.tpl.conf | 19 +++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 57abac7ee..a9d26d151 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -20,12 +20,23 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; - # Ciphers with modern configuration - # Source : https://mozilla.github.io/server-side-tls/ssl-config-generator/ - ssl_protocols TLSv1.2; - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; + # Ciphers with intermediate compatibility + # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=intermediate + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + + # Ciphers with modern compatibility + # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=modern + # Uncomment the following to use modern ciphers, but remove compatibility with some old clients (android < 5.0, Internet Explorer < 10, ...) + #ssl_protocols TLSv1.2; + #ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; + + # Uncomment the following directive after DH generation + # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 + #ssl_dhparam /etc/ssl/private/dh2048.pem; + add_header Strict-Transport-Security "max-age=31536000;"; location / { diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 8466eeef3..685ae01b8 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -25,12 +25,23 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; - # Ciphers with modern configuration - # Source : https://mozilla.github.io/server-side-tls/ssl-config-generator/ - ssl_protocols TLSv1.2; - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on; + # Ciphers with intermediate compatibility + # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=intermediate + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; + + # Ciphers with modern compatibility + # https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.6.2&openssl=1.0.1t&hsts=yes&profile=modern + # Uncomment the following to use modern ciphers, but remove compatibility with some old clients (android < 5.0, Internet Explorer < 10, ...) + #ssl_protocols TLSv1.2; + #ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; + + # Uncomment the following directive after DH generation + # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 + #ssl_dhparam /etc/ssl/private/dh2048.pem; + add_header Strict-Transport-Security "max-age=31536000;"; access_by_lua_file /usr/share/ssowat/access.lua; From 47ce6d9e33eea2d1ce02b40f31b697384f9b036a Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 30 Apr 2017 22:37:52 +0200 Subject: [PATCH 0338/1066] New helper ynh_secure_remove (#281) * New helper ynh_secure_remove A secure way to remove a file or directory. Prevent to knew issues. Tested with this paths: - / -> Not removed - /var -> Not removed - /var/www -> Not removed - /var/www/file -> Removed - /opt -> Not removed - /opt/file -> Removed - /home/yunohost.app -> Not removed - /home -> Not removed - /home/ -> Not removed - // -> Not removed - /etc/cron.d/ -> Not removed - /etc -> Not removed - /etc/ -> Not removed - /etc/X11 -> Removed - /etc/X11/$var -> Removed (if $var is not empty) * JimboJoe's typo fix --- data/helpers.d/filesystem | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index bce58b5cf..0d13c4825 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -80,3 +80,31 @@ properly with chmod/chown." >&2 chmod 755 $TMP_DIR echo $TMP_DIR } + +# Remove a file or a directory securely +# +# usage: ynh_secure_remove path_to_remove +# | arg: path_to_remove - File or directory to remove +ynh_secure_remove () { + path_to_remove=$1 + forbidden_path=" \ + /var/www \ + /home/yunohost.app" + + if [[ "$forbidden_path" =~ "$path_to_remove" \ + # Match all paths or subpaths in $forbidden_path + || "$path_to_remove" =~ ^/[[:alnum:]]+$ \ + # Match all first level paths from / (Like /var, /root, etc...) + || "${path_to_remove:${#path_to_remove}-1}" = "/" ]] + # Match if the path finishes by /. Because it seems there is an empty variable + then + echo "Avoid deleting $path_to_remove." >&2 + else + if [ -e "$path_to_remove" ] + then + sudo rm -R "$path_to_remove" + else + echo "$path_to_remove wasn't deleted because it doesn't exist." >&2 + fi + fi +} From afe28470c0c7776afe4824c9ec81abd6eab52d40 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Wed, 3 May 2017 19:11:01 +0200 Subject: [PATCH 0339/1066] [enh] Add execution dir in env on hook exec (#293) * [enh] Add execution dir in env on hook exec * [enh] YNH_EXECUTION_DIR become YNH_CWD * [fix] Hook exec with no env fail * [enh] Remove dead condition --- src/yunohost/hook.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index b675caa2e..7d53a2d5f 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -326,6 +326,11 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: cmd_script = path + # Add Execution dir to environment var + if env is None: + env = {} + env['YNH_CWD'] = chdir + # Construct command to execute if user == "root": command = ['sh', '-c'] @@ -337,11 +342,11 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: # use xtrace on fd 7 which is redirected to stdout cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' - if env: - # prepend environment variables - cmd = '{0} {1}'.format( - ' '.join(['{0}={1}'.format(k, shell_quote(v)) \ - for k, v in env.items()]), cmd) + + # prepend environment variables + cmd = '{0} {1}'.format( + ' '.join(['{0}={1}'.format(k, shell_quote(v)) \ + for k, v in env.items()]), cmd) command.append(cmd.format(script=cmd_script, args=cmd_args)) if logger.isEnabledFor(log.DEBUG): From 0e44a422742f089b54d42a10f1ca0429926bc2d5 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 3 May 2017 19:19:41 +0200 Subject: [PATCH 0340/1066] New helper ynh_setup_source (#282) * New helper ynh_setup_source THE trollest helper :D An helper to handle download, checksum, copy, patches, extra files and even a way to install without internet connection. Really usefull to simplify this part of the packaging. * [fix] Source id and pkg _dir missing, strip-components deactivation * [fix] Replace tab by 4 spaces * [fix] False default value in ynh_setup_source helpers * [fix] Empty var failed on set -u into ynh_setup_source * [fix] Multiple patch on setup sources helper * [fix] Doc, patch and extra_file in ynh_setup helpers * [enh] Allow to use ynh_setup_source in another dir * [fix] Doc ynh_get_plain_key * [fix] Missing default value in ynh_setup_source * Missing closing } after merge with unstable ? --- data/helpers.d/utils | 112 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index a7c8bbff9..c327fab9e 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -25,6 +25,118 @@ ynh_get_plain_key() { done } +# Download, check integrity, uncompress and patch the source from app.src +# +# The file conf/app.src need to contains: +# +# SOURCE_URL=Address to download the app archive +# SOURCE_SUM=Control sum +# # (Optional) Programm to check the integrity (sha256sum, md5sum$YNH_EXECUTION_DIR/...) +# # default: sha256 +# SOURCE_SUM_PRG=sha256 +# # (Optional) Archive format +# # default: tar.gz +# SOURCE_FORMAT=tar.gz +# # (Optional) Put false if source are directly in the archive root +# # default: true +# SOURCE_IN_SUBDIR=false +# # (Optionnal) Name of the local archive (offline setup support) +# # default: ${src_id}.${src_format} +# SOURCE_FILENAME=example.tar.gz +# +# Details: +# This helper download sources from SOURCE_URL if there is no local source +# archive in /opt/yunohost-apps-src/APP_ID/SOURCE_FILENAME +# +# Next, it check the integrity with "SOURCE_SUM_PRG -c --status" command. +# +# If it's ok, the source archive will be uncompress in $dest_dir. If the +# SOURCE_IN_SUBDIR is true, the first level directory of the archive will be +# removed. +# +# Finally, patches named sources/patches/${src_id}-*.patch and extra files in +# sources/extra_files/$src_id will be applyed to dest_dir +# +# +# usage: ynh_setup_source dest_dir [source_id] +# | arg: dest_dir - Directory where to setup sources +# | arg: source_id - Name of the app, if the package contains more than one app +ynh_setup_source () { + local dest_dir=$1 + local src_id=${2:-app} # If the argument is not given, source_id equal "app" + + # Load value from configuration file (see above for a small doc about this file + # format) + local src_url=$(grep 'SOURCE_URL=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_sum=$(grep 'SOURCE_SUM=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_format=$(grep 'SOURCE_FORMAT=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_filename=$(grep 'SOURCE_FILENAME=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + + # Default value + src_sumprg=${src_sumprg:-sha256sum} + src_in_subdir=${src_in_subdir:-true} + src_format=${src_format:-tar.gz} + src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') + if [ "$src_filename" = "" ] ; then + src_filename="${src_id}.${src_format}" + fi + local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" + + if test -e "$local_src" + then # Use the local source file if it is present + cp $local_src $src_filename + else # If not, download the source + wget -nv -O $src_filename $src_url + fi + + # Check the control sum + echo "${src_sum} ${src_filename}" | ${src_sumprg} -c --status \ + || ynh_die "Corrupt source" + + # Extract source into the app dir + mkdir -p "$dest_dir" + if [ "$src_format" = "zip" ] + then + # Zip format + # Using of a temp directory, because unzip doesn't manage --strip-components + if $src_in_subdir ; then + local tmp_dir=$(mktemp -d) + unzip -quo $src_filename -d "$tmp_dir" + cp -a $tmp_dir/*/. "$dest_dir" + ynh_secure_remove "$tmp_dir" + else + unzip -quo $src_filename -d "$dest_dir" + fi + else + local strip="" + if $src_in_subdir ; then + strip="--strip-components 1" + fi + if [[ "$src_format" =~ ^tar.gz|tar.bz2|tar.xz$ ]] ; then + tar -xf $src_filename -C "$dest_dir" $strip + else + ynh_die "Archive format unrecognized." + fi + fi + + # Apply patches + if (( $(find $YNH_EXECUTION_DIR/../sources/patches/ -type f -name "${src_id}-*.patch" 2> /dev/null | wc -l) > "0" )); then + local old_dir=$(pwd) + (cd "$dest_dir" \ + && for p in $YNH_EXECUTION_DIR/../sources/patches/${src_id}-*.patch; do \ + patch -p1 < $p; done) \ + || ynh_die "Unable to apply patches" + cd $old_dir + fi + + # Add supplementary files + if test -e "$YNH_EXECUTION_DIR/../sources/extra_files/${src_id}"; then + cp -a $YNH_EXECUTION_DIR/../sources/extra_files/$src_id/. "$dest_dir" + fi +} + # Curl abstraction to help with POST requests to local pages (such as installation forms) # # $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?)) From 785790b930ef0d02716294475b789cc9a0e3829e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 4 May 2017 16:23:35 +0200 Subject: [PATCH 0341/1066] [enh] Add a custom --yunodebug option for pytest (#303) --- src/yunohost/tests/conftest.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index f979d1b42..946eec23c 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -3,6 +3,10 @@ import moulinette sys.path.append("..") + +def pytest_addoption(parser): + parser.addoption("--yunodebug", action="store_true", default=False) + ############################################################################### # Tweak moulinette init to have yunohost namespace # ############################################################################### @@ -39,7 +43,7 @@ moulinette.core.Moulinette18n.n = new_m18nn ############################################################################### -def _init_moulinette(): +def pytest_cmdline_main(config): """Configure logging and initialize the moulinette""" # Define loggers handlers handlers = set(['tty']) @@ -47,7 +51,10 @@ def _init_moulinette(): # Define loggers level level = 'INFO' - tty_level = 'SUCCESS' + if config.option.yunodebug: + tty_level = 'DEBUG' + else: + tty_level = 'SUCCESS' # Custom logging configuration logging = { @@ -99,5 +106,3 @@ def _init_moulinette(): # Initialize moulinette moulinette.init(logging_config=logging, _from_source=False) - -_init_moulinette() From 59aafebea7129f98b73a5218f9879a73e63b1538 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 4 May 2017 18:46:20 +0200 Subject: [PATCH 0342/1066] [microdecision] Fix typo in checkport, fixes issue 912 on redmine --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2f80d0ac2..1d61ce876 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1071,8 +1071,8 @@ def app_checkport(port): # This import cannot be moved on top of file because it create a recursive # import... - from yunohost.tools import tools_portavailable - availability = tools_portavailable(port) + from yunohost.tools import tools_port_available + availability = tools_port_available(port) if availability["available"]: logger.success(m18n.n('port_available', port=int(port))) else: From 3571747718ea47e7a2d522adc8d5b2c03c64d261 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 6 May 2017 16:53:54 +0200 Subject: [PATCH 0343/1066] Nouveau helper ynh_webpath_available et ynh_webpath_register (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Nouveau helper ynh_path_validity Simplement un wrapper de yunohost app checkurl. Peut-être une occasion de lui donner un autre nom plus parlant. Il me semble justement qu'il en était question. Conflicts: data/helpers.d/network * Renaming ynh_path_validity to ynh_webpath_available * Splitting domain and path * Use the new url-available command instead of deprecated checkurl * Adding ynh_webpath_register * [enh] Check before register - Need to be tested. - And... probably lack a echo to inform in case of error. * Fixing helper following discussion with Maniack --- data/helpers.d/network | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/network b/data/helpers.d/network index 750a49cf6..c6764c1f5 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -36,4 +36,32 @@ ynh_find_port () { port=$((port+1)) # Else, pass to next port done echo $port -} \ No newline at end of file +} + +# Check availability of a web path +# +# example: ynh_webpath_available some.domain.tld /coffee +# +# usage: ynh_webpath_available domain path +# | arg: domain - the domain/host of the url +# | arg: path - the web path to check the availability of +ynh_webpath_available () { + local domain=$1 + local path=$2 + sudo yunohost domain url-available $domain $path +} + +# Register/book a web path for an app +# +# example: ynh_webpath_register wordpress some.domain.tld /coffee +# +# usage: ynh_webpath_register app domain path +# | arg: app - the app for which the domain should be registered +# | arg: domain - the domain/host of the web path +# | arg: path - the web path to be registered +ynh_webpath_register () { + local app=$1 + local domain=$2 + local path=$3 + sudo yunohost app register-url $app $domain $path +} From c8647fc21f0e41a80a7be4bf866be0deecba6a21 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Sun, 7 May 2017 19:05:42 +0200 Subject: [PATCH 0344/1066] [fix] ynh_setup_source: use YNH_CWD instead of YNH_EXECUTION_DIR (#305) Following this discussion: https://github.com/YunoHost/yunohost/pull/282#issuecomment-299637999 --- data/helpers.d/utils | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index c327fab9e..9328936e5 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -31,13 +31,13 @@ ynh_get_plain_key() { # # SOURCE_URL=Address to download the app archive # SOURCE_SUM=Control sum -# # (Optional) Programm to check the integrity (sha256sum, md5sum$YNH_EXECUTION_DIR/...) +# # (Optional) Program to check the integrity (sha256sum, md5sum...) # # default: sha256 # SOURCE_SUM_PRG=sha256 # # (Optional) Archive format # # default: tar.gz # SOURCE_FORMAT=tar.gz -# # (Optional) Put false if source are directly in the archive root +# # (Optional) Put false if sources are directly in the archive root # # default: true # SOURCE_IN_SUBDIR=false # # (Optionnal) Name of the local archive (offline setup support) @@ -45,17 +45,17 @@ ynh_get_plain_key() { # SOURCE_FILENAME=example.tar.gz # # Details: -# This helper download sources from SOURCE_URL if there is no local source +# This helper downloads sources from SOURCE_URL if there is no local source # archive in /opt/yunohost-apps-src/APP_ID/SOURCE_FILENAME # -# Next, it check the integrity with "SOURCE_SUM_PRG -c --status" command. +# Next, it checks the integrity with "SOURCE_SUM_PRG -c --status" command. # -# If it's ok, the source archive will be uncompress in $dest_dir. If the +# If it's ok, the source archive will be uncompressed in $dest_dir. If the # SOURCE_IN_SUBDIR is true, the first level directory of the archive will be # removed. # # Finally, patches named sources/patches/${src_id}-*.patch and extra files in -# sources/extra_files/$src_id will be applyed to dest_dir +# sources/extra_files/$src_id will be applied to dest_dir # # # usage: ynh_setup_source dest_dir [source_id] @@ -63,16 +63,16 @@ ynh_get_plain_key() { # | arg: source_id - Name of the app, if the package contains more than one app ynh_setup_source () { local dest_dir=$1 - local src_id=${2:-app} # If the argument is not given, source_id equal "app" + local src_id=${2:-app} # If the argument is not given, source_id equals "app" # Load value from configuration file (see above for a small doc about this file # format) - local src_url=$(grep 'SOURCE_URL=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) - local src_sum=$(grep 'SOURCE_SUM=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) - local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) - local src_format=$(grep 'SOURCE_FORMAT=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) - local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) - local src_filename=$(grep 'SOURCE_FILENAME=' "$YNH_EXECUTION_DIR/../conf/${src_id}.src" | cut -d= -f2-) + local src_url=$(grep 'SOURCE_URL=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_sum=$(grep 'SOURCE_SUM=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_format=$(grep 'SOURCE_FORMAT=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_filename=$(grep 'SOURCE_FILENAME=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) # Default value src_sumprg=${src_sumprg:-sha256sum} @@ -122,18 +122,18 @@ ynh_setup_source () { fi # Apply patches - if (( $(find $YNH_EXECUTION_DIR/../sources/patches/ -type f -name "${src_id}-*.patch" 2> /dev/null | wc -l) > "0" )); then + if (( $(find $YNH_CWD/../sources/patches/ -type f -name "${src_id}-*.patch" 2> /dev/null | wc -l) > "0" )); then local old_dir=$(pwd) (cd "$dest_dir" \ - && for p in $YNH_EXECUTION_DIR/../sources/patches/${src_id}-*.patch; do \ + && for p in $YNH_CWD/../sources/patches/${src_id}-*.patch; do \ patch -p1 < $p; done) \ || ynh_die "Unable to apply patches" cd $old_dir fi # Add supplementary files - if test -e "$YNH_EXECUTION_DIR/../sources/extra_files/${src_id}"; then - cp -a $YNH_EXECUTION_DIR/../sources/extra_files/$src_id/. "$dest_dir" + if test -e "$YNH_CWD/../sources/extra_files/${src_id}"; then + cp -a $YNH_CWD/../sources/extra_files/$src_id/. "$dest_dir" fi } From dfee06404e31470197f18ff29e2c0d2d56944b43 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 8 May 2017 19:05:49 +0200 Subject: [PATCH 0345/1066] Nouveaux helpers ynh_mysql_generate_db et ynh_mysql_remove_db (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Nouveaux helpers ynh_mysql_generate_db et ynh_mysql_remove_db Helpers pour créer une base de donnée, son utilisateur et un mot de passe. Et son pendant pour la supprimer. * Separate corrections of names * Grammar nazism * Update according to comments * Renaming ynh_make_valid_dbid to ynh_sanitize_dbid * Fixing remaining make_valid_dbid * Implement remaining comments * Add a check that the user exist before deleting it --- data/helpers.d/mysql | 69 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index dda441dc3..f5105a4e4 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -56,6 +56,9 @@ ynh_mysql_create_db() { # Drop a database # +# If you intend to drop the database *and* the associated user, +# consider using ynh_mysql_remove_db instead. +# # usage: ynh_mysql_drop_db db # | arg: db - the database name to drop ynh_mysql_drop_db() { @@ -83,6 +86,21 @@ ynh_mysql_create_user() { "CREATE USER '${1}'@'localhost' IDENTIFIED BY '${2}';" } +# Check if a mysql user exists +# +# usage: ynh_mysql_user_exists user +# | arg: user - the user for which to check existence +function ynh_mysql_user_exists() +{ + local user=$1 + if [[ -z $(ynh_mysql_execute_as_root "SELECT User from mysql.user WHERE User = '$user';") ]] + then + return 1 + else + return 0 + fi +} + # Drop a user # # usage: ynh_mysql_drop_user user @@ -90,3 +108,54 @@ ynh_mysql_create_user() { ynh_mysql_drop_user() { ynh_mysql_execute_as_root "DROP USER '${1}'@'localhost';" } + +# Create a database, an user and its password. Then store the password in the app's config +# +# After executing this helper, the password of the created database will be available in $db_pwd +# It will also be stored as "mysqlpwd" into the app settings. +# +# usage: ynh_mysql_setup_db user name +# | arg: user - Owner of the database +# | arg: name - Name of the database +ynh_mysql_setup_db () { + local db_user="$1" + local db_name="$2" + db_pwd=$(ynh_string_random) # Generate a random password + ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database + ynh_app_setting_set $app mysqlpwd $db_pwd # Store the password in the app's config +} + +# Remove a database if it exists, and the associated user +# +# usage: ynh_mysql_remove_db user name +# | arg: user - Owner of the database +# | arg: name - Name of the database +ynh_mysql_remove_db () { + local db_user="$1" + local db_name="$2" + local mysql_root_password=$(sudo cat $MYSQL_ROOT_PWD_FILE) + if mysqlshow -u root -p$mysql_root_password | grep -q "^| $db_name"; then # Check if the database exists + echo "Removing database $db_name" >&2 + ynh_mysql_drop_db $db_name # Remove the database + else + echo "Database $db_name not found" >&2 + fi + + # Remove mysql user if it exists + if $(ynh_mysql_user_exists $db_user); then + ynh_mysql_drop_user $db_user + fi +} + +# Sanitize a string intended to be the name of a database +# (More specifically : replace - and . by _) +# +# Exemple: dbname=$(ynh_sanitize_dbid $app) +# +# usage: ynh_sanitize_dbid name +# | arg: name - name to correct/sanitize +# | ret: the corrected name +ynh_sanitize_dbid () { + dbid=${1//[-.]/_} # We should avoid having - and . in the name of databases. They are replaced by _ + echo $dbid +} From ad08acee184a899943e15e8c6e08385c68c158a7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 8 May 2017 23:33:44 +0200 Subject: [PATCH 0346/1066] Add a script to test m18n keys usage --- tests/_test_m18nkeys.py | 82 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/_test_m18nkeys.py diff --git a/tests/_test_m18nkeys.py b/tests/_test_m18nkeys.py new file mode 100644 index 000000000..ee8df0dc6 --- /dev/null +++ b/tests/_test_m18nkeys.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +import re +import glob +import json +import yaml + +############################################################################### +# Find used keys in python code # +############################################################################### + +# This regex matches « foo » in patterns like « m18n.n( "foo" » +p = re.compile(r'm18n\.n\(\s*[\"\']([a-zA-Z1-9_]+)[\"\']') + +python_files = glob.glob("/vagrant/yunohost/src/yunohost/*.py") +python_files.extend(glob.glob("/vagrant/yunohost/src/yunohost/utils/*.py")) +python_files.append("/vagrant/yunohost/bin/yunohost") + +python_keys = set() +for python_file in python_files: + with open(python_file) as f: + keys_in_file = p.findall(f.read()) + for key in keys_in_file: + python_keys.add(key) + +############################################################################### +# Find keys used in actionmap # +############################################################################### + +actionmap_keys = set() +actionmap = yaml.load(open("../data/actionsmap/yunohost.yml")) +for _, category in actionmap.items(): + if "actions" not in category.keys(): + continue + for _, action in category["actions"].items(): + if "arguments" not in action.keys(): + continue + for _, argument in action["arguments"].items(): + if "extra" not in argument.keys(): + continue + if "password" in argument["extra"]: + actionmap_keys.add(argument["extra"]["password"]) + if "ask" in argument["extra"]: + actionmap_keys.add(argument["extra"]["ask"]) + if "pattern" in argument["extra"]: + actionmap_keys.add(argument["extra"]["pattern"][1]) + if "help" in argument["extra"]: + print argument["extra"]["help"] + +# These keys are used but difficult to parse +actionmap_keys.add("admin_password") + +############################################################################### +# Load en locale json keys # +############################################################################### + +en_locale_file = "/vagrant/yunohost/locales/en.json" +with open(en_locale_file) as f: + en_locale_json = json.loads(f.read()) + +en_locale_keys = set(en_locale_json.keys()) + +############################################################################### +# Compare keys used and keys defined # +############################################################################### + +used_keys = python_keys.union(actionmap_keys) + +keys_used_but_not_defined = used_keys.difference(en_locale_keys) +keys_defined_but_not_used = en_locale_keys.difference(used_keys) + +if len(keys_used_but_not_defined) != 0: + print "> Error ! Those keys are used in some files but not defined :" + for key in sorted(keys_used_but_not_defined): + print " - %s" % key + +if len(keys_defined_but_not_used) != 0: + print "> Warning ! Those keys are defined but seems unused :" + for key in sorted(keys_defined_but_not_used): + print " - %s" % key + + From 00d9b9ee864d353d7b631927f0c4e1ddd4e56504 Mon Sep 17 00:00:00 2001 From: Moul Date: Wed, 10 May 2017 22:53:40 +0200 Subject: [PATCH 0347/1066] [fix] app_checkport was broken. --- src/yunohost/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1d61ce876..acf1eb84e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1072,8 +1072,7 @@ def app_checkport(port): # This import cannot be moved on top of file because it create a recursive # import... from yunohost.tools import tools_port_available - availability = tools_port_available(port) - if availability["available"]: + if tools_port_available(port): logger.success(m18n.n('port_available', port=int(port))) else: raise MoulinetteError(errno.EINVAL, From fee4272eac097f6ff96955b8d420b8ff170d6bd5 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Thu, 11 May 2017 23:01:49 +0200 Subject: [PATCH 0348/1066] Fix ynh_local_curl (#306) POST arguments aren't given to the request if quotes are used. --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 9328936e5..8ff9e5791 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -161,7 +161,7 @@ ynh_local_curl () { if [ -n "$POST_data" ] then # Add --data arg and remove the last character, which is an unecessary '&' - POST_data="--data \"${POST_data::-1}\"" + POST_data="--data ${POST_data::-1}" fi # Curl the URL From 3f953699eb3fc0657088e7cf73951ba315d12355 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 May 2017 16:51:29 +0200 Subject: [PATCH 0349/1066] [fix] Unexpected comment line ending up in /etc/resolv.dnsmasq.conf (#312) --- data/hooks/conf_regen/43-dnsmasq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index e298f7eaa..2c8ce797b 100755 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -21,7 +21,7 @@ do_pre_regen() { cp plain/dnsmasq.conf ${pending_dir}/etc/dnsmasq.conf # add resolver file - cat plain/resolv.dnsmasq.conf | grep nameserver | shuf > ${pending_dir}/etc/resolv.dnsmasq.conf + cat plain/resolv.dnsmasq.conf | grep "^nameserver" | shuf > ${pending_dir}/etc/resolv.dnsmasq.conf # retrieve variables ipv4=$(curl -s -4 https://ip.yunohost.org 2>/dev/null || true) From 2d6abf4ffeecd3d27c7676adf95e10d0383cd47a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 May 2017 17:14:42 +0200 Subject: [PATCH 0350/1066] Use ssl-cert group instead of metronome (#222) --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b39e15178..59421cb3a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -226,8 +226,8 @@ def _certificate_install_selfsigned(domain_list, force=False): # Set appropriate permissions _set_permissions(new_cert_folder, "root", "root", 0755) - _set_permissions(key_file, "root", "metronome", 0640) - _set_permissions(crt_file, "root", "metronome", 0640) + _set_permissions(key_file, "root", "ssl-cert", 0640) + _set_permissions(crt_file, "root", "ssl-cert", 0640) _set_permissions(conf_file, "root", "root", 0600) # Actually enable the certificate we created @@ -534,7 +534,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) - _set_permissions(domain_key_file, "root", "metronome", 0640) + _set_permissions(domain_key_file, "root", "ssl-cert", 0640) _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) @@ -604,7 +604,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): f.write(signed_certificate) f.write(intermediate_certificate) - _set_permissions(domain_cert_file, "root", "metronome", 0640) + _set_permissions(domain_cert_file, "root", "ssl-cert", 0640) if staging: return From f9437bbd3339020cf46c199ac58c5a8f039850ce Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 16 May 2017 17:18:07 +0200 Subject: [PATCH 0351/1066] Global settings (#229) * [enh] add base to run tests * [enh] start global settings proto * [mod] has -> exists * [enh] configure actionmap for settings module * [enh] add a default values mecanism * [enh] nicer yaml dump * [mod] DRY * [fix] moulinette doesn't respect positionned arguments * [fix] typo * [mod] don't print when it's not needed * [enh] raise a moulinette exception when the key doesn't exists * [mod] add todo comments * [mod] no more default value * [enh] namespace all global settings commands * [mod] unix way, be quiet except when needed * [fix] forgot to add namespace argument to settings-list * [fix] fail_silently wasn't considered as a cli flag * [mod] refactoring * [enh] remove empty namespace on remove * [enh] print a warning if attempt to list an empty namespace settings * [mod] refactoring * [enh] handle case where I can't open settings file * [enh] handle case where I can't write settings file * [enh] handle case where I can't serialized settings data into yaml * [mod] add error return codes * [enh] start to move to new architecture for settings * [enh] uses a dict instead of a tuple for settings * [mod] no more namespace in settings list * [mod] settings.exists isn't relevant anymore * [mod] settings.remove isn't relevant anymore * [enh] fix settings and switch to json * [enh] adapt settings set * [enh] don't set a key that doesn't exists * [enh] check type of keys before settings them * [enh] start implement default * [enh] handle case where key doesnt exist for default * [enh] i18n for bad key type on settings set * [enh] i18n for bad key type on settings set for enum * [mod] exception for weird situation * [mod] this message isn't used anymore * [enh] i18n for unknown setting key from local settings * [mod] style * [enh] start to work on a reset mecanism * [enh] complain if settings_reset is called without yes * [fix] --yes of reset is a boolean option * [enh] backup old settings before resetting * [fix] bad usage of logger * [enh] backup unknown settings * [enh] move settings description in translations * [enh] add tests for settings * [enh] migrate to pytest assert style * [fix] typo * [doc] add some comments for not explicite part of the code * [mod] possibilities -> choices for uniformised vocabulary * [mod] follow rest semantic * [doc] made namespace usage more explicit * [fix] we don't use namespace key anymore * [enh] make settings_default available in cli * [fix] *really* be Rest semantic * [doc] add docstrings to settings module functions * [enh] reset-all and --full option * [fix] Remove unused global_settings_reset_not_yes --- data/actionsmap/yunohost.yml | 49 ++++++ locales/en.json | 13 ++ src/yunohost/settings.py | 236 ++++++++++++++++++++++++++++ src/yunohost/tests/conftest.py | 1 - src/yunohost/tests/test_settings.py | 166 +++++++++++++++++++ 5 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/settings.py create mode 100644 src/yunohost/tests/test_settings.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 39c62398c..09b5687f3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -976,6 +976,55 @@ monitor: action_help: Disable server monitoring +############################# +# Settings # +############################# +settings: + category_help: Manage YunoHost global settings + actions: + + ### settings_list() + list: + action_help: list all entries of the settings + api: GET /settings + + ### settings_get() + get: + action_help: get an entry value in the settings + api: GET /settings/ + arguments: + key: + help: Settings key + --full: + help: Show more details + action: store_true + + ### settings_set() + set: + action_help: set an entry value in the settings + api: POST /settings/ + arguments: + key: + help: Settings key + -v: + full: --value + help: new value + extra: + required: True + + ### settings_reset_all() + reset-all: + action_help: reset all settings to their default value + api: DELETE /settings + + ### settings_reset() + reset: + action_help: set an entry value to its default one + api: DELETE /settings/ + arguments: + key: + help: Settings key + ############################# # Service # ############################# diff --git a/locales/en.json b/locales/en.json index 2e85d6d4b..6deffba84 100644 --- a/locales/en.json +++ b/locales/en.json @@ -121,6 +121,19 @@ "firewall_reloaded": "The firewall has been reloaded", "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", "format_datetime_short": "%m/%d/%Y %I:%M %p", + "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}", + "global_settings_cant_serialize_setings": "Failed to serialzed settings data, reason: {reason:s}", + "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}", + "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list", + "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", + "global_settings_setting_example_bool": "Example boolean option", + "global_settings_setting_example_int": "Example int option", + "global_settings_setting_example_string": "Example string option", + "global_settings_setting_example_enum": "Example enum option", + "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", "hook_list_by_invalid": "Invalid property to list hook by", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py new file mode 100644 index 000000000..c2266a4c9 --- /dev/null +++ b/src/yunohost/settings.py @@ -0,0 +1,236 @@ +import os +import json +import errno + +from datetime import datetime +from collections import OrderedDict + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +logger = getActionLogger('yunohost.settings') + +SETTINGS_PATH = "/etc/yunohost/settings.json" +SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" + +# a settings entry is in the form of: +# namespace.subnamespace.name: {type, value, default, description, [choices]} +# choices is only for enum +# the keyname can have as many subnamespace as needed but should have at least +# one level of namespace + +# description is implied from the translated strings +# the key is "global_settings_setting_%s" % key.replace(".", "_") + +# type can be: +# * bool +# * int +# * string +# * enum (in form a python list) + +# we don't store the value in default options +DEFAULTS = OrderedDict([ + ("example.bool", {"type": "bool", "default": True}), + ("example.int", {"type": "int", "default": 42}), + ("example.string", {"type": "string", "default": "yolo swag"}), + ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), +]) + + +def settings_get(key, full=False): + """ + Get an entry value in the settings + + Keyword argument: + key -- Settings key + + """ + settings = _get_settings() + + if key not in settings: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_key_doesnt_exists', settings_key=key)) + + if full: + return settings[key] + + return settings[key]['value'] + + +def settings_list(): + """ + List all entries of the settings + + """ + return _get_settings() + + +def settings_set(key, value): + """ + Set an entry value in the settings + + Keyword argument: + key -- Settings key + value -- New value + + """ + settings = _get_settings() + + if key not in settings: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_key_doesnt_exists', settings_key=key)) + + key_type = settings[key]["type"] + + if key_type == "bool": + if not isinstance(value, bool): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) + elif key_type == "int": + if not isinstance(value, int) or isinstance(value, bool): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) + elif key_type == "string": + if not isinstance(value, basestring): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) + elif key_type == "enum": + if value not in settings[key]["choices"]: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_choice_for_enum', setting=key, + received_type=type(value).__name__, + expected_type=", ".join(settings[key]["choices"]))) + else: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_unknown_type', setting=key, + unknown_type=key_type)) + + settings[key]["value"] = value + + _save_settings(settings) + + +def settings_reset(key): + """ + Set an entry value to its default one + + Keyword argument: + key -- Settings key + + """ + settings = _get_settings() + + if key not in settings: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_key_doesnt_exists', settings_key=key)) + + settings[key]["value"] = settings[key]["default"] + _save_settings(settings) + + +def settings_reset_all(): + """ + Reset all settings to their default value + + Keyword argument: + yes -- Yes I'm sure I want to do that + + """ + settings = _get_settings() + + # For now on, we backup the previous settings in case of but we don't have + # any mecanism to take advantage of those backups. It could be a nice + # addition but we'll see if this is a common need. + # Another solution would be to use etckeeper and integrate those + # modification inside of it and take advantage of its git history + old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.now().strftime("%F_%X") + _save_settings(settings, location=old_settings_backup_path) + + for value in settings.values(): + value["value"] = value["default"] + + _save_settings(settings) + + return { + "old_settings_backup_path": old_settings_backup_path, + "message": m18n.n("global_settings_reset_success", path=old_settings_backup_path) + } + + +def _get_settings(): + settings = {} + + for key, value in DEFAULTS.copy().items(): + settings[key] = value + settings[key]["value"] = value["default"] + settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_")) + + if not os.path.exists(SETTINGS_PATH): + return settings + + # we have a very strict policy on only allowing settings that we know in + # the OrderedDict DEFAULTS + # For various reason, while reading the local settings we might encounter + # settings that aren't in DEFAULTS, those can come from settings key that + # we have removed, errors or the user trying to modify + # /etc/yunohost/settings.json + # To avoid to simply overwrite them, we store them in + # /etc/yunohost/settings-unknown.json in case of + unknown_settings = {} + unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" + + if os.path.exists(unknown_settings_path): + try: + unknown_settings = json.load(open(unknown_settings_path, "r")) + except Exception as e: + logger.warning("Error while loading unknown settings %s" % e) + + try: + with open(SETTINGS_PATH) as settings_fd: + local_settings = json.load(settings_fd) + + for key, value in local_settings.items(): + if key in settings: + settings[key] = value + settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_")) + else: + logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file', + setting_key=key)) + unknown_settings[key] = value + except Exception as e: + raise MoulinetteError(errno.EIO, m18n.n('global_settings_cant_open_settings', reason=e), + exc_info=1) + + if unknown_settings: + try: + _save_settings(unknown_settings, location=unknown_settings_path) + except Exception as e: + logger.warning("Failed to save unknown settings (because %s), aborting." % e) + + return settings + + +def _save_settings(settings, location=SETTINGS_PATH): + settings_without_description = {} + for key, value in settings.items(): + settings_without_description[key] = value + if "description" in value: + del settings_without_description[key]["description"] + + try: + result = json.dumps(settings_without_description, indent=4) + except Exception as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('global_settings_cant_serialize_setings', reason=e), + exc_info=1) + + try: + with open(location, "w") as settings_fd: + settings_fd.write(result) + except Exception as e: + raise MoulinetteError(errno.EIO, + m18n.n('global_settings_cant_write_settings', reason=e), + exc_info=1) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 946eec23c..65c1d3ace 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -105,4 +105,3 @@ def pytest_cmdline_main(config): # Initialize moulinette moulinette.init(logging_config=logging, _from_source=False) - diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py new file mode 100644 index 000000000..746f5a9d4 --- /dev/null +++ b/src/yunohost/tests/test_settings.py @@ -0,0 +1,166 @@ +import os +import json +import pytest + +from moulinette.core import MoulinetteError + +from yunohost.settings import settings_get, settings_list, _get_settings, \ + settings_set, settings_reset, settings_reset_all, \ + SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH + + +def setup_function(function): + os.system("mv /etc/yunohost/settings.json /etc/yunohost/settings.json.saved") + + +def teardown_function(function): + os.system("mv /etc/yunohost/settings.json.saved /etc/yunohost/settings.json") + + +def test_settings_get_bool(): + assert settings_get("example.bool") == True + +def test_settings_get_full_bool(): + assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Example boolean option"} + + +def test_settings_get_int(): + assert settings_get("example.int") == 42 + +def test_settings_get_full_int(): + assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Example int option"} + + +def test_settings_get_string(): + assert settings_get("example.string") == "yolo swag" + +def test_settings_get_full_string(): + assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Example string option"} + + +def test_settings_get_enum(): + assert settings_get("example.enum") == "a" + +def test_settings_get_full_enum(): + assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Example enum option", "choices": ["a", "b", "c"]} + + +def test_settings_get_doesnt_exists(): + with pytest.raises(MoulinetteError): + settings_get("doesnt.exists") + + +def test_settings_list(): + assert settings_list() == _get_settings() + + +def test_settings_set(): + settings_set("example.bool", False) + assert settings_get("example.bool") == False + + +def test_settings_set_int(): + settings_set("example.int", 21) + assert settings_get("example.int") == 21 + + +def test_settings_set_enum(): + settings_set("example.enum", "c") + assert settings_get("example.enum") == "c" + + +def test_settings_set_doesexit(): + with pytest.raises(MoulinetteError): + settings_set("doesnt.exist", True) + + +def test_settings_set_bad_type_bool(): + with pytest.raises(MoulinetteError): + settings_set("example.bool", 42) + with pytest.raises(MoulinetteError): + settings_set("example.bool", "pouet") + + +def test_settings_set_bad_type_int(): + with pytest.raises(MoulinetteError): + settings_set("example.int", True) + with pytest.raises(MoulinetteError): + settings_set("example.int", "pouet") + + +def test_settings_set_bad_type_string(): + with pytest.raises(MoulinetteError): + settings_set("example.string", True) + with pytest.raises(MoulinetteError): + settings_set("example.string", 42) + + +def test_settings_set_bad_value_enum(): + with pytest.raises(MoulinetteError): + settings_set("example.enum", True) + with pytest.raises(MoulinetteError): + settings_set("example.enum", "e") + with pytest.raises(MoulinetteError): + settings_set("example.enum", 42) + with pytest.raises(MoulinetteError): + settings_set("example.enum", "pouet") + + +def test_settings_list_modified(): + settings_set("example.int", 21) + assert settings_list()["example.int"] == {'default': 42, 'description': 'Example int option', 'type': 'int', 'value': 21} + + +def test_reset(): + settings_set("example.int", 21) + assert settings_get("example.int") == 21 + settings_reset("example.int") + assert settings_get("example.int") == settings_get("example.int", True)["default"] + + +def test_settings_reset_doesexit(): + with pytest.raises(MoulinetteError): + settings_reset("doesnt.exist") + + +def test_reset_all(): + settings_before = settings_list() + settings_set("example.bool", False) + settings_set("example.int", 21) + settings_set("example.string", "pif paf pouf") + settings_set("example.enum", "c") + assert settings_before != settings_list() + settings_reset_all() + if settings_before != settings_list(): + for i in settings_before: + assert settings_before[i] == settings_list()[i] + + +def test_reset_all_backup(): + settings_before = settings_list() + settings_set("example.bool", False) + settings_set("example.int", 21) + settings_set("example.string", "pif paf pouf") + settings_set("example.enum", "c") + settings_after_modification = settings_list() + assert settings_before != settings_after_modification + old_settings_backup_path = settings_reset_all()["old_settings_backup_path"] + + for i in settings_after_modification: + del settings_after_modification[i]["description"] + + assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) + + + +def test_unknown_keys(): + unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" + unknown_setting = { + "unkown_key": {"value": 42, "default": 31, "type": "int"}, + } + open(SETTINGS_PATH, "w").write(json.dumps(unknown_setting)) + + # stimulate a write + settings_reset_all() + + assert unknown_setting == json.load(open(unknown_settings_path, "r")) From c0146b69c60ded723e89f8ed91f1caceedd299b8 Mon Sep 17 00:00:00 2001 From: opi Date: Mon, 22 May 2017 15:23:24 +0200 Subject: [PATCH 0352/1066] [fix] Wrong identifier for diagnosis related strings. (#309) --- locales/de.json | 12 ++++++------ locales/en.json | 12 ++++++------ locales/es.json | 12 ++++++------ locales/fr.json | 12 ++++++------ locales/hi.json | 12 ++++++------ locales/it.json | 12 ++++++------ 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/locales/de.json b/locales/de.json index c51beebe1..2dc585128 100644 --- a/locales/de.json +++ b/locales/de.json @@ -217,7 +217,7 @@ "service_conf_up_to_date": "Die Konfiguration für den Dienst '{service}' ist bereits aktuell", "package_not_installed": "Das Paket '{pkgname}' ist nicht installiert", "pattern_positive_number": "Muss eine positive Zahl sein", - "diagnostic_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", + "diagnosis_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", "app_incompatible": "Die Anwendung ist nicht mit deiner YunoHost-Version kompatibel", "app_not_correctly_installed": "{app:s} scheint nicht richtig installiert worden zu sein", @@ -226,11 +226,11 @@ "app_requirements_unmeet": "Anforderungen werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", - "diagnostic_debian_version_error": "Debian Version konnte nicht abgerufen werden: {error}", - "diagnostic_monitor_disk_error": "Festplatten können nicht aufgelistet werden: {error}", - "diagnostic_monitor_network_error": "Netzwerk kann nicht angezeigt werden: {error}", - "diagnostic_monitor_system_error": "System kann nicht angezeigt werden: {error}", - "diagnostic_no_apps": "Keine Anwendung ist installiert", + "diagnosis_debian_version_error": "Debian Version konnte nicht abgerufen werden: {error}", + "diagnosis_monitor_disk_error": "Festplatten können nicht aufgelistet werden: {error}", + "diagnosis_monitor_network_error": "Netzwerk kann nicht angezeigt werden: {error}", + "diagnosis_monitor_system_error": "System kann nicht angezeigt werden: {error}", + "diagnosis_no_apps": "Keine Anwendung ist installiert", "domains_available": "Verfügbare Domains:", "dyndns_key_not_found": "DNS-Schlüssel für die Domain wurde nicht gefunden", "dyndns_no_domain_registered": "Es wurde keine Domain mit DynDNS registriert", diff --git a/locales/en.json b/locales/en.json index 6deffba84..c1d4d9a39 100644 --- a/locales/en.json +++ b/locales/en.json @@ -79,12 +79,12 @@ "backup_running_hooks": "Running backup hooks...", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", - "diagnostic_debian_version_error": "Can't retrieve the Debian version: {error}", - "diagnostic_kernel_version_error": "Can't retrieve kernel version: {error}", - "diagnostic_monitor_disk_error": "Can't monitor disks: {error}", - "diagnostic_monitor_network_error": "Can't monitor network: {error}", - "diagnostic_monitor_system_error": "Can't monitor system: {error}", - "diagnostic_no_apps": "No installed application", + "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", + "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}", + "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", + "diagnosis_monitor_network_error": "Can't monitor network: {error}", + "diagnosis_monitor_system_error": "Can't monitor system: {error}", + "diagnosis_no_apps": "No installed application", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_created": "The domain has been created", "domain_creation_failed": "Unable to create domain", diff --git a/locales/es.json b/locales/es.json index d0ed51914..130951494 100644 --- a/locales/es.json +++ b/locales/es.json @@ -65,12 +65,12 @@ "backup_running_hooks": "Ejecutando los hooks de copia de seguridad...", "custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app:s}", "custom_appslist_name_required": "Debe proporcionar un nombre para su lista de aplicaciones personalizadas", - "diagnostic_debian_version_error": "No se puede obtener la versión de Debian: {error}", - "diagnostic_kernel_version_error": "No se puede obtener la versión del kernel: {error}", - "diagnostic_monitor_disk_error": "No se pueden monitorizar los discos: {error}", - "diagnostic_monitor_network_error": "No se puede monitorizar la red: {error}", - "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", - "diagnostic_no_apps": "Aplicación no instalada", + "diagnosis_debian_version_error": "No se puede obtener la versión de Debian: {error}", + "diagnosis_kernel_version_error": "No se puede obtener la versión del kernel: {error}", + "diagnosis_monitor_disk_error": "No se pueden monitorizar los discos: {error}", + "diagnosis_monitor_network_error": "No se puede monitorizar la red: {error}", + "diagnosis_monitor_system_error": "No se puede monitorizar el sistema: {error}", + "diagnosis_no_apps": "Aplicación no instalada", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecute 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", diff --git a/locales/fr.json b/locales/fr.json index 83230955e..ce6e8192f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -66,12 +66,12 @@ "backup_running_hooks": "Exécution des scripts de sauvegarde...", "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {app:s}", "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée", - "diagnostic_debian_version_error": "Impossible de déterminer la version de Debian : {error}", - "diagnostic_kernel_version_error": "Impossible de récupérer la version du noyau : {error}", - "diagnostic_monitor_disk_error": "Impossible de superviser les disques : {error}", - "diagnostic_monitor_network_error": "Impossible de superviser le réseau : {error}", - "diagnostic_monitor_system_error": "Impossible de superviser le système : {error}", - "diagnostic_no_apps": "Aucune application installée", + "diagnosis_debian_version_error": "Impossible de déterminer la version de Debian : {error}", + "diagnosis_kernel_version_error": "Impossible de récupérer la version du noyau : {error}", + "diagnosis_monitor_disk_error": "Impossible de superviser les disques : {error}", + "diagnosis_monitor_network_error": "Impossible de superviser le réseau : {error}", + "diagnosis_monitor_system_error": "Impossible de superviser le système : {error}", + "diagnosis_no_apps": "Aucune application installée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »", "domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", diff --git a/locales/hi.json b/locales/hi.json index 2ed328282..015fd4e5e 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -64,12 +64,12 @@ "backup_running_hooks": "बैकअप हुक्स चल रहे है...", "custom_app_url_required": "आप को अपनी कस्टम एप्लिकेशन '{app:s}' को अपग्रेड करने के लिए यूआरएल(URL) देने की आवश्यकता है", "custom_appslist_name_required": "आप को अपनी कस्टम एप्लीकेशन के लिए नाम देने की आवश्यकता है", - "diagnostic_debian_version_error": "डेबियन वर्जन प्राप्त करने में असफलता {error}", - "diagnostic_kernel_version_error": "कर्नेल वर्जन प्राप्त नहीं की जा पा रही : {error}", - "diagnostic_monitor_disk_error": "डिस्क की मॉनिटरिंग नहीं की जा पा रही: {error}", - "diagnostic_monitor_network_error": "नेटवर्क की मॉनिटरिंग नहीं की जा पा रही: {error}", - "diagnostic_monitor_system_error": "सिस्टम की मॉनिटरिंग नहीं की जा पा रही: {error}", - "diagnostic_no_apps": "कोई एप्लीकेशन इन्सटाल्ड नहीं है", + "diagnosis_debian_version_error": "डेबियन वर्जन प्राप्त करने में असफलता {error}", + "diagnosis_kernel_version_error": "कर्नेल वर्जन प्राप्त नहीं की जा पा रही : {error}", + "diagnosis_monitor_disk_error": "डिस्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnosis_monitor_network_error": "नेटवर्क की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnosis_monitor_system_error": "सिस्टम की मॉनिटरिंग नहीं की जा पा रही: {error}", + "diagnosis_no_apps": "कोई एप्लीकेशन इन्सटाल्ड नहीं है", "dnsmasq_isnt_installed": "dnsmasq इन्सटाल्ड नहीं लगता,इनस्टॉल करने के लिए किप्या ये कमांड चलाये 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cert_gen_failed": "सर्टिफिकेट उत्पन करने में असमर्थ", "domain_created": "डोमेन बनाया गया", diff --git a/locales/it.json b/locales/it.json index be94afa09..cb5f35d81 100644 --- a/locales/it.json +++ b/locales/it.json @@ -84,12 +84,12 @@ "backup_running_hooks": "Esecuzione dei hook di backup...", "custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}", "custom_appslist_name_required": "Devi fornire un nome per la lista di applicazioni personalizzata", - "diagnostic_debian_version_error": "Impossibile riportare la versione di Debian: {error}", - "diagnostic_kernel_version_error": "Impossibile riportare la versione del kernel: {error}", - "diagnostic_monitor_disk_error": "Impossibile controllare i dischi: {error}", - "diagnostic_monitor_network_error": "Impossibile controllare la rete: {error}", - "diagnostic_monitor_system_error": "Impossibile controllare il sistema: {error}", - "diagnostic_no_apps": "Nessuna applicazione installata", + "diagnosis_debian_version_error": "Impossibile riportare la versione di Debian: {error}", + "diagnosis_kernel_version_error": "Impossibile riportare la versione del kernel: {error}", + "diagnosis_monitor_disk_error": "Impossibile controllare i dischi: {error}", + "diagnosis_monitor_network_error": "Impossibile controllare la rete: {error}", + "diagnosis_monitor_system_error": "Impossibile controllare il sistema: {error}", + "diagnosis_no_apps": "Nessuna applicazione installata", "dnsmasq_isnt_installed": "dnsmasq non sembra installato, impartisci il comando 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_creation_failed": "Impossibile creare un dominio", "domain_deleted": "Il dominio è stato cancellato", From c0a40dd2df551e5e7cda8ba7b8f987bc52c81dce Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 22 May 2017 15:24:16 +0200 Subject: [PATCH 0353/1066] New helpers ynh_store_file_checksum and ynh_backup_if_checksum_is_different (#286) * New helpers ynh_store_checksum_config and ynh_compare_checksum_config Helpers for avoid destruction of personalised config files. If the config file was manually modified, make a backup of it. The name of this backup is returned, so the packager can choose which of this both file will used by default. * Implement @JimboJoe's comments. * Setting local variables as local * Adding warning about $app that should be defined * Remove "globally" in comment to limit confusion * Remove "globally" in comment to limit confusion * Remove compress and use /home/yunohost.conf/backup * Changing timestamp format to match regen-conf's * Tested and fixed ;) --- data/helpers.d/filesystem | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 0d13c4825..f0f3afb06 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -81,6 +81,44 @@ properly with chmod/chown." >&2 echo $TMP_DIR } +# Calculate and store a file checksum into the app settings +# +# $app should be defined when calling this helper +# +# usage: ynh_store_file_checksum file +# | arg: file - The file on which the checksum will performed, then stored. +ynh_store_file_checksum () { + local checksum_setting_name=checksum_${1//[\/ ]/_} # Replace all '/' and ' ' by '_' + ynh_app_setting_set $app $checksum_setting_name $(sudo md5sum "$1" | cut -d' ' -f1) +} + +# Verify the checksum and backup the file if it's different +# This helper is primarily meant to allow to easily backup personalised/manually +# modified config files. +# +# $app should be defined when calling this helper +# +# usage: ynh_backup_if_checksum_is_different file +# | arg: file - The file on which the checksum test will be perfomed. +# +# | ret: Return the name a the backup file, or nothing +ynh_backup_if_checksum_is_different () { + local file=$1 + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + local checksum_value=$(ynh_app_setting_get $app $checksum_setting_name) + if [ -n "$checksum_value" ] + then # Proceed only if a value was stored into the app settings + if ! echo "$checksum_value $file" | sudo md5sum -c --status + then # If the checksum is now different + backup_file="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')" + sudo mkdir -p "$(dirname "$backup_file")" + sudo cp -a "$file" "$backup_file" # Backup the current file + echo "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file" >&2 + echo "$backup_file" # Return the name of the backup file + fi + fi +} + # Remove a file or a directory securely # # usage: ynh_secure_remove path_to_remove From 7af130d81bee16522f799cbfc28cb9f4d1acb7c2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 May 2017 15:24:47 +0200 Subject: [PATCH 0354/1066] Allow regen-conf to manage new files already when they're already present on the system (#311) --- locales/en.json | 3 ++- src/yunohost/service.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index c1d4d9a39..6c6c8f277 100644 --- a/locales/en.json +++ b/locales/en.json @@ -216,10 +216,11 @@ "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'", "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created", - "service_conf_file_not_managed": "The configuration file '{conf}' is not managed yet and will not be updated", "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", "service_conf_file_removed": "The configuration file '{conf}' has been removed", "service_conf_file_updated": "The configuration file '{conf}' has been updated", + "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", + "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", diff --git a/src/yunohost/service.py b/src/yunohost/service.py index e64c9cd89..91e654f0b 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -391,15 +391,21 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, logger.debug("> no changes to system conf has been made") conf_status = 'managed' regenerated = True - elif force and to_remove: + elif not to_remove: + # If the conf exist but is not managed yet, and is not to be removed, + # we assume that it is safe to regen it, since the file is backuped + # anyway (by default in _regen), as long as we warn the user + # appropriately. + logger.warning(m18n.n('service_conf_new_managed_file', + conf=system_path, service=service)) + regenerated = _regen(system_path, pending_path) + conf_status = 'new' + elif force: regenerated = _regen(system_path) conf_status = 'force-removed' - elif force: - regenerated = _regen(system_path, pending_path) - conf_status = 'force-updated' else: - logger.warning(m18n.n('service_conf_file_not_managed', - conf=system_path)) + logger.warning(m18n.n('service_conf_file_kept_back', + conf=system_path, service=service)) conf_status = 'unmanaged' # -> system conf has not been manually modified elif system_hash == saved_hash: From 6371ab4a5780b522bf003162e8bcb90b0d2762f3 Mon Sep 17 00:00:00 2001 From: opi Date: Wed, 24 May 2017 08:42:16 +0200 Subject: [PATCH 0355/1066] [enh] Increase message size. #914 (#307) --- data/templates/postfix/main.cf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index b0b2688d9..0501b56da 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -60,8 +60,8 @@ mailbox_size_limit = 0 recipient_delimiter = + inet_interfaces = all -#### Fit to the maximum message size allowed by GMail or Yahoo #### -message_size_limit = 26214400 +#### Fit to the maximum message size to 30mb, more than allowed by GMail or Yahoo #### +message_size_limit = 31457280 # Virtual Domains Control virtual_mailbox_domains = ldap:/etc/postfix/ldap-domains.cf From 7102bf1b0eadc9eb34739125f2eafd2722788556 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 31 May 2017 16:09:39 +0200 Subject: [PATCH 0356/1066] [fix] Missing change from metronome to ssl-cert (#316) --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 59421cb3a..a7d3b9389 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -595,7 +595,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): # Move the private key domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") shutil.move(domain_key_file, domain_key_file_finaldest) - _set_permissions(domain_key_file_finaldest, "root", "metronome", 0640) + _set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0640) # Write the cert domain_cert_file = os.path.join(new_cert_folder, "crt.pem") From 64ab4a82b3b1c1cd3006019af395b072f564de5a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Wed, 31 May 2017 17:06:40 +0200 Subject: [PATCH 0357/1066] [fix] Yypos in en.json (#314) * serialzed => serialized * typo close global_settings_key_doesnt_exists * serialized/serialize & setings/settings --- locales/en.json | 4 ++-- src/yunohost/settings.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6c6c8f277..2ba2a48f6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -124,9 +124,9 @@ "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}", "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}", "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}", - "global_settings_cant_serialize_setings": "Failed to serialzed settings data, reason: {reason:s}", + "global_settings_cant_serialize_settings": "Failed to serialize settings data, reason: {reason:s}", "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}", - "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list", + "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'", "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", "global_settings_setting_example_bool": "Example boolean option", "global_settings_setting_example_int": "Example int option", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index c2266a4c9..f11371aee 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -224,7 +224,7 @@ def _save_settings(settings, location=SETTINGS_PATH): result = json.dumps(settings_without_description, indent=4) except Exception as e: raise MoulinetteError(errno.EINVAL, - m18n.n('global_settings_cant_serialize_setings', reason=e), + m18n.n('global_settings_cant_serialize_settings', reason=e), exc_info=1) try: From 2de7e3301bd1b1ec5dd4dcf4b25ad371d475314e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Jun 2017 18:14:39 +0200 Subject: [PATCH 0358/1066] [fix] Typo in applist migration (#317) --- src/yunohost/app.py | 8 ++++---- src/yunohost/tests/test_appslist.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index acf1eb84e..044b81632 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1877,20 +1877,20 @@ def _parse_app_instance_name(app_instance_name): def _using_legacy_appslist_system(): """ Return True if we're using the old fetchlist scheme. - This is determined by the presence of some cron job yunohost-appslist-foo + This is determined by the presence of some cron job yunohost-applist-foo """ - return glob.glob("/etc/cron.d/yunohost-appslist-*") != [] + return glob.glob("/etc/cron.d/yunohost-applist-*") != [] def _migrate_appslist_system(): """ Migrate from the legacy fetchlist system to the new one """ - legacy_crons = glob.glob("/etc/cron.d/yunohost-appslist-*") + legacy_crons = glob.glob("/etc/cron.d/yunohost-applist-*") for cron_path in legacy_crons: - appslist_name = os.path.basename(cron_path).replace("yunohost-appslist-", "") + appslist_name = os.path.basename(cron_path).replace("yunohost-applist-", "") logger.info(m18n.n('appslist_migrating', appslist=appslist_name)) # Parse appslist url in cron diff --git a/src/yunohost/tests/test_appslist.py b/src/yunohost/tests/test_appslist.py index 4750e32a8..6b7141f4a 100644 --- a/src/yunohost/tests/test_appslist.py +++ b/src/yunohost/tests/test_appslist.py @@ -22,7 +22,7 @@ def setup_function(function): os.remove(f) # Clear appslist crons - files = glob.glob("/etc/cron.d/yunohost-appslist-*") + files = glob.glob("/etc/cron.d/yunohost-applist-*") for f in files: os.remove(f) @@ -280,7 +280,7 @@ def test_appslist_remove_unknown(): def add_legacy_cron(name, url): - with open("/etc/cron.d/yunohost-appslist-%s" % name, "w") as f: + with open("/etc/cron.d/yunohost-applist-%s" % name, "w") as f: f.write('00 00 * * * root yunohost app fetchlist -u %s -n %s > /dev/null 2>&1\n' % (url, name)) @@ -288,7 +288,7 @@ def test_appslist_check_using_legacy_system_testFalse(): """ If no legacy cron job is there, the check should return False """ - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] assert _using_legacy_appslist_system() is False @@ -296,7 +296,7 @@ def test_appslist_check_using_legacy_system_testTrue(): """ If there's a legacy cron job, the check should return True """ - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] add_legacy_cron("yunohost", "https://app.yunohost.org/official.json") assert _using_legacy_appslist_system() is True @@ -307,7 +307,7 @@ def test_appslist_system_migration(): """ # Start with no legacy cron, no appslist registered - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] assert app_listlists() == {} assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") @@ -321,7 +321,7 @@ def test_appslist_system_migration(): assert _using_legacy_appslist_system() is False # No legacy cron job should remain - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] # Check they are in app_listlists anyway appslist_dict = app_listlists() @@ -339,7 +339,7 @@ def test_appslist_system_migration_badcron(): """ # Start with no legacy cron, no appslist registered - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] assert app_listlists() == {} assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") @@ -352,7 +352,7 @@ def test_appslist_system_migration_badcron(): assert _using_legacy_appslist_system() is False # No legacy cron should remain, but it should be backuped in /etc/yunohost - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] assert os.path.exists("/etc/yunohost/wtflist.oldlist.bkp") # Appslist should still be empty @@ -365,7 +365,7 @@ def test_appslist_system_migration_conflict(): """ # Start with no legacy cron, no appslist registered - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] assert app_listlists() == {} assert not os.path.exists("/etc/cron.daily/yunohost-fetch-appslists") @@ -379,7 +379,7 @@ def test_appslist_system_migration_conflict(): assert _using_legacy_appslist_system() is False # No legacy cron job should remain - assert glob.glob("/etc/cron.d/yunohost-appslist-*") == [] + assert glob.glob("/etc/cron.d/yunohost-applist-*") == [] # Only one among "dummy" and "yunohost" should be listed appslist_dict = app_listlists() From d3eeb4bbc7a746ec54ab7577507e6f5f21fd01d4 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 2 Jun 2017 13:41:16 +0200 Subject: [PATCH 0359/1066] [enh] Refactor backup management to pave the way to borg (#275) * [enh] Use a csv to list file to backup * [enh] Use csv python module * [wip] Backup refactoring * [wip] Backup class refactoring * [enh] Add archivemount dependencies * [wip] Restore refactoring * [fix] Some error in this refactoring * [fix] Missing backup key translation * [fix] Bad YNH_CWD in hook backup * [fix] App backup part was broken * [fix] Restore operation was broken * [fix] No compressed backup * [fix] Don't commit backup path into csv if app backup fail * [fix] Default backup collect_dir should be in tmp subdir * [enh] Simplify a copy code * [enh] Build backup info from properties * [enh] Improve comments presentation * Adding first tests for backup/restore * Adding more backup/restore app test scenario * [enh] Separate BackupMethods in distinct class * Adding test of restoring a wordpress archive from 2.4 * [fix] Be able to delete backup link too * [fix] Bad internationalization key * [fix] Edge case with empty mysql pwd restore * [fix] Unset var in restore * [fix] Edge case with empty mysql pwd restore * Adding test for backup crash handling * Cleaning tests + checking tmp dir is empty * [fix] Missing tmp in backup path * [fix] Error on reading backup csv * Adding test of failed restore * Adding tests when not enough space available * Simplifying tests using markers * [fix] ynh backup/restore helpers with only one arg * [fix] Unmount subdir with python * [enh] Improve backup size management * [fix] None object in backup * [enh] Remove dead code * [fix] Missing locales * [enh] Adapat test about needed space * [fix] Pass some test * [enh] Remove dead code * [enh] Pass all test * [enh] Adding test that backups contains what's expected * Fix typo in tests * [fix] Bad documentation * [enh] Add comment * [enh] Use len in place of implicit {} == False * [enh] Add comment * [enh] Add comment * [enh] Refactoring on _collect_app_files * Adding skeleton for remaining tests to write * [enh] Use a csv to list file to backup * [enh] Use csv python module * [wip] Backup refactoring * [wip] Backup class refactoring * [enh] Add archivemount dependencies * [wip] Restore refactoring * [fix] Some error in this refactoring * [fix] Missing backup key translation * [fix] Bad YNH_CWD in hook backup * [fix] App backup part was broken * [fix] Restore operation was broken * [fix] No compressed backup * [fix] Don't commit backup path into csv if app backup fail * [fix] Default backup collect_dir should be in tmp subdir * [enh] Simplify a copy code * [enh] Build backup info from properties * [enh] Improve comments presentation * Adding first tests for backup/restore * Adding more backup/restore app test scenario * [enh] Separate BackupMethods in distinct class * Adding test of restoring a wordpress archive from 2.4 * [fix] Be able to delete backup link too * [fix] Bad internationalization key * [fix] Edge case with empty mysql pwd restore * [fix] Unset var in restore * [fix] Edge case with empty mysql pwd restore * Adding test for backup crash handling * Cleaning tests + checking tmp dir is empty * [fix] Missing tmp in backup path * [fix] Error on reading backup csv * Adding test of failed restore * Adding tests when not enough space available * Simplifying tests using markers * [fix] ynh backup/restore helpers with only one arg * [fix] Unmount subdir with python * [enh] Improve backup size management * [fix] None object in backup * [enh] Remove dead code * [fix] Missing locales * [enh] Adapat test about needed space * [fix] Pass some test * [enh] Remove dead code * [enh] Pass all test * [enh] Adding test that backups contains what's expected * Fix typo in tests * [fix] Bad documentation * Adding skeleton for remaining tests to write * [enh] Add comment * [enh] Use len in place of implicit {} == False * [enh] Add comment * [enh] Add comment * [enh] Refactoring on _collect_app_files * [fix] Replay e1a507 deleted by rebase * [fix] ynh_restore helper * Renaming 'hooks' terminology to 'system' where it makes sense * Propagating new --system/--ignore-system to actionmap * Adding more tests + clarifying some functions and messages * Factorize out the definition and validation of backup/restore targets * Add missing key * Use list comprehension instead of dirty loops * [enh] Add docstring in BackupManager * [enh] Add docstring on BackupMethod(s) * [fix] Remove deadcode * [fix] Remove debug message * [enh] Add comments on RestoreManager * [enh] Add comments on backup constants * Adding a proper report/result for each backup target * Skipping tests not implemented yet * Fixing little mistake from merging * [fix] Support different fs or archivemount error * [enh] Backup helpers readability * [fix] Copy backup method * [fix] Deprecated warning always displayed * [enh] Retrieve info.json file inside tar.gz * Trying to reorganize methods with sections for readability * [enh] Support archivemount failure * [fix] Missing env var for system part restore helpers * Clarifying disk usage / free space computation * [enh] Refactoring around backup set_targets() * Clarifying structure of backup_create and backup_restore * Move RestoreManager between BackupManager and BackupMethods * [fix] Missing locales * [fix] System part restore if archivemount failure * [enh] Extract all conf instead of specific code * [fix] Other output directory (compressed archive) * [enh] Add test for uncompressed backup * [fix] Compressed backup in an existing output directory * [fix] Return size for retro-compatibility * [fix] Mountpoint check aborting script when called with -eu * [fix] Avoid failure test with set -eu * [fix] locale strings missing/bad arguments * Check free space before mount * [fix] ynh_restore_helpers with existing archive path * Adding skeletons for moar tests * Fixing some weird bug in _get_archive_path * Adding a regen-conf at the end of system restore * Adding tests of system restore from 2.4 * Have a class dedicated to target management * Cleaning tests * Misc formatting * More meaningful variable names inside app restore * [fix] can't call source ../settings/scripts/_common.sh in app backup * [fix] ynh_install_app_dependencies is not compatible with readonly mount * [fix] Remove temporary file --- data/actionsmap/yunohost.yml | 33 +- data/helpers.d/filesystem | 230 +- data/helpers.d/package | 5 +- data/hooks/post_backup_create/99-umount | 13 - data/hooks/restore/11-conf_ynh_mysql | 8 +- debian/control | 1 + locales/en.json | 35 +- src/yunohost/backup.py | 2507 ++++++++++++++++++---- src/yunohost/tests/test_backuprestore.py | 637 ++++++ 9 files changed, 2964 insertions(+), 505 deletions(-) delete mode 100644 data/hooks/post_backup_create/99-umount create mode 100644 src/yunohost/tests/test_backuprestore.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 09b5687f3..cccc1adbb 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -748,18 +748,25 @@ backup: full: --no-compress help: Do not create an archive file action: store_true - --hooks: - help: List of backup hooks names to execute + --system: + help: List of system parts to backup (all by default) nargs: "*" - --ignore-hooks: - help: Do not execute backup hooks - action: store_true --apps: - help: List of application names to backup + help: List of application names to backup (all by default) nargs: "*" + --hooks: + help: (Deprecated) See --system + nargs: "*" + --ignore-system: + help: Do not backup system + action: store_true --ignore-apps: help: Do not backup apps action: store_true + --ignore-hooks: + help: (Deprecated) See --ignore-system + action: store_true + ### backup_restore() restore: @@ -772,17 +779,23 @@ backup: arguments: name: help: Name of the local backup archive - --hooks: - help: List of restauration hooks names to execute + --system: + help: List of system parts to restore (all by default) nargs: "*" --apps: - help: List of application names to restore + help: List of application names to restore (all by default) nargs: "*" + --hooks: + help: (Deprecated) See --system + nargs: "*" + --ignore-system: + help: Do not restore system parts + action: store_true --ignore-apps: help: Do not restore apps action: store_true --ignore-hooks: - help: Do not restore hooks + help: (Deprecated) See --ignore-system action: store_true --force: help: Force restauration on an already installed system diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index f0f3afb06..6fb073e06 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -1,57 +1,205 @@ CAN_BIND=${CAN_BIND:-1} -# Mark a file or a directory for backup -# Note: currently, SRCPATH will be copied or binded to DESTPATH +# Add a file or a directory to the list of paths to backup +# +# Note: this helper could be used in backup hook or in backup script inside an +# app package +# +# Details: ynh_backup writes SRC and the relative DEST into a CSV file. And it +# creates the parent destination directory +# +# If DEST is ended by a slash it complete this path with the basename of SRC. +# +# usage: ynh_backup src [dest [is_big [arg]]] +# | arg: src - file or directory to bind or symlink or copy. it shouldn't be in +# the backup dir. +# | arg: dest - destination file or directory inside the +# backup dir +# | arg: is_big - 1 to indicate data are big (mail, video, image ...) +# | arg: arg - Deprecated arg +# +# example: +# # Wordpress app context +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" +# # => This line will be added into CSV file +# # "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/nginx.conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf/" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf" +# +# #Deprecated usages (maintained for retro-compatibility) +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "${backup_dir}/conf/nginx.conf" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/nginx.conf" +# +# ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "/conf/" +# # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf" # -# usage: ynh_backup srcdir destdir to_bind no_root -# | arg: srcdir - directory to bind or copy -# | arg: destdir - mountpoint or destination directory -# | arg: to_bind - 1 to bind mounting the directory if possible -# | arg: no_root - 1 to execute commands as current user ynh_backup() { - local SRCPATH=$1 - local DESTPATH=$2 - local TO_BIND=${3:-0} - local SUDO_CMD="sudo" - [[ "${4:-}" = "1" ]] && SUDO_CMD= + # TODO find a way to avoid injection by file strange naming ! + local SRC_PATH="$1" + local DEST_PATH="${2:-}" + local IS_BIG="${3:-0}" - # validate arguments - [[ -e "${SRCPATH}" ]] || { - echo "Source path '${SRCPATH}' does not exist" >&2 + # ============================================================================== + # Format correctly source and destination paths + # ============================================================================== + # Be sure the source path is not empty + [[ -e "${SRC_PATH}" ]] || { + echo "Source path '${SRC_PATH}' does not exist" >&2 return 1 } - # prepend the backup directory - [[ -n "${YNH_APP_BACKUP_DIR:-}" && "${DESTPATH:0:1}" != "/" ]] \ - && DESTPATH="${YNH_APP_BACKUP_DIR}/${DESTPATH}" - [[ ! -e "${DESTPATH}" ]] || { - echo "Destination path '${DESTPATH}' already exist" >&2 - return 1 - } + # Transform the source path as an absolute path + # If it's a dir remove the ending / + SRC_PATH=$(realpath "$SRC_PATH") - # attempt to bind mounting the directory - if [[ "${CAN_BIND}" = "1" && "${TO_BIND}" = "1" ]]; then - eval $SUDO_CMD mkdir -p "${DESTPATH}" + # If there is no destination path, initialize it with the source path + # relative to "/". + # eg: SRC_PATH=/etc/yunohost -> DEST_PATH=etc/yunohost + if [[ -z "$DEST_PATH" ]]; then - if sudo mount --rbind "${SRCPATH}" "${DESTPATH}"; then - # try to remount destination directory as read-only - sudo mount -o remount,ro,bind "${SRCPATH}" "${DESTPATH}" \ - || true - return 0 - else - CAN_BIND=0 - echo "Bind mounting seems to be disabled on your system." - echo "You have maybe to check your apparmor configuration." + DEST_PATH="${SRC_PATH#/}" + + else + if [[ "${DEST_PATH:0:1}" == "/" ]]; then + + # If the destination path is an absolute path, transform it as a path + # relative to the current working directory ($YNH_CWD) + # + # If it's an app backup script that run this helper, YNH_CWD is equal to + # $YNH_BACKUP_DIR/apps/APP_INSTANCE_NAME/backup/ + # + # If it's a system part backup script, YNH_CWD is equal to $YNH_BACKUP_DIR + DEST_PATH="${DEST_PATH#$YNH_CWD/}" + + # Case where $2 is an absolute dir but doesn't begin with $YNH_CWD + [[ "${DEST_PATH:0:1}" == "/" ]] \ + && DEST_PATH="${DEST_PATH#/}" fi - # delete mountpoint directory safely - mountpoint -q "${DESTPATH}" && sudo umount -R "${DESTPATH}" - eval $SUDO_CMD rm -rf "${DESTPATH}" + # Complete DEST_PATH if ended by a / + [[ "${DEST_PATH: -1}" == "/" ]] \ + && DEST_PATH="${DEST_PATH}/$(basename $SRC_PATH)" fi - # ... or just copy the directory - eval $SUDO_CMD mkdir -p $(dirname "${DESTPATH}") - eval $SUDO_CMD cp -a "${SRCPATH}" "${DESTPATH}" + # Check if DEST_PATH already exists in tmp archive + [[ ! -e "${DEST_PATH}" ]] || { + echo "Destination path '${DEST_PATH}' already exist" >&2 + return 1 + } + + # Add the relative current working directory to the destination path + local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR}" + REL_DIR="${REL_DIR%/}/" + DEST_PATH="${REL_DIR}${DEST_PATH}" + DEST_PATH="${DEST_PATH#/}" + # ============================================================================== + + # ============================================================================== + # Write file to backup into backup_list + # ============================================================================== + local SRC=$(echo "${SRC_PATH}" | sed -r 's/"/\"\"/g') + local DEST=$(echo "${DEST_PATH}" | sed -r 's/"/\"\"/g') + echo "\"${SRC}\",\"${DEST}\"" >> "${YNH_BACKUP_CSV}" + + # ============================================================================== + + # Create the parent dir of the destination path + # It's for retro compatibility, some script consider ynh_backup creates this dir + mkdir -p $(dirname "$YNH_BACKUP_DIR/${DEST_PATH}") +} + +# Restore all files linked to the restore hook or to the restore app script +# +# usage: ynh_restore +# +ynh_restore () { + # Deduce the relative path of $YNH_CWD + local REL_DIR="${YNH_CWD#$YNH_BACKUP_DIR/}" + REL_DIR="${REL_DIR%/}/" + + # For each destination path begining by $REL_DIR + cat ${YNH_BACKUP_CSV} | tr -d $'\r' | grep -ohP "^\".*\",\"$REL_DIR.*\"$" | \ + while read line; do + local ORIGIN_PATH=$(echo "$line" | grep -ohP "^\"\K.*(?=\",\".*\"$)") + local ARCHIVE_PATH=$(echo "$line" | grep -ohP "^\".*\",\"$REL_DIR\K.*(?=\"$)") + ynh_restore_file "$ARCHIVE_PATH" "$ORIGIN_PATH" + done +} + +# Return the path in the archive where has been stocked the origin path +# +# usage: _get_archive_path ORIGIN_PATH +_get_archive_path () { + # For security reasons we use csv python library to read the CSV + sudo python -c " +import sys +import csv +with open(sys.argv[1], 'r') as backup_file: + backup_csv = csv.DictReader(backup_file, fieldnames=['source', 'dest']) + for row in backup_csv: + if row['source']==sys.argv[2].strip('\"'): + print row['dest'] + sys.exit(0) + raise Exception('Original path for %s not found' % sys.argv[2]) + " "${YNH_BACKUP_CSV}" "$1" + return $? +} + +# Restore a file or a directory +# +# Use the registered path in backup_list by ynh_backup to restore the file at +# the good place. +# +# usage: ynh_restore_file ORIGIN_PATH [ DEST_PATH ] +# | arg: ORIGIN_PATH - Path where was located the file or the directory before +# to be backuped or relative path to $YNH_CWD where it is located in the backup archive +# | arg: DEST_PATH - Path where restore the file or the dir, if unspecified, +# the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in +# the archive, the destination will be searched into backup.csv +# +# examples: +# ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf" +# # if apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf exists, restore it into +# # /etc/nginx/conf.d/$domain.d/$app.conf +# # if no, search a correspondance in the csv (eg: conf/nginx.conf) and restore it into +# # /etc/nginx/conf.d/$domain.d/$app.conf +# +# # DON'T GIVE THE ARCHIVE PATH: +# ynh_restore_file "conf/nginx.conf" +# +ynh_restore_file () { + local ORIGIN_PATH="/${1#/}" + local ARCHIVE_PATH="$YNH_CWD${ORIGIN_PATH}" + # Default value for DEST_PATH = /$ORIGIN_PATH + local DEST_PATH="${2:-$ORIGIN_PATH}" + + # If ARCHIVE_PATH doesn't exist, search for a corresponding path in CSV + if [ ! -d "$ARCHIVE_PATH" ] && [ ! -f "$ARCHIVE_PATH" ] && [ ! -L "$ARCHIVE_PATH" ]; then + ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" + fi + + # Restore ORIGIN_PATH into DEST_PATH + mkdir -p $(dirname "$DEST_PATH") + + # Do a copy if it's just a mounting point + if mountpoint -q $YNH_BACKUP_DIR; then + if [[ -d "${ARCHIVE_PATH}" ]]; then + ARCHIVE_PATH="${ARCHIVE_PATH}/." + mkdir -p "$DEST_PATH" + fi + cp -a "$ARCHIVE_PATH" "${DEST_PATH}" + # Do a move if YNH_BACKUP_DIR is already a copy + else + mv "$ARCHIVE_PATH" "${DEST_PATH}" + fi } # Deprecated helper since it's a dangerous one! @@ -60,7 +208,7 @@ ynh_bind_or_cp() { local NO_ROOT=0 [[ "${AS_ROOT}" = "1" ]] || NO_ROOT=1 echo "This helper is deprecated, you should use ynh_backup instead" >&2 - ynh_backup "$1" "$2" 1 "$NO_ROOT" + ynh_backup "$1" "$2" 1 } # Create a directory under /tmp diff --git a/data/helpers.d/package b/data/helpers.d/package index b292a6c8e..66982cf25 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -121,7 +121,7 @@ ynh_install_app_dependencies () { if ynh_package_is_installed "${dep_app}-ynh-deps"; then echo "A package named ${dep_app}-ynh-deps is already installed" >&2 else - cat > ./${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build + cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc Priority: optional Package: ${dep_app}-ynh-deps @@ -131,8 +131,9 @@ Architecture: all Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. EOF - ynh_package_install_from_equivs ./${dep_app}-ynh-deps.control \ + ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies + rm /tmp/${dep_app}-ynh-deps.control ynh_app_setting_set $app apt_dependencies $dependencies fi } diff --git a/data/hooks/post_backup_create/99-umount b/data/hooks/post_backup_create/99-umount deleted file mode 100644 index a9ad5efec..000000000 --- a/data/hooks/post_backup_create/99-umount +++ /dev/null @@ -1,13 +0,0 @@ - -tmp_dir=$1 -retcode=$2 - -FAILURE=0 - -# Iterate over inverted ordered mountpoints to prevent issues -for m in $(mount | grep " ${tmp_dir}" | awk '{ print $3 }' | tac); do - sudo umount $m - [[ $? != 0 ]] && FAILURE=1 -done - -exit $FAILURE diff --git a/data/hooks/restore/11-conf_ynh_mysql b/data/hooks/restore/11-conf_ynh_mysql index b2f8c8e31..8b8438c0e 100644 --- a/data/hooks/restore/11-conf_ynh_mysql +++ b/data/hooks/restore/11-conf_ynh_mysql @@ -6,9 +6,13 @@ service mysql status >/dev/null 2>&1 \ # retrieve current and new password [ -f /etc/yunohost/mysql ] \ - && curr_pwd=$(sudo cat /etc/yunohost/mysql) \ - || curr_pwd="yunohost" + && curr_pwd=$(sudo cat /etc/yunohost/mysql) new_pwd=$(sudo cat "${backup_dir}/root_pwd" || sudo cat "${backup_dir}/mysql") +[ -z "$curr_pwd" ] && curr_pwd="yunohost" +[ -z "$new_pwd" ] && { + . /usr/share/yunohost/helpers.d/string + new_pwd=$(ynh_string_random 10) +} # attempt to change it sudo mysqladmin -s -u root -p"$curr_pwd" password "$new_pwd" || { diff --git a/debian/control b/debian/control index 3be8b917e..e9a0a3d47 100644 --- a/debian/control +++ b/debian/control @@ -26,6 +26,7 @@ Depends: ${python:Depends}, ${misc:Depends} , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , haveged + , archivemount Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper diff --git a/locales/en.json b/locales/en.json index 2ba2a48f6..e4f785002 100644 --- a/locales/en.json +++ b/locales/en.json @@ -54,29 +54,54 @@ "ask_main_domain": "Main domain", "ask_new_admin_password": "New administration password", "ask_password": "Password", + "backup_abstract_method": "This backup method hasn't yet been implemented", "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", + "backup_applying_method_tar": "Creating the backup tar archive...", + "backup_applying_method_copy": "Copying all files to backup...", + "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", + "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", - "backup_archive_hook_not_exec": "Hook '{hook:s}' not executed in this backup", + "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", "backup_archive_name_exists": "The backup's archive name already exists", "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", "backup_archive_open_failed": "Unable to open the backup archive", + "backup_archive_mount_failed": "Mounting the backup archive failed", + "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", + "backup_ask_for_copying_if_needed": "Your system don't support completely the quick method to organize files in the archive, do you want to organize its by copying {size:s}MB?", + "backup_borg_not_implemented": "Borg backup method is not yet implemented", + "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", + "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", "backup_created": "Backup created", "backup_creating_archive": "Creating the backup archive...", "backup_creation_failed": "Backup creation failed", + "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", + "backup_csv_addition_failed": "Unable to add files to backup into the CSV file", + "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", + "backup_custom_backup_error": "Custom backup method failure on 'backup' step", + "backup_custom_mount_error": "Custom backup method failure on 'mount' step", "backup_delete_error": "Unable to delete '{path:s}'", "backup_deleted": "The backup has been deleted", "backup_extracting_archive": "Extracting the backup archive...", "backup_hook_unknown": "Backup hook '{hook:s}' unknown", "backup_invalid_archive": "Invalid backup archive", + "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", "backup_nothings_done": "There is nothing to save", + "backup_method_tar_finished": "Backup tar archive created", + "backup_method_copy_finished": "Backup copy finished", + "backup_method_borg_finished": "Backup into borg finished", + "backup_method_custom_finished": "Custom backup method '{method:s}' finished", "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", + "backup_system_part_failed": "Unable to backup the '{part:s}' system part", + "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", + "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.", + "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", @@ -199,14 +224,20 @@ "restore_action_required": "You must specify something to restore", "restore_already_installed_app": "An app is already installed with the id '{app:s}'", "restore_app_failed": "Unable to restore the app '{app:s}'", + "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", "restore_complete": "Restore complete", "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", + "restore_extracting": "Extracting needed files from the archive...", "restore_failed": "Unable to restore the system", - "restore_hook_unavailable": "Restoration hook '{hook:s}' not available on your system", + "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", + "restore_mounting_archive": "Mounting archive into '{path:s}'", + "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", "restore_nothings_done": "Nothing has been restored", "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", + "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "service_add_failed": "Unable to add service '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 725bf2fda..03598ec48 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -31,6 +31,8 @@ import time import tarfile import shutil import subprocess +import csv +import tempfile from glob import glob from collections import OrderedDict @@ -39,367 +41,2073 @@ from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger from yunohost.app import ( - app_info, app_ssowatconf, _is_installed, _parse_app_instance_name + app_info, _is_installed, _parse_app_instance_name ) from yunohost.hook import ( - hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER + hook_list, hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER ) from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall +from yunohost.service import service_regen_conf BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH - +APP_MARGIN_SPACE_SIZE = 100 # In MB +CONF_MARGIN_SPACE_SIZE = 10 # IN MB +POSTINSTALL_ESTIMATE_SPACE_SIZE = 5 # In MB +MB_ALLOWED_TO_ORGANIZE = 10 logger = getActionLogger('yunohost.backup') -def backup_create(name=None, description=None, output_directory=None, - no_compress=False, ignore_hooks=False, hooks=[], - ignore_apps=False, apps=[]): +class BackupRestoreTargetsManager(object): + """ + BackupRestoreTargetsManager manage the targets + in BackupManager and RestoreManager + """ + + def __init__(self): + + self.targets = {} + self.results = { + "system": {}, + "apps": {} + } + + + def set_result(self, category, element, value): + """ + Change (or initialize) the current status/result of a given target. + + Args: + category -- The category of the target + + element -- The target for which to change the status/result + + value -- The new status/result, among "Unknown", "Success", + "Warning", "Error" and "Skipped" + """ + + levels = [ "Unknown", "Success", "Warning", "Error", "Skipped" ] + + assert value in levels + + if element not in self.results[category].keys(): + self.results[category][element] = value + else: + currentValue = self.results[category][element] + if (levels.index(currentValue) > levels.index(value)): + return + else: + self.results[category][element] = value + + + def set_wanted(self, category, + wanted_targets, available_targets, + error_if_wanted_target_is_unavailable): + """ + Define and validate targets to be backuped or to be restored (list of + system parts, apps..). The wanted targets are compared and filtered + with respect to the available targets. If a wanted targets is not + available, a call to "error_if_wanted_target_is_unavailable" is made. + + Args: + category -- The category (apps or system) for which to set the + targets ; + + wanted_targets -- List of targets which are wanted by the user. Can be + "None" or [], corresponding to "No targets" or "All + targets" ; + + available_targets -- List of targets which are really available ; + + error_if_wanted_target_is_unavailable + -- Callback for targets which are not available. + """ + + # If no targets wanted, set as empty list + if wanted_targets is None: + self.targets[category] = [] + + # If all targets wanted, use all available targets + elif wanted_targets == []: + self.targets[category] = available_targets + + # If the user manually specified which targets to backup, we need to + # validate that each target is actually available + else: + self.targets[category] = [part for part in wanted_targets + if part in available_targets] + + # Display an error for each target asked by the user but which is + # unknown + unavailable_targets = [part for part in wanted_targets + if part not in available_targets] + + for target in unavailable_targets: + self.set_result(category, target, "Skipped") + error_if_wanted_target_is_unavailable(target) + + # For target with no result yet (like 'Skipped'), set it as unknown + if self.targets[category] is not None: + for target in self.targets[category]: + self.set_result(category, target, "Unknown") + + return self.list(category, exclude=["Skipped"]) + + + def list(self, category, include=None, exclude=None): + """ + List targets in a given category. + + The list is filtered with a whitelist (include) or blacklist (exclude) + with respect to the current 'result' of the target. + """ + + assert (include and isinstance(include, list) and not exclude) \ + or (exclude and isinstance(exclude, list) and not include) + + if include: + return [target for target in self.targets[category] + if self.results[category][target] in include] + + if exclude: + return [target for target in self.targets[category] + if self.results[category][target] not in exclude] + + +class BackupManager(): + """ + This class collect files to backup in a list and apply one or several + backup method on it. + + The list contains dict with source and dest properties. The goal of this csv + is to list all directories and files which need to be backup in this + archive. The `source` property is the path of the source (dir or file). + The `dest` property is the path where it could be placed in the archive. + + The list is filled by app backup scripts and system/user backup hooks. + Files located in the work_dir are automatically added. + + With this list, "backup methods" are able to apply their backup strategy on + data listed in it. It's possible to tar each path (tar methods), to mount + each dir into the work_dir, to copy each files (copy method) or to call a + custom method (via a custom script). + + Note: some future backups methods (like borg) are not able to specify a + different place than the original path. That's why the ynh_restore_file + helpers use primarily the SOURCE_PATH as argument. + + Public properties: + info (getter) + work_dir (getter) # FIXME currently it's not a getter + is_tmp_work_dir (getter) + paths_to_backup (getter) # FIXME not a getter and list is not protected + name (getter) # FIXME currently it's not a getter + size (getter) # FIXME currently it's not a getter + + Public methods: + add(self, method) + set_system_targets(self, system_parts=[]) + set_apps_targets(self, apps=[]) + collect_files(self) + backup(self) + + Usage: + backup_manager = BackupManager(name="mybackup", description="bkp things") + + # Add backup method to apply + backup_manager.add(BackupMethod.create('copy','/mnt/local_fs')) + backup_manager.add(BackupMethod.create('tar','/mnt/remote_fs')) + + # Define targets to be backuped + backup_manager.set_system_targets(["data"]) + backup_manager.set_apps_targets(["wordpress"]) + + # Collect files to backup from targets + backup_manager.collect_files() + + # Apply backup methods + backup_manager.backup() + """ + + def __init__(self, name=None, description='', work_dir=None): + """ + BackupManager constructor + + Args: + name -- (string) The name of this backup (without spaces). If + None, the name will be generated (default: None) + + description -- (string) A description for this future backup archive + (default: '') + + work_dir -- (None|string) A path where prepare the archive. If None, + temporary work_dir will be created (default: None) + """ + self.description = description or '' + self.created_at = int(time.time()) + self.apps_return = {} + self.system_return = {} + self.methods = [] + self.paths_to_backup = [] + self.size_details = { + 'system': {}, + 'apps': {} + } + self.targets = BackupRestoreTargetsManager() + + + # Define backup name if needed + if not name: + name = self._define_backup_name() + self.name = name + + # Define working directory if needed and initialize it + self.work_dir = work_dir + if self.work_dir is None: + self.work_dir = os.path.join(BACKUP_PATH, 'tmp', name) + self._init_work_dir() + + ########################################################################### + # Misc helpers # + ########################################################################### + + @property + def info(self): + """(Getter) Dict containing info about the archive being created""" + return { + 'description': self.description, + 'created_at': self.created_at, + 'size': self.size, + 'size_details': self.size_details, + 'apps': self.apps_return, + 'system': self.system_return + } + + @property + def is_tmp_work_dir(self): + """(Getter) Return true if the working directory is temporary and should + be clean at the end of the backup""" + return self.work_dir == os.path.join(BACKUP_PATH, 'tmp', self.name) + + def __repr__(self): + return json.dumps(self.info) + + def _define_backup_name(self): + """Define backup name + + Return: + (string) A backup name created from current date 'YYMMDD-HHMMSS' + """ + # FIXME: case where this name already exist + return time.strftime('%Y%m%d-%H%M%S') + + def _init_work_dir(self): + """Initialize preparation directory + + Ensure the working directory exists and is empty + + exception: + backup_output_directory_not_empty -- (MoulinetteError) Raised if the + directory was given by the user and isn't empty + + (TODO) backup_cant_clean_tmp_working_directory -- (MoulinetteError) + Raised if the working directory isn't empty, is temporary and can't + be automaticcaly cleaned + + (TODO) backup_cant_create_working_directory -- (MoulinetteError) Raised + if iyunohost can't create the working directory + """ + + # FIXME replace isdir by exists ? manage better the case where the path + # exists + if not os.path.isdir(self.work_dir): + filesystem.mkdir(self.work_dir, 0750, parents=True, uid='admin') + elif self.is_tmp_work_dir: + logger.debug("temporary directory for backup '%s' already exists", + self.work_dir) + # FIXME May be we should clean the workdir here + raise MoulinetteError( + errno.EIO, m18n.n('backup_output_directory_not_empty')) + + ########################################################################### + # Backup target management # + ########################################################################### + + def set_system_targets(self, system_parts=[]): + """ + Define and validate targetted apps to be backuped + + Args: + system_parts -- (list) list of system parts which should be backuped. + If empty list, all system will be backuped. If None, + no system parts will be backuped. + """ + def unknown_error(part): + logger.error(m18n.n('backup_hook_unknown', hook=part)) + + self.targets.set_wanted("system", + system_parts, hook_list('backup')["hooks"], + unknown_error) + + + def set_apps_targets(self, apps=[]): + """ + Define and validate targetted apps to be backuped + + Args: + apps -- (list) list of apps which should be backuped. If given an empty + list, all apps will be backuped. If given None, no apps will be + backuped. + """ + def unknown_error(app): + logger.error(m18n.n('unbackup_app', app=app)) + + target_list = self.targets.set_wanted("apps", apps, + os.listdir('/etc/yunohost/apps'), + unknown_error) + + # Additionnaly, we need to check that each targetted app has a + # backup and restore scripts + + for app in target_list: + app_script_folder = "/etc/yunohost/apps/%s/scripts" % app + backup_script_path = os.path.join(app_script_folder, "backup") + restore_script_path = os.path.join(app_script_folder, "restore") + + if not os.path.isfile(backup_script_path): + logger.warning(m18n.n('backup_with_no_backup_script_for_app', app=app)) + self.targets.set_result("apps", app, "Skipped") + + elif not os.path.isfile(restore_script_path): + logger.warning(m18n.n('backup_with_no_restore_script_for_app', app=app)) + self.targets.set_result("apps", app, "Warning") + + + ########################################################################### + # Management of files to backup / "The CSV" # + ########################################################################### + + def _import_to_list_to_backup(self, tmp_csv): + """ + Commit collected path from system hooks or app scripts + + Args: + tmp_csv -- (string) Path to a temporary csv file with source and + destinations column to add to the list of paths to backup + """ + _call_for_each_path(self, BackupManager._add_to_list_to_backup, tmp_csv) + + + def _add_to_list_to_backup(self, source, dest=None): + """ + Mark file or directory to backup + + This method add source/dest couple to the "paths_to_backup" list. + + Args: + source -- (string) Source path to backup + + dest -- (string) Destination path in the archive. If it ends by a + slash the basename of the source path will be added. If None, + the source path will be used, so source files will be set up + at the same place and with same name than on the system. + (default: None) + + Usage: + self._add_to_list_to_backup('/var/www/wordpress', 'sources') + # => "wordpress" dir will be move and rename as "sources" + + self._add_to_list_to_backup('/var/www/wordpress', 'sources/') + # => "wordpress" dir will be put inside "sources/" and won't be renamed + + """ + if dest is None: + dest = source + source = os.path.join(self.work_dir, source) + if dest.endswith("/"): + dest = os.path.join(dest, os.path.basename(source)) + self.paths_to_backup.append({'source': source, 'dest': dest}) + + + def _write_csv(self): + """ + Write the backup list into a CSV + + The goal of this csv is to list all directories and files which need to + be backup in this archive. For the moment, this CSV contains 2 columns. + The first column `source` is the path of the source (dir or file). The + second `dest` is the path where it could be placed in the archive. + + This CSV is filled by app backup scripts and system/user hooks. + Files in the work_dir are automatically added. + + With this CSV, "backup methods" are able to apply their backup strategy + on data listed in it. It's possible to tar each path (tar methods), to + mount each dir into the work_dir, to copy each files (copy methods) or + a custom method (via a custom script). + + Note: some future backups methods (like borg) are not able to specify a + different place than the original path. That's why the ynh_restore_file + helpers use primarily the SOURCE_PATH as argument. + + Error: + backup_csv_creation_failed -- Raised if the CSV couldn't be created + backup_csv_addition_failed -- Raised if we can't write in the CSV + """ + self.csv_path = os.path.join(self.work_dir, 'backup.csv') + try: + self.csv_file = open(self.csv_path, 'a') + self.fieldnames = ['source', 'dest'] + self.csv = csv.DictWriter(self.csv_file, fieldnames=self.fieldnames, + quoting=csv.QUOTE_ALL) + except (IOError, OSError, csv.Error): + logger.error(m18n.n('backup_csv_creation_failed')) + + for row in self.paths_to_backup: + try: + self.csv.writerow(row) + except csv.Error: + logger.error(m18n.n('backup_csv_addition_failed')) + self.csv_file.close() + + + ########################################################################### + # File collection from system parts and apps # + ########################################################################### + + def collect_files(self): + """ + Collect all files to backup, write its into a CSV and create a + info.json file + + Files to backup are listed by system parts backup hooks and by backup + app scripts that have been defined with the set_targets() method. + + Some files or directories inside the working directory are added by + default: + + info.json -- info about the archive + backup.csv -- a list of paths to backup + apps/ -- some apps generate here temporary files to backup (like + database dump) + conf/ -- system configuration backup scripts could generate here + temporary files to backup + data/ -- system data backup scripts could generate here temporary + files to backup + hooks/ -- restore scripts associated to system backup scripts are + copied here + + Exceptions: + "backup_nothings_done" -- (MoulinetteError) This exception is raised if + nothing has been listed. + """ + + self._collect_system_files() + self._collect_apps_files() + + # Check if something has been saved ('success' or 'warning') + successfull_apps = self.targets.list("apps", include=["Success", "Warning"]) + successfull_system = self.targets.list("system", include=["Success", "Warning"]) + + if not successfull_apps and not successfull_system: + filesystem.rm(self.work_dir, True, True) + raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) + + + # Add unlisted files from backup tmp dir + self._add_to_list_to_backup('backup.csv') + self._add_to_list_to_backup('info.json') + if len(self.apps_return) > 0: + self._add_to_list_to_backup('apps') + if os.path.isdir(os.path.join(self.work_dir, 'conf')): + self._add_to_list_to_backup('conf') + if os.path.isdir(os.path.join(self.work_dir, 'data')): + self._add_to_list_to_backup('data') + + # Write CSV file + self._write_csv() + + # Calculate total size + self._compute_backup_size() + + # Create backup info file + with open("%s/info.json" % self.work_dir, 'w') as f: + f.write(json.dumps(self.info)) + + + def _get_env_var(self, app=None): + """ + Define environment variables for apps or system backup scripts. + + Args: + app -- (string|None) The instance name of the app we want the variable + environment. If you want a variable environment for a system backup + script keep None. (default: None) + + Return: + (Dictionnary) The environment variables to apply to the script + """ + env_var = {} + + _, tmp_csv = tempfile.mkstemp(prefix='backupcsv_') + env_var['YNH_BACKUP_DIR'] = self.work_dir + env_var['YNH_BACKUP_CSV'] = tmp_csv + + if app is not None: + app_id, app_instance_nb = _parse_app_instance_name(app) + env_var["YNH_APP_ID"] = app_id + env_var["YNH_APP_INSTANCE_NAME"] = app + env_var["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + tmp_app_dir = os.path.join('apps/', app) + tmp_app_bkp_dir = os.path.join(self.work_dir, tmp_app_dir, 'backup') + env_var["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir + + return env_var + + + def _collect_system_files(self): + """ + List file to backup for each selected system part + + This corresponds to scripts in data/hooks/backup/ (system hooks) and + to those in /etc/yunohost/hooks.d/backup/ (user hooks) + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths toi + backup + """ + + system_targets = self.targets.list("system", exclude=["Skipped"]) + + # If nothing to backup, return immediately + if system_targets == []: + return + + logger.info(m18n.n('backup_running_hooks')) + + # Prepare environnement + env_dict = self._get_env_var() + + # Actual call to backup scripts/hooks + + ret = hook_callback('backup', + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir) + + if ret["succeed"] != []: + self.system_return = ret["succeed"] + + # Add files from targets (which they put in the CSV) to the list of + # files to backup + self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) + + # Save restoration hooks for each part that suceeded (and which have + # a restore hook available) + + restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore") + if not os.path.exists(restore_hooks_dir): + filesystem.mkdir(restore_hooks_dir, mode=0750, + parents=True, uid='admin') + + restore_hooks = hook_list("restore")["hooks"] + + for part in ret['succeed'].keys(): + if part in restore_hooks: + part_restore_hooks = hook_info("restore", part)["hooks"] + for hook in part_restore_hooks: + self._add_to_list_to_backup(hook["path"], "hooks/restore/") + self.targets.set_result("system", part, "Success") + else: + logger.warning(m18n.n('restore_hook_unavailable', hook=part)) + self.targets.set_result("system", part, "Warning") + + for part in ret['failed'].keys(): + logger.error(m18n.n('backup_system_part_failed', part=part)) + self.targets.set_result("system", part, "Error") + + + def _collect_apps_files(self): + """ Prepare backup for each selected apps """ + + apps_targets = self.targets.list("apps", exclude=["Skipped"]) + + for app_instance_name in apps_targets: + self._collect_app_files(app_instance_name) + + + def _collect_app_files(self, app): + """ + List files to backup for the app into the paths_to_backup dict. + + If the app backup script fails, paths from this app already listed for + backup aren't added to the general list and will be ignored + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths toi + backup + YNH_APP_BACKUP_DIR -- The directory where the script should put + temporary files to backup like database dump, + files in this directory don't need to be added to + the temporary CSV. + YNH_APP_ID -- The app id (eg wordpress) + YNH_APP_INSTANCE_NAME -- The app instance name (eg wordpress__3) + YNH_APP_INSTANCE_NUMBER -- The app instance number (eg 3) + + + Args: + app -- (string) an app instance name (already installed) to backup + + Exceptions: + backup_app_failed -- Raised at the end if the app backup script + execution failed + """ + app_setting_path = os.path.join('/etc/yunohost/apps/', app) + + # Prepare environment + env_dict = self._get_env_var(app) + tmp_app_bkp_dir = env_dict["YNH_APP_BACKUP_DIR"] + settings_dir = os.path.join(self.work_dir, 'apps', app, 'settings') + + logger.info(m18n.n('backup_running_app_script', app=app)) + try: + # Prepare backup directory for the app + filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') + + # Copy the app settings to be able to call _common.sh + shutil.copytree(app_setting_path, settings_dir) + + # Copy app backup script in a temporary folder and execute it + _, tmp_script = tempfile.mkstemp(prefix='backup_') + app_script = os.path.join(app_setting_path, 'scripts/backup') + subprocess.call(['install', '-Dm555', app_script, tmp_script]) + + hook_exec(tmp_script, args=[tmp_app_bkp_dir, app], + raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") + + self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) + except: + abs_tmp_app_dir = os.path.join(self.work_dir, 'apps/', app) + shutil.rmtree(abs_tmp_app_dir, ignore_errors=True) + logger.exception(m18n.n('backup_app_failed', app=app)) + self.targets.set_result("apps", app, "Error") + else: + # Add app info + i = app_info(app) + self.apps_return[app] = { + 'version': i['version'], + 'name': i['name'], + 'description': i['description'], + } + self.targets.set_result("apps", app, "Success") + + # Remove tmp files in all situations + finally: + filesystem.rm(tmp_script, force=True) + filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) + + + ########################################################################### + # Actual backup archive creation / method management # + ########################################################################### + + def add(self, method): + """ + Add a backup method that will be applied after the files collection step + + Args: + method -- (BackupMethod) A backup method. Currently, you can use those: + TarBackupMethod + CopyBackupMethod + CustomBackupMethod + """ + self.methods.append(method) + + + def backup(self): + """Apply backup methods""" + + for method in self.methods: + logger.info(m18n.n('backup_applying_method_' + method.method_name)) + method.mount_and_backup(self) + logger.info(m18n.n('backup_method_' + method.method_name + '_finished')) + + + def _compute_backup_size(self): + """ + Compute backup global size and details size for each apps and system + parts + + Update self.size and self.size_details + + Note: currently, these sizes are the size in this archive, not really + the size of needed to restore the archive. To know the size needed to + restore we should consider apt/npm/pip dependencies space and database + dump restore operations. + + Return: + (int) The global size of the archive in bytes + """ + # FIXME Database dump will be loaded, so dump should use almost the + # double of their space + # FIXME Some archive will set up dependencies, those are not in this + # size info + self.size = 0 + for system_key in self.system_return: + self.size_details['system'][system_key] = 0 + for app_key in self.apps_return: + self.size_details['apps'][app_key] = 0 + + for row in self.paths_to_backup: + if row['dest'] != "info.json": + size = disk_usage(row['source']) + + # Add size to apps details + splitted_dest = row['dest'].split('/') + category = splitted_dest[0] + if category == 'apps': + for app_key in self.apps_return: + if row['dest'].startswith('apps/'+app_key): + self.size_details['apps'][app_key] += size + break + # OR Add size to the correct system element + elif category == 'data' or category == 'conf': + for system_key in self.system_return: + if row['dest'].startswith(system_key.replace('_', '/')): + self.size_details['system'][system_key] += size + break + + self.size += size + + return self.size + + +class RestoreManager(): + """ + RestoreManager allow to restore a past backup archive + + Currently it's a tar.gz file, but it could be another kind of archive + + Public properties: + info (getter)i # FIXME + work_dir (getter) # FIXME currently it's not a getter + name (getter) # FIXME currently it's not a getter + success (getter) + result (getter) # FIXME + + Public methods: + set_targets(self, system_parts=[], apps=[]) + restore(self) + + Usage: + restore_manager = RestoreManager(name) + + restore_manager.set_targets(None, ['wordpress__3']) + + restore_manager.restore() + + if restore_manager.success: + logger.success(m18n.n('restore_complete')) + + return restore_manager.result + """ + + def __init__(self, name, repo=None, method='tar'): + """ + RestoreManager constructor + + Args: + name -- (string) Archive name + repo -- (string|None) Repository where is this archive, it could be a + path (default: /home/yunohost.backup/archives) + method -- (string) Method name to use to mount the archive + """ + # Retrieve and open the archive + # FIXME this way to get the info is not compatible with copy or custom + # backup methods + self.info = backup_info(name, with_details=True) + self.archive_path = self.info['path'] + self.name = name + self.method = BackupMethod.create(method) + self.targets = BackupRestoreTargetsManager() + + ########################################################################### + # Misc helpers # + ########################################################################### + + @property + def success(self): + + successful_apps = self.targets.list("apps", include=["Success", "Warning"]) + successful_system = self.targets.list("system", include=["Success", "Warning"]) + + return len(successful_apps) != 0 \ + or len(successful_system) != 0 + + + def _read_info_files(self): + """ + Read the info file from inside an archive + + Exceptions: + backup_invalid_archive -- Raised if we can't read the info + """ + # Retrieve backup info + info_file = os.path.join(self.work_dir, "info.json") + try: + with open(info_file, 'r') as f: + self.info = json.load(f) + + # Historically, "system" was "hooks" + if "system" not in self.info.keys(): + self.info["system"] = self.info["hooks"] + except IOError: + logger.debug("unable to load '%s'", info_file, exc_info=1) + raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) + else: + logger.debug("restoring from backup '%s' created on %s", self.name, + time.ctime(self.info['created_at'])) + + + def _postinstall_if_needed(self): + """ + Post install yunohost if needed + + Exceptions: + backup_invalid_archive -- Raised if the current_host isn't in the + archive + """ + # Check if YunoHost is installed + if not os.path.isfile('/etc/yunohost/installed'): + # Retrieve the domain from the backup + try: + with open("%s/conf/ynh/current_host" % self.work_dir, 'r') as f: + domain = f.readline().rstrip() + except IOError: + logger.debug("unable to retrieve current_host from the backup", + exc_info=1) + # FIXME include the current_host by default ? + raise MoulinetteError(errno.EIO, + m18n.n('backup_invalid_archive')) + + logger.debug("executing the post-install...") + tools_postinstall(domain, 'yunohost', True) + + + def clean(self): + """ + End a restore operations by cleaning the working directory and + regenerate ssowat conf (if some apps were restored) + """ + + successfull_apps = self.targets.list("apps", include=["Success", "Warning"]) + + if successfull_apps != []: + # Quickfix: the old app_ssowatconf(auth) instruction failed due to + # ldap restore hooks + os.system('sudo yunohost app ssowatconf') + + if os.path.ismount(self.work_dir): + ret = subprocess.call(["umount", self.work_dir]) + if ret != 0: + logger.warning(m18n.n('restore_cleaning_failed')) + filesystem.rm(self.work_dir, True, True) + + + ########################################################################### + # Restore target manangement # + ########################################################################### + + def set_system_targets(self, system_parts=[]): + """ + Define system parts that will be restored + + Args: + system_parts -- (list) list of system parts which should be restored. + If an empty list if given, restore all system part in + the archive. If None is given, no system will be restored. + """ + + def unknown_error(part): + logger.error(m18n.n("backup_archive_system_part_not_available", + part=part)) + + target_list = self.targets.set_wanted("system", + system_parts, + self.info['system'].keys(), + unknown_error) + + # Now we need to check that the restore hook is actually available for + # all targets we want to restore + + # These are the hooks on the current installation + available_restore_system_hooks = hook_list("restore")["hooks"] + + for system_part in target_list: + # By default, we'll use the restore hooks on the current install + # if available + + # FIXME: so if the restore hook exist we use the new one and not + # the one from backup. So hook should not break compatibility.. + + if system_part in available_restore_system_hooks: + continue + + # Otherwise, attempt to find it (or them?) in the archive + hook_paths = '{:s}/hooks/restore/*-{:s}'.format(self.work_dir, system_part) + hook_paths = glob(hook_paths) + + # If we didn't find it, we ain't gonna be able to restore it + if len(hook_paths) == 0: + logger.exception(m18n.n('restore_hook_unavailable', part=system_part)) + self.targets.set_result("system", system_part, "Skipped") + continue + + # Otherwise, add it from the archive to the system + # FIXME: Refactor hook_add and use it instead + custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, 'restore') + filesystem.mkdir(custom_restore_hook_folder, 755, True) + for hook_path in hook_paths: + logger.debug("Adding restoration script '%s' to the system " + "from the backup archive '%s'", hook_path, + self.archive_path) + shutil.copy(hook_path, custom_restore_hook_folder) + + def set_apps_targets(self, apps=[]): + """ + Define and validate targetted apps to be restored + + Args: + apps -- (list) list of apps which should be restored. If [] is given, + all apps in the archive will be restored. If None is given, + no apps will be restored. + """ + + def unknown_error(app): + logger.error(m18n.n('backup_archive_app_not_found', + app=app)) + + self.targets.set_wanted("apps", + apps, + self.info['apps'].keys(), + unknown_error) + + + ########################################################################### + # Archive mounting # + ########################################################################### + + def mount(self): + """ + Mount the archive. We avoid copy to be able to restore on system without + too many space. + + Use the mount method from the BackupMethod instance and read info about + this archive + + Exceptions: + restore_removing_tmp_dir_failed -- Raised if it's not possible to remove + the working directory + """ + + self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.name) + + if os.path.ismount(self.work_dir): + logger.debug("An already mounting point '%s' already exists", + self.work_dir) + ret = subprocess.call(['umount', self.work_dir]) + if ret == 0: + subprocess.call(['rmdir', self.work_dir]) + logger.debug("Unmount dir: {}".format(self.work_dir)) + else: + raise MoulinetteError(errno.EIO, + m18n.n('restore_removing_tmp_dir_failed')) + elif os.path.isdir(self.work_dir): + logger.debug("temporary restore directory '%s' already exists", + self.work_dir) + ret = subprocess.call(['rm', '-Rf', self.work_dir]) + if ret == 0: + logger.debug("Delete dir: {}".format(self.work_dir)) + else: + raise MoulinetteError(errno.EIO, + m18n.n('restore_removing_tmp_dir_failed')) + + filesystem.mkdir(self.work_dir, parents=True) + + self.method.mount(self) + + self._read_info_files() + + ########################################################################### + # Space computation / checks # + ########################################################################### + + def _compute_needed_space(self): + """ + Compute needed space to be able to restore + + Return: + size -- (int) needed space to backup in bytes + margin -- (int) margin to be sure the backup don't fail by missing space + in bytes + """ + system = self.targets.list("system", exclude=["Skipped"]) + apps = self.targets.list("apps", exclude=["Skipped"]) + restore_all_system = (system == self.info['system'].keys()) + restore_all_apps = (apps == self.info['apps'].keys()) + + # If complete restore operations (or legacy archive) + margin = CONF_MARGIN_SPACE_SIZE * 1024 * 1024 + if (restore_all_system and restore_all_apps) or 'size_details' not in self.info: + size = self.info['size'] + if 'size_details' not in self.info or \ + self.info['size_details']['apps'] != {}: + margin = APP_MARGIN_SPACE_SIZE * 1024 * 1024 + # Partial restore don't need all backup size + else: + size = 0 + if system is not None: + for system_element in system: + size += self.info['size_details']['system'][system_element] + + # TODO how to know the dependencies size ? + if apps is not None: + for app in apps: + size += self.info['size_details']['apps'][app] + margin = APP_MARGIN_SPACE_SIZE * 1024 * 1024 + + if not os.path.isfile('/etc/yunohost/installed'): + size += POSTINSTALL_ESTIMATE_SPACE_SIZE * 1024 * 1024 + return (size, margin) + + def assert_enough_free_space(self): + """ + Check available disk space + + Exceptions: + restore_may_be_not_enough_disk_space -- Raised if there isn't enough + space to cover the security margin space + restore_not_enough_disk_space -- Raised if there isn't enough space + """ + + free_space = free_space_in_directory(BACKUP_PATH) + + (needed_space, margin) = self._compute_needed_space() + if free_space >= needed_space + margin: + return True + elif free_space > needed_space: + # TODO Add --force options to avoid the error raising + raise MoulinetteError(errno.EIO, + m18n.n('restore_may_be_not_enough_disk_space', + free_space=free_space, + needed_space=needed_space, + margin=margin)) + else: + raise MoulinetteError(errno.EIO, + m18n.n('restore_not_enough_disk_space', + free_space=free_space, + needed_space=needed_space, + margin=margin)) + + ########################################################################### + # "Actual restore" (reverse step of the backup collect part) # + ########################################################################### + + def restore(self): + """ + Restore the archive + + Restore system parts and apps after mounting the archive, checking free + space and postinstall if needed + """ + + try: + self._postinstall_if_needed() + self._restore_system() + self._restore_apps() + finally: + self.clean() + + + def _restore_system(self): + """ Restore user and system parts """ + + system_targets = self.targets.list("system", exclude=["Skipped"]) + + # If nothing to restore, return immediately + if system_targets == []: + return + + logger.info(m18n.n('restore_running_hooks')) + + env_dict = self._get_env_var() + ret = hook_callback('restore', + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir) + + for part in ret['succeed'].keys(): + self.targets.set_result("system", part, "Success") + + for part in ret['failed'].keys(): + logger.error(m18n.n('restore_system_part_failed', part=part)) + self.targets.set_result("system", part, "Error") + + service_regen_conf() + + + def _restore_apps(self): + """Restore all apps targeted""" + + apps_targets = self.targets.list("apps", exclude=["Skipped"]) + + for app in apps_targets: + self._restore_app(app) + + + def _restore_app(self, app_instance_name): + """ + Restore an app + + Environment variables: + YNH_BACKUP_DIR -- The backup working directory (in + "/home/yunohost.backup/tmp/BACKUPNAME" or could be + defined by the user) + YNH_BACKUP_CSV -- A temporary CSV where the script whould list paths to + backup + YNH_APP_BACKUP_DIR -- The directory where the script should put + temporary files to backup like database dump, + files in this directory don't need to be added to + the temporary CSV. + YNH_APP_ID -- The app id (eg wordpress) + YNH_APP_INSTANCE_NAME -- The app instance name (eg wordpress__3) + YNH_APP_INSTANCE_NUMBER -- The app instance number (eg 3) + + Args: + app_instance_name -- (string) The app name to restore (no app with this + name should be already install) + + Exceptions: + restore_already_installed_app -- Raised if an app with this app instance + name already exists + restore_app_failed -- Raised if the restore bash script failed + """ + def copytree(src, dst, symlinks=False, ignore=None): + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + shutil.copytree(s, d, symlinks, ignore) + else: + shutil.copy2(s, d) + + # Check if the app is not already installed + if _is_installed(app_instance_name): + logger.error(m18n.n('restore_already_installed_app', + app=app_instance_name)) + self.targets.set_result("apps", app_instance_name, "Error") + return + + app_dir_in_archive = os.path.join(self.work_dir, 'apps', app_instance_name) + app_backup_in_archive = os.path.join(app_dir_in_archive, 'backup') + app_settings_in_archive = os.path.join(app_dir_in_archive, 'settings') + app_scripts_in_archive = os.path.join(app_settings_in_archive, 'scripts') + + # Check if the app has a restore script + app_restore_script_in_archive = os.path.join(app_scripts_in_archive, + 'restore') + if not os.path.isfile(app_restore_script_in_archive): + logger.warning(m18n.n('unrestore_app', app=app_instance_name)) + self.targets.set_result("apps", app_instance_name, "Warning") + return + + logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) + try: + # Restore app settings + app_settings_new_path = os.path.join('/etc/yunohost/apps/', + app_instance_name) + app_scripts_new_path = os.path.join(app_settings_new_path, 'scripts') + shutil.copytree(app_settings_in_archive, app_settings_new_path) + filesystem.chmod(app_settings_new_path, 0400, 0400, True) + filesystem.chown(app_scripts_new_path, 'admin', None, True) + + # Copy the app scripts to a writable temporary folder + # FIXME : use 'install -Dm555' or something similar to what's done + # in the backup method ? + tmp_folder_for_app_restore = tempfile.mkdtemp(prefix='restore') + copytree(app_scripts_in_archive, tmp_folder_for_app_restore) + filesystem.chmod(tmp_folder_for_app_restore, 0550, 0550, True) + filesystem.chown(tmp_folder_for_app_restore, 'admin', None, True) + restore_script = os.path.join(tmp_folder_for_app_restore, 'restore') + + # Prepare env. var. to pass to script + env_dict = self._get_env_var(app_instance_name) + + # Execute app restore script + hook_exec(restore_script, + args=[app_backup_in_archive, app_instance_name], + chdir=app_backup_in_archive, + raise_on_error=True, + env=env_dict, + user="root") + except: + logger.exception(m18n.n('restore_app_failed', + app=app_instance_name)) + self.targets.set_result("apps", app_instance_name, "Error") + + remove_script = os.path.join(app_scripts_in_archive, 'remove') + + # Setup environment for remove script + app_id, app_instance_nb = _parse_app_instance_name(app_instance_name) + env_dict_remove = {} + env_dict_remove["YNH_APP_ID"] = app_id + env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name + env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + + # Execute remove script + # TODO: call app_remove instead + if hook_exec(remove_script, args=[app_instance_name], + env=env_dict_remove, user="root") != 0: + logger.warning(m18n.n('app_not_properly_removed', + app=app_instance_name)) + + # Cleaning app directory + shutil.rmtree(app_settings_new_path, ignore_errors=True) + + + # TODO Cleaning app hooks + else: + self.targets.set_result("apps", app_instance_name, "Success") + finally: + # Cleaning temporary scripts directory + shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) + + + def _get_env_var(self, app=None): + """ Define environment variable for hooks call """ + env_var = {} + env_var['YNH_BACKUP_DIR'] = self.work_dir + env_var['YNH_BACKUP_CSV'] = os.path.join(self.work_dir, "backup.csv") + + if app is not None: + app_dir_in_archive = os.path.join(self.work_dir, 'apps', app) + app_backup_in_archive = os.path.join(app_dir_in_archive, 'backup') + + # Parse app instance name and id + app_id, app_instance_nb = _parse_app_instance_name(app) + + env_var["YNH_APP_ID"] = app_id + env_var["YNH_APP_INSTANCE_NAME"] = app + env_var["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + env_var["YNH_APP_BACKUP_DIR"] = app_backup_in_archive + + return env_var + +############################################################################### +# Backup methods # +############################################################################### + +class BackupMethod(object): + """ + BackupMethod is an abstract class that represents a way to backup and + restore a list of files. + + Daughters of this class can be used by a BackupManager or RestoreManager + instance. Some methods are meant to be used by BackupManager and others + by RestoreManager. + + BackupMethod has a factory method "create" to initialize instances. + + Currently, there are 3 BackupMethods implemented: + + CopyBackupMethod + ---------------- + This method corresponds to a raw (uncompressed) copy of files to a location, + and (could?) reverse the copy when restoring. + + TarBackupMethod + --------------- + This method compresses all files to backup in a .tar.gz archive. When + restoring, it tries to mount the archive using archivemount/fuse instead + of untaring the archive. Some systems don't support fuse (for these, + it automatically falls back to untaring the required parts). + + CustomBackupMethod + ------------------ + This one use a custom bash scrip/hook "backup_method" to do the + backup/restore operations. A user can add his own hook inside + /etc/yunohost/hooks.d/backup_method/ + + Public properties: + method_name + + Public methods: + mount_and_backup(self, backup_manager) + mount(self, restore_manager) + create(cls, method, **kwargs) + + Usage: + method = BackupMethod.create("tar") + method.mount_and_backup(backup_manager) + #or + method = BackupMethod.create("copy") + method.mount(restore_manager) + """ + def __init__(self, repo = None): + """ + BackupMethod constructors + + Note it is an abstract class. You should use the "create" class method + to create instance. + + Args: + repo -- (string|None) A string that represent the repo where put or + get the backup. It could be a path, and in future a + BackupRepository object. If None, the default repo is used : + /home/yunohost.backup/archives/ + """ + self.repo = ARCHIVES_PATH if repo is None else repo + + @property + def method_name(self): + """Return the string name of a BackupMethod (eg "tar" or "copy")""" + raise MoulinetteError(errno.EINVAL, m18n.n('backup_abstract_method')) + + @property + def name(self): + """Return the backup name""" + return self.manager.name + + @property + def work_dir(self): + """ + Return the working directory + + For a BackupManager, it is the directory where we prepare the files to + backup + + For a RestoreManager, it is the directory where we mount the archive + before restoring + """ + return self.manager.work_dir + + def need_mount(self): + """ + Return True if this backup method need to organize path to backup by + binding its in the working directory before to backup its. + + Indeed, some methods like tar or copy method don't need to organize + files before to add it inside the archive, but others like borgbackup + are not able to organize directly the files. In this case we have the + choice to organize in the working directory before to put in the archive + or to organize after mounting the archive before the restoring + operation. + + The default behaviour is to return False. To change it override the + method. + + Note it's not a property because some overrided methods could do long + treatment to get this info + """ + return False + + def mount_and_backup(self, backup_manager): + """ + Run the backup on files listed by the BackupManager instance + + This method shouldn't be overrided, prefer overriding self.backup() and + self.clean() + + Args: + backup_manager -- (BackupManager) A backup manager instance that has + already done the files collection step. + """ + self.manager = backup_manager + if self.need_mount(): + self._organize_files() + + try: + self.backup() + finally: + self.clean() + + def mount(self, restore_manager): + """ + Mount the archive from RestoreManager instance in the working directory + + This method should be extended. + + Args: + restore_manager -- (RestoreManager) A restore manager instance + contains an archive to restore. + """ + self.manager = restore_manager + + def clean(self): + """ + Umount sub directories of working dirextories and delete it if temporary + + Exceptions: + backup_cleaning_failed -- Raise if we were not able to unmount sub + directories of the working directories + """ + if self.need_mount(): + if self._recursive_umount(self.work_dir) > 0: + raise MoulinetteError(errno.EINVAL, + m18n.n('backup_cleaning_failed')) + + if self.manager.is_tmp_work_dir: + filesystem.rm(self.work_dir, True, True) + + def _recursive_umount(directory): + """ + Recursively umount sub directories of a directory + + Args: + directory -- a directory path + """ + mount_lines = subprocess.check_output("mount").split("\n") + + points_to_umount = [ line.split(" ")[2] + for line in mount_lines + if len(line) >= 3 + and line.split(" ")[2].startswith(directory) ] + ret = 0 + for point in reversed(points_to_umount): + ret = subprocess.call(["umount", point]) + if ret != 0: + ret = 1 + logger.warning(m18n.n('backup_cleaning_failed', point)) + continue + + return ret + + def _check_is_enough_free_space(self): + """ + Check free space in repository or output directory before to backup + + Exceptions: + not_enough_disk_space -- Raise if there isn't enough space. + """ + # TODO How to do with distant repo or with deduplicated backup ? + backup_size = self.manager.size + + free_space = free_space_in_directory(self.repo) + + if free_space < backup_size: + logger.debug('Not enough space at %s (free: %s / needed: %d)', + self.repo, free_space, backup_size) + raise MoulinetteError(errno.EIO, m18n.n( + 'not_enough_disk_space', path=self.repo)) + + def _organize_files(self): + """ + Mount all csv src in their related path + + The goal is to organize the files app by app and hook by hook, before + custom backup method or before the restore operation (in the case of an + unorganize archive). + + The usage of binding could be strange for a user because the du -sb + command will return that the working directory is big. + + Exceptions: + backup_unable_to_organize_files + """ + paths_needed_to_be_copied = [] + for path in self.manager.paths_to_backup: + src = path['src'] + + if self.manager is RestoreManager: + # TODO Support to run this before a restore (and not only before + # backup). To do that RestoreManager.unorganized_work_dir should + # be implemented + src = os.path.join(self.unorganized_work_dir, src) + + dest = os.path.join(self.work_dir, path['dest']) + dest_dir = os.path.dirname(dest) + + # Be sure the parent dir of destination exists + filesystem.mkdir(dest_dir, parent=True) + + # Try to bind files + if os.path.isdir(src): + filesystem.mkdir(dest, parent=True) + ret = subprocess.call(["mount", "-r", "--rbind", src, dest]) + if ret == 0: + continue + else: + logger.warning(m18n.n("bind_mouting_disable")) + subprocess.call(["mountpoint", "-q", dest, + "&&", "umount", "-R", dest]) + elif os.path.isfile(src) or os.path.islink(src): + # Create a hardlink if src and dest are on the filesystem + if os.stat(src).st_dev == os.stat(dest_dir).st_dev: + os.link(src, dest) + continue + + # Add to the list to copy + paths_needed_to_be_copied.append(path) + + if len(paths_needed_to_be_copied) == 0: + return + + # Manage the case where we are not able to use mount bind abilities + # It could be just for some small files on different filesystems or due + # to mounting error + + # Compute size to copy + size = sum(disk_usage(path['src']) for path in paths_needed_to_be_copied) + size /= (1024 * 1024) # Convert bytes to megabytes + + # Ask confirmation for copying + if size > MB_ALLOWED_TO_ORGANIZE: + try: + i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', + answers='y/N', size=size)) + except NotImplemented: + logger.error(m18n.n('backup_unable_to_organize_files')) + else: + if i != 'y' and i != 'Y': + logger.error(m18n.n('backup_unable_to_organize_files')) + + # Copy unbinded path + logger.info(m18n.n('backup_copying_to_organize_the_archive', size=size)) + for path in paths_needed_to_be_copied: + if os.path.isdir(src): + shutil.copytree(src, dest, symlinks=True) + else: + shutil.copy(src, dest) + + @classmethod + def create(cls, method, *args): + """ + Factory method to create instance of BackupMethod + + Args: + method -- (string) The method name of an existing BackupMethod. If the + name is unknown the CustomBackupMethod will be tried + + ... -- Specific args for the method, could be the repo target by the + method + + Return a BackupMethod instance + """ + if not isinstance(method, basestring): + methods = [] + for m in method: + methods.append(BackupMethod.create(m, *args)) + return methods + + bm_class = { + 'copy': CopyBackupMethod, + 'tar': TarBackupMethod, + 'borg': BorgBackupMethod + } + if method in ["copy", "tar", "borg"]: + return bm_class[method](*args) + else: + return CustomBackupMethod(*args) + + +class CopyBackupMethod(BackupMethod): + """ + This class just do an uncompress copy of each file in a location, and + could be the inverse for restoring + """ + def __init__(self, repo = None): + super(CopyBackupMethod, self).__init__(repo) + + @property + def method_name(self): + return 'copy' + + def backup(self): + """ Copy prepared files into a the repo """ + # Check free space in output + self._check_is_enough_free_space() + + for path in self.manager.paths_to_backup: + source = path['source'] + dest = os.path.join(self.repo, path['dest']) + if source == dest: + logger.debug("Files already copyed") + return + + dest_parent = os.path.dirname(dest) + if not os.path.exists(dest_parent): + filesystem.mkdir(dest_parent, 0750, True, uid='admin') + + if os.path.isdir(source): + shutil.copytree(source, dest) + else: + shutil.copy(source, dest) + + def mount(self): + """ + Mount the uncompress backup in readonly mode to the working directory + + Exceptions: + backup_no_uncompress_archive_dir -- Raised if the repo doesn't exists + backup_cant_mount_uncompress_archive -- Raised if the binding failed + """ + # FIXME: This code is untested because there is no way to run it from + # the ynh cli + super(CopyBackupMethod, self).mount() + + if not os.path.isdir(self.repo): + raise MoulinetteError(errno.EIO, + m18n.n('backup_no_uncompress_archive_dir')) + + filesystem.mkdir(self.work_dir, parent=True) + ret = subprocess.call(["mount", "-r", "--rbind", self.repo, + self.work_dir]) + if ret == 0: + return + else: + logger.warning(m18n.n("bind_mouting_disable")) + subprocess.call(["mountpoint", "-q", dest, + "&&", "umount", "-R", dest]) + raise MoulinetteError(errno.EIO, + m18n.n('backup_cant_mount_uncompress_archive')) + + +class TarBackupMethod(BackupMethod): + """ + This class compress all files to backup in archive. To restore it try to + mount the archive with archivemount (fuse). Some system don't support fuse. + """ + + def __init__(self, repo=None): + super(TarBackupMethod, self).__init__(repo) + + + @property + def method_name(self): + return 'tar' + + + @property + def _archive_file(self): + """Return the compress archive path""" + return os.path.join(self.repo, self.name + '.tar.gz') + + + def backup(self): + """ + Compress prepared files + + It adds the info.json in /home/yunohost.backup/archives and if the + compress archive isn't located here, add a symlink to the archive to. + + Exceptions: + backup_archive_open_failed -- Raised if we can't open the archive + backup_creation_failed -- Raised if we can't write in the + compress archive + """ + + if not os.path.exists(self.repo): + filesystem.mkdir(self.repo, 0750, parents=True, uid='admin') + + # Check free space in output + self._check_is_enough_free_space() + + # Open archive file for writing + try: + tar = tarfile.open(self._archive_file, "w:gz") + except: + logger.debug("unable to open '%s' for writing", + self._archive_file, exc_info=1) + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_open_failed')) + + # Add files to the archive + try: + for path in self.manager.paths_to_backup: + # Add the "source" into the archive and transform the path into + # "dest" + tar.add(path['source'], arcname=path['dest']) + tar.close() + except IOError: + logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) + raise MoulinetteError(errno.EIO, + m18n.n('backup_creation_failed')) + + # Move info file + shutil.copy(os.path.join(self.work_dir, 'info.json'), + os.path.join(ARCHIVES_PATH, self.name + '.info.json')) + + # If backuped to a non-default location, keep a symlink of the archive + # to that location + link = os.path.join(ARCHIVES_PATH, self.name + '.tar.gz') + if not os.path.isfile(link): + os.symlink(self._archive_file, link) + + + def mount(self, restore_manager): + """ + Mount the archive. We avoid copy to be able to restore on system without + too many space. + + Exceptions: + backup_archive_open_failed -- Raised if the archive can't be open + backup_archive_mount_failed -- Raised if the system don't support + archivemount + """ + super(TarBackupMethod, self).mount(restore_manager) + + # Check the archive can be open + try: + tar = tarfile.open(self._archive_file, "r:gz") + except: + logger.debug("cannot open backup archive '%s'", + self._archive_file, exc_info=1) + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_open_failed')) + tar.close() + + # Mount the tarball + ret = subprocess.call(['archivemount', '-o', 'readonly', + self._archive_file, self.work_dir]) + if ret != 0: + logger.warning(m18n.n('backup_archive_mount_failed')) + + logger.info(m18n.n("restore_extracting")) + tar = tarfile.open(self._archive_file, "r:gz") + tar.extract('info.json', path=self.work_dir) + + try: + tar.extract('backup.csv', path=self.work_dir) + except KeyError: + # Old backup archive have no backup.csv file + pass + + # Extract system parts backup + conf_extracted = False + + system_targets = self.manager.targets.list("system", exclude=["Skipped"]) + apps_targets = self.manager.targets.list("apps", exclude=["Skipped"]) + + for system_part in system_targets: + # Caution: conf_ynh_currenthost helpers put its files in + # conf/ynh + if system_part.startswith("conf_"): + if conf_extracted: + continue + system_part = "conf/" + conf_extracted = True + else: + system_part = system_part.replace("_", "/") + "/" + subdir_and_files = [ + tarinfo for tarinfo in tar.getmembers() + if tarinfo.name.startswith(system_part) + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + subdir_and_files = [ + tarinfo for tarinfo in tar.getmembers() + if tarinfo.name.startswith("hooks/restore/") + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + + + # Extract apps backup + for app in apps_targets: + subdir_and_files = [ + tarinfo for tarinfo in tar.getmembers() + if tarinfo.name.startswith("apps/" + app) + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) + + + +class BorgBackupMethod(BackupMethod): + + @property + def method_name(self): + return 'borg' + + + def backup(self): + """ Backup prepared files with borg """ + super(CopyBackupMethod, self).backup() + + # TODO run borg create command + raise MoulinetteError( + errno.EIO, m18n.n('backup_borg_not_implemented')) + + + def mount(self, mnt_path): + raise MoulinetteError( + errno.EIO, m18n.n('backup_borg_not_implemented')) + + +class CustomBackupMethod(BackupMethod): + """ + This class use a bash script/hook "backup_method" to do the + backup/restore operations. A user can add his own hook inside + /etc/yunohost/hooks.d/backup_method/ + """ + def __init__(self, repo = None, **kwargs): + super(CustomBackupMethod, self).__init__(repo) + self.args = kwargs + self._need_mount = None + + + @property + def method_name(self): + return 'borg' + + + def need_mount(self): + """Call the backup_method hook to know if we need to organize files + + Exceptions: + backup_custom_need_mount_error -- Raised if the hook failed + """ + ret = hook_callback('backup_method', method, + args=self._get_args('need_mount')) + if ret['succeed']: + return True + else: + raise MoulinetteError(errno.EIO, + m18n.n('backup_custom_need_mount_error')) + + + def backup(self): + """ + Launch a custom script to backup + + Exceptions: + backup_custom_backup_error -- Raised if the custom script failed + """ + + ret = hook_callback('backup_method', method, + args=self._get_args('backup')) + if ret['failed']: + raise MoulinetteError(errno.EIO, + m18n.n('backup_custom_backup_error')) + + def mount(self, restore_manager): + """ + Launch a custom script to mount the custom archive + + Exceptions: + backup_custom_mount_error -- Raised if the custom script failed + """ + super(CustomBackupMethod, self).mount(restore_manager) + ret = hook_callback('backup_method', method, + args=self._get_args('mount')) + if ret['failed']: + raise MoulinetteError(errno.EIO, + m18n.n('backup_custom_mount_error')) + + + def _get_args(self, action): + """Return the arguments to give to the custom script""" + return [action, self.work_dir, self.name, self.repo, self.manager.size, + self.manager.description] + + +############################################################################### +# "Front-end" # +############################################################################### + +def backup_create(name=None, description=None, methods=[], + output_directory=None, no_compress=False, + ignore_system=False, system=[], + ignore_apps=False, apps=[], + ignore_hooks=False, hooks=[]): """ Create a backup local archive Keyword arguments: name -- Name of the backup archive description -- Short description of the backup + method -- Method of backup to use output_directory -- Output directory for the backup no_compress -- Do not create an archive file - hooks -- List of backup hooks names to execute - ignore_hooks -- Do not execute backup hooks + system -- List of system elements to backup + ignore_system -- Ignore system elements apps -- List of application names to backup ignore_apps -- Do not backup apps + hooks -- (Deprecated) Renamed to "system" + ignore_hooks -- (Deprecated) Renamed to "ignore_system" """ + # TODO: Add a 'clean' argument to clean output directory - tmp_dir = None - env_var = {} - # Validate what to backup - if ignore_hooks and ignore_apps: - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_action_required')) + ########################################################################### + # Validate / parse arguments # + ########################################################################### - # Validate and define backup name - timestamp = int(time.time()) - if not name: - name = time.strftime('%Y%m%d-%H%M%S') - if name in backup_list()['archives']: - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_archive_name_exists')) + # Historical, deprecated options + if ignore_hooks != False: + logger.warning("--ignore-hooks is deprecated and will be removed in the" + "future. Please use --ignore-system instead.") + ignore_system = ignore_hooks - # Validate additional arguments - if no_compress and not output_directory: + if hooks != [] and hooks is not None: + logger.warning("--hooks is deprecated and will be removed in the" + "future. Please use --system instead.") + system = hooks + + # Validate that there's something to backup + if ignore_system and ignore_apps: raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_required')) + m18n.n('backup_action_required')) + + # Validate there is no archive with the same name + if name and name in backup_list()['archives']: + raise MoulinetteError(errno.EINVAL, + m18n.n('backup_archive_name_exists')) + + # Validate output_directory option if output_directory: output_directory = os.path.abspath(output_directory) # Check for forbidden folders if output_directory.startswith(ARCHIVES_PATH) or \ - re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', - output_directory): + re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', + output_directory): raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_forbidden')) + m18n.n('backup_output_directory_forbidden')) - # Create the output directory - if not os.path.isdir(output_directory): - logger.debug("creating output directory '%s'", output_directory) - os.makedirs(output_directory, 0750) # Check that output directory is empty - elif no_compress and os.listdir(output_directory): + if os.path.isdir(output_directory) and no_compress and \ + os.listdir(output_directory): raise MoulinetteError(errno.EIO, - m18n.n('backup_output_directory_not_empty')) + m18n.n('backup_output_directory_not_empty')) + elif no_compress: + raise MoulinetteError(errno.EINVAL, + m18n.n('backup_output_directory_required')) - # Do not compress, so set temporary directory to output one and - # disable bind mounting to prevent data loss in case of a rm - # See: https://dev.yunohost.org/issues/298 + # Define methods (retro-compat) + if methods == []: if no_compress: - logger.debug('bind mounting will be disabled') - tmp_dir = output_directory - env_var['CAN_BIND'] = 0 + methods = ['copy'] + else: + methods = ['tar'] # In future, borg will be the default actions + + if ignore_system: + system = None + elif system is None: + system = [] + + if ignore_apps: + apps = None + elif apps is None: + apps = [] + + ########################################################################### + # Intialize # + ########################################################################### + + # Create yunohost archives directory if it does not exists + _create_archive_dir() + + # Prepare files to backup + if no_compress: + backup_manager = BackupManager(name, description, + work_dir=output_directory) else: - output_directory = ARCHIVES_PATH + backup_manager = BackupManager(name, description) - # Create archives directory if it does not exists - if not os.path.isdir(ARCHIVES_PATH): - os.mkdir(ARCHIVES_PATH, 0750) + # Add backup methods + if output_directory: + methods = BackupMethod.create(methods, output_directory) + else: + methods = BackupMethod.create(methods) - def _clean_tmp_dir(retcode=0): - ret = hook_callback('post_backup_create', args=[tmp_dir, retcode]) - if not ret['failed']: - filesystem.rm(tmp_dir, True, True) - return True - else: - logger.warning(m18n.n('backup_cleaning_failed')) - return False + for method in methods: + backup_manager.add(method) - # Create temporary directory - if not tmp_dir: - tmp_dir = "%s/tmp/%s" % (BACKUP_PATH, name) - if os.path.isdir(tmp_dir): - logger.debug("temporary directory for backup '%s' already exists", - tmp_dir) - if not _clean_tmp_dir(): - raise MoulinetteError( - errno.EIO, m18n.n('backup_output_directory_not_empty')) - filesystem.mkdir(tmp_dir, 0750, parents=True, uid='admin') + # Add backup targets (system and apps) + backup_manager.set_system_targets(system) + backup_manager.set_apps_targets(apps) - # Initialize backup info - info = { - 'description': description or '', - 'created_at': timestamp, - 'apps': {}, - 'hooks': {}, - } + ########################################################################### + # Collect files and put them in the archive # + ########################################################################### - # Run system hooks - if not ignore_hooks: - # Check hooks availibility - hooks_filtered = set() - if hooks: - for hook in hooks: - try: - hook_info('backup', hook) - except: - logger.error(m18n.n('backup_hook_unknown', hook=hook)) - else: - hooks_filtered.add(hook) + # Collect files to be backup (by calling app backup script / system hooks) + backup_manager.collect_files() - if not hooks or hooks_filtered: - logger.info(m18n.n('backup_running_hooks')) - ret = hook_callback('backup', hooks_filtered, args=[tmp_dir], - env=env_var) - if ret['succeed']: - info['hooks'] = ret['succeed'] - - # Save relevant restoration hooks - tmp_hooks_dir = tmp_dir + '/hooks/restore' - filesystem.mkdir(tmp_hooks_dir, 0750, True, uid='admin') - for h in ret['succeed'].keys(): - try: - i = hook_info('restore', h) - except: - logger.warning(m18n.n('restore_hook_unavailable', - hook=h), exc_info=1) - else: - for f in i['hooks']: - shutil.copy(f['path'], tmp_hooks_dir) - - # Backup apps - if not ignore_apps: - # Filter applications to backup - apps_list = set(os.listdir('/etc/yunohost/apps')) - apps_filtered = set() - if apps: - for a in apps: - if a not in apps_list: - logger.warning(m18n.n('unbackup_app', app=a)) - else: - apps_filtered.add(a) - else: - apps_filtered = apps_list - - # Run apps backup scripts - tmp_script = '/tmp/backup_' + str(timestamp) - for app_instance_name in apps_filtered: - app_setting_path = '/etc/yunohost/apps/' + app_instance_name - - # Check if the app has a backup and restore script - app_script = app_setting_path + '/scripts/backup' - app_restore_script = app_setting_path + '/scripts/restore' - if not os.path.isfile(app_script): - logger.warning(m18n.n('unbackup_app', app=app_instance_name)) - continue - elif not os.path.isfile(app_restore_script): - logger.warning(m18n.n('unrestore_app', app=app_instance_name)) - - tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) - tmp_app_bkp_dir = tmp_app_dir + '/backup' - logger.info(m18n.n('backup_running_app_script', app=app_instance_name)) - try: - # Prepare backup directory for the app - filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') - shutil.copytree(app_setting_path, tmp_app_dir + '/settings') - - # Copy app backup script in a temporary folder and execute it - subprocess.call(['install', '-Dm555', app_script, tmp_script]) - - # Prepare env. var. to pass to script - app_id, app_instance_nb = _parse_app_instance_name( - app_instance_name) - env_dict = env_var.copy() - env_dict["YNH_APP_ID"] = app_id - env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir - - hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_instance_name], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") - except: - logger.exception(m18n.n('backup_app_failed', app=app_instance_name)) - # Cleaning app backup directory - shutil.rmtree(tmp_app_dir, ignore_errors=True) - else: - # Add app info - i = app_info(app_instance_name) - info['apps'][app_instance_name] = { - 'version': i['version'], - 'name': i['name'], - 'description': i['description'], - } - finally: - filesystem.rm(tmp_script, force=True) - - # Check if something has been saved - if not info['hooks'] and not info['apps']: - _clean_tmp_dir(1) - raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) - - # Calculate total size - backup_size = int(subprocess.check_output( - ['du', '-sb', tmp_dir]).split()[0].decode('utf-8')) - info['size'] = backup_size - - # Create backup info file - with open("%s/info.json" % tmp_dir, 'w') as f: - f.write(json.dumps(info)) - - # Create the archive - if not no_compress: - logger.info(m18n.n('backup_creating_archive')) - - # Check free space in output directory at first - avail_output = subprocess.check_output( - ['df', '--block-size=1', '--output=avail', tmp_dir]).split() - if len(avail_output) < 2 or int(avail_output[1]) < backup_size: - logger.debug('not enough space at %s (free: %s / needed: %d)', - output_directory, avail_output[1], backup_size) - _clean_tmp_dir(3) - raise MoulinetteError(errno.EIO, m18n.n( - 'not_enough_disk_space', path=output_directory)) - - # Open archive file for writing - archive_file = "%s/%s.tar.gz" % (output_directory, name) - try: - tar = tarfile.open(archive_file, "w:gz") - except: - logger.debug("unable to open '%s' for writing", - archive_file, exc_info=1) - _clean_tmp_dir(2) - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_open_failed')) - - # Add files to the archive - try: - tar.add(tmp_dir, arcname='') - tar.close() - except IOError as e: - logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) - _clean_tmp_dir(3) - raise MoulinetteError(errno.EIO, - m18n.n('backup_creation_failed')) - - # FIXME : it looks weird that the "move info file" is not enabled if - # user activated "no_compress" ... or does it really means - # "dont_keep_track_of_this_backup_in_history" ? - - # Move info file - shutil.move(tmp_dir + '/info.json', - '{:s}/{:s}.info.json'.format(ARCHIVES_PATH, name)) - - # If backuped to a non-default location, keep a symlink of the archive - # to that location - if output_directory != ARCHIVES_PATH: - link = "%s/%s.tar.gz" % (ARCHIVES_PATH, name) - os.symlink(archive_file, link) - - # Clean temporary directory - if tmp_dir != output_directory: - _clean_tmp_dir() + # Apply backup methods on prepared files + backup_manager.backup() logger.success(m18n.n('backup_created')) - # Return backup info - info['name'] = name - return {'archive': info} + return { + 'name': backup_manager.name, + 'size': backup_manager.size, + 'results': backup_manager.targets.results + } -def backup_restore(auth, name, hooks=[], ignore_hooks=False, - apps=[], ignore_apps=False, force=False): +def backup_restore(auth, name, + system=[], ignore_system=False, + apps=[], ignore_apps=False, + hooks=[], ignore_hooks=False, + force=False): """ Restore from a local backup archive Keyword argument: name -- Name of the local backup archive - hooks -- List of restoration hooks names to execute - ignore_hooks -- Do not execute backup hooks + force -- Force restauration on an already installed system + system -- List of system parts to restore + ignore_system -- Do not restore any system parts apps -- List of application names to restore ignore_apps -- Do not restore apps - force -- Force restauration on an already installed system + hooks -- (Deprecated) Renamed to "system" + ignore_hooks -- (Deprecated) Renamed to "ignore_system" """ + + ########################################################################### + # Validate / parse arguments # + ########################################################################### + + # Historical, deprecated options + if ignore_hooks != False: + logger.warning("--ignore-hooks is deprecated and will be removed in the" + "future. Please use --ignore-system instead.") + ignore_system = ignore_hooks + if hooks != []: + logger.warning("--hooks is deprecated and will be removed in the" + "future. Please use --system instead.") + system = hooks + # Validate what to restore - if ignore_hooks and ignore_apps: + if ignore_system and ignore_apps: raise MoulinetteError(errno.EINVAL, - m18n.n('restore_action_required')) + m18n.n('restore_action_required')) - # Retrieve and open the archive - info = backup_info(name) - archive_file = info['path'] - try: - tar = tarfile.open(archive_file, "r:gz") - except: - logger.debug("cannot open backup archive '%s'", - archive_file, exc_info=1) - raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) + if ignore_system: + system = None + elif system is None: + system = [] - # Check temporary directory - tmp_dir = "%s/tmp/%s" % (BACKUP_PATH, name) - if os.path.isdir(tmp_dir): - logger.debug("temporary directory for restoration '%s' already exists", - tmp_dir) - os.system('rm -rf %s' % tmp_dir) + if ignore_apps: + apps = None + elif apps is None: + apps = [] - # Check available disk space - statvfs = os.statvfs(BACKUP_PATH) - free_space = statvfs.f_frsize * statvfs.f_bavail - if free_space < info['size']: - logger.debug("%dB left but %dB is needed", free_space, info['size']) - raise MoulinetteError( - errno.EIO, m18n.n('not_enough_disk_space', path=BACKUP_PATH)) - - def _clean_tmp_dir(retcode=0): - ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) - if not ret['failed']: - filesystem.rm(tmp_dir, True, True) - else: - logger.warning(m18n.n('restore_cleaning_failed')) - - # Extract the tarball - logger.info(m18n.n('backup_extracting_archive')) - tar.extractall(tmp_dir) - tar.close() - - # Retrieve backup info - info_file = "%s/info.json" % tmp_dir - try: - with open(info_file, 'r') as f: - info = json.load(f) - except IOError: - logger.debug("unable to load '%s'", info_file, exc_info=1) - raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) - else: - logger.debug("restoring from backup '%s' created on %s", name, - time.ctime(info['created_at'])) - - # Initialize restauration summary result - result = { - 'apps': [], - 'hooks': {}, - } + # TODO don't ask this question when restoring apps only and certain system + # parts # Check if YunoHost is installed - if os.path.isfile('/etc/yunohost/installed'): + if os.path.isfile('/etc/yunohost/installed') and not ignore_system: logger.warning(m18n.n('yunohost_already_installed')) if not force: try: @@ -412,154 +2120,37 @@ def backup_restore(auth, name, hooks=[], ignore_hooks=False, if i == 'y' or i == 'Y': force = True if not force: - _clean_tmp_dir() raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) - else: - # Retrieve the domain from the backup - try: - with open("%s/conf/ynh/current_host" % tmp_dir, 'r') as f: - domain = f.readline().rstrip() - except IOError: - logger.debug("unable to retrieve current_host from the backup", - exc_info=1) - raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) - logger.debug("executing the post-install...") - tools_postinstall(domain, 'yunohost', True) + # TODO Partial app restore could not work if ldap is not restored before + # TODO repair mysql if broken and it's a complete restore - # Run system hooks - if not ignore_hooks: - # Filter hooks to execute - hooks_list = set(info['hooks'].keys()) - _is_hook_in_backup = lambda h: True - if hooks: - def _is_hook_in_backup(h): - if h in hooks_list: - return True - logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) - return False - else: - hooks = hooks_list + ########################################################################### + # Initialize # + ########################################################################### - # Check hooks availibility - hooks_filtered = set() - for h in hooks: - if not _is_hook_in_backup(h): - continue - try: - hook_info('restore', h) - except: - tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format(tmp_dir, h)) - if not tmp_hooks: - logger.exception(m18n.n('restore_hook_unavailable', hook=h)) - continue - # Add restoration hook from the backup to the system - # FIXME: Refactor hook_add and use it instead - restore_hook_folder = CUSTOM_HOOK_FOLDER + 'restore' - filesystem.mkdir(restore_hook_folder, 755, True) - for f in tmp_hooks: - logger.debug("adding restoration hook '%s' to the system " - "from the backup archive '%s'", f, archive_file) - shutil.copy(f, restore_hook_folder) - hooks_filtered.add(h) + restore_manager = RestoreManager(name) - if hooks_filtered: - logger.info(m18n.n('restore_running_hooks')) - ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) - result['hooks'] = ret['succeed'] + restore_manager.set_system_targets(system) + restore_manager.set_apps_targets(apps) - # Add apps restore hook - if not ignore_apps: - # Filter applications to restore - apps_list = set(info['apps'].keys()) - apps_filtered = set() - if apps: - for a in apps: - if a not in apps_list: - logger.error(m18n.n('backup_archive_app_not_found', app=a)) - else: - apps_filtered.add(a) - else: - apps_filtered = apps_list + restore_manager.assert_enough_free_space() - for app_instance_name in apps_filtered: - tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_instance_name) - tmp_app_bkp_dir = tmp_app_dir + '/backup' + ########################################################################### + # Mount the archive then call the restore for each system part / app # + ########################################################################### - # Parse app instance name and id - # TODO: Use app_id to check if app is installed? - app_id, app_instance_nb = _parse_app_instance_name(app_instance_name) + restore_manager.mount() + restore_manager.restore() - # Check if the app is not already installed - if _is_installed(app_instance_name): - logger.error(m18n.n('restore_already_installed_app', - app=app_instance_name)) - continue - - # Check if the app has a restore script - app_script = tmp_app_dir + '/settings/scripts/restore' - if not os.path.isfile(app_script): - logger.warning(m18n.n('unrestore_app', app=app_instance_name)) - continue - - tmp_settings_dir = tmp_app_dir + '/settings' - app_setting_path = '/etc/yunohost/apps/' + app_instance_name - logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) - try: - # Copy app settings and set permissions - # TODO: Copy app hooks too - shutil.copytree(tmp_app_dir + '/settings', app_setting_path) - filesystem.chmod(app_setting_path, 0555, 0444, True) - filesystem.chmod(app_setting_path + '/settings.yml', 0400) - - # Set correct right to the temporary settings folder - filesystem.chmod(tmp_settings_dir, 0550, 0550, True) - filesystem.chown(tmp_settings_dir, 'admin', None, True) - - # Prepare env. var. to pass to script - env_dict = {} - env_dict["YNH_APP_ID"] = app_id - env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - env_dict["YNH_APP_BACKUP_DIR"] = tmp_app_bkp_dir - - # Execute app restore script - hook_exec(app_script, args=[tmp_app_bkp_dir, app_instance_name], - raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict, user="root") - except: - logger.exception(m18n.n('restore_app_failed', app=app_instance_name)) - - app_script = tmp_app_dir + '/settings/scripts/remove' - - # Setup environment for remove script - env_dict_remove = {} - env_dict_remove["YNH_APP_ID"] = app_id - env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name - env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - - # Execute remove script - # TODO: call app_remove instead - if hook_exec(app_script, args=[app_instance_name], - env=env_dict_remove, user="root") != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) - - # Cleaning app directory - shutil.rmtree(app_setting_path, ignore_errors=True) - else: - result['apps'].append(app_instance_name) # Check if something has been restored - if not result['hooks'] and not result['apps']: - _clean_tmp_dir(1) + if restore_manager.success: + logger.success(m18n.n('restore_complete')) + else: raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) - if result['apps']: - app_ssowatconf(auth) - _clean_tmp_dir() - logger.success(m18n.n('restore_complete')) - - return result + return restore_manager.targets.results def backup_list(with_info=False, human_readable=False): @@ -612,7 +2203,7 @@ def backup_info(name, with_details=False, human_readable=False): # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_name_unknown', name=name)) + m18n.n('backup_archive_name_unknown', name=name)) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -621,16 +2212,24 @@ def backup_info(name, with_details=False, human_readable=False): # Raise exception if link is broken (e.g. on unmounted external storage) if not os.path.exists(archive_file): raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_broken_link', path=archive_file)) + m18n.n('backup_archive_broken_link', + path=archive_file)) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) + if not os.path.exists(info_file): + tar = tarfile.open(archive_file, "r:gz") + info_dir = info_file + '.d' + tar.extract('info.json', path=info_dir) + tar.close() + shutil.move(os.path.join(info_dir, 'info.json'), info_file) + os.rmdir(info_dir) + try: with open(info_file) as f: # Retrieve backup info info = json.load(f) except: - # TODO: Attempt to extract backup info file from tarball logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) @@ -653,8 +2252,13 @@ def backup_info(name, with_details=False, human_readable=False): } if with_details: - for d in ['apps', 'hooks']: - result[d] = info[d] + system_key = "system" + # Historically 'system' was 'hooks' + if "hooks" in info.keys(): + system_key = "hooks" + + result["apps"] = info["apps"] + result["system"] = info[system_key] return result @@ -672,7 +2276,7 @@ def backup_delete(name): info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) for backup_file in [archive_file, info_file]: - if not os.path.isfile(backup_file): + if not os.path.isfile(backup_file) and not os.path.islink(backup_file): raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', name=backup_file)) try: @@ -685,3 +2289,36 @@ def backup_delete(name): hook_callback('post_backup_delete', args=[name]) logger.success(m18n.n('backup_deleted')) + +############################################################################### +# Misc helpers # +############################################################################### + +def _create_archive_dir(): + """ Create the YunoHost archives directory if doesn't exist """ + if not os.path.isdir(ARCHIVES_PATH): + os.mkdir(ARCHIVES_PATH, 0750) + + +def _call_for_each_path(self, callback, csv_path=None): + """ Call a callback for each path in csv """ + if csv_path is None: + csv_path = self.csv_path + with open(csv_path, "r") as backup_file: + backup_csv = csv.DictReader(backup_file, fieldnames=['source', 'dest']) + for row in backup_csv: + callback(self, row['source'], row['dest']) + + +def free_space_in_directory(dirpath): + stat = os.statvfs(dirpath) + return stat.f_frsize * stat.f_bavail + + +def disk_usage(path): + # We don't do this in python with os.stat because we don't want + # to follow symlinks + + du_output = subprocess.check_output(['du', '-sb', path]) + return int(du_output.split()[0].decode('utf-8')) + diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py new file mode 100644 index 000000000..245dee758 --- /dev/null +++ b/src/yunohost/tests/test_backuprestore.py @@ -0,0 +1,637 @@ +import pytest +import time +import requests +import os +import shutil +import subprocess +from mock import ANY + +from moulinette.core import init_authenticator +from yunohost.app import app_install, app_remove, app_ssowatconf +from yunohost.app import _is_installed +from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete +from yunohost.domain import _get_maindomain +from moulinette.core import MoulinetteError + +# Get main domain +maindomain = _get_maindomain() + +# Instantiate LDAP Authenticator +AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') +AUTH_PARAMETERS = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} +auth = None + +def setup_function(function): + + print "" + + global auth + auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) + + assert backup_test_dependencies_are_met() + + clean_tmp_backup_directory() + reset_ssowat_conf() + delete_all_backups() + uninstall_test_apps_if_needed() + + assert len(backup_list()["archives"]) == 0 + + markers = function.__dict__.keys() + + if "with_wordpress_archive_from_2p4" in markers: + add_archive_wordpress_from_2p4() + assert len(backup_list()["archives"]) == 1 + + if "with_backup_legacy_app_installed" in markers: + assert not app_is_installed("backup_legacy_app") + install_app("backup_legacy_app_ynh", "/yolo") + assert app_is_installed("backup_legacy_app") + + if "with_backup_recommended_app_installed" in markers: + assert not app_is_installed("backup_recommended_app") + install_app("backup_recommended_app_ynh", "/yolo", + "&helper_to_test=ynh_restore_file") + assert app_is_installed("backup_recommended_app") + + if "with_backup_recommended_app_installed_with_ynh_restore" in markers: + assert not app_is_installed("backup_recommended_app") + install_app("backup_recommended_app_ynh", "/yolo", + "&helper_to_test=ynh_restore") + assert app_is_installed("backup_recommended_app") + + if "with_system_archive_from_2p4" in markers: + add_archive_system_from_2p4() + assert len(backup_list()["archives"]) == 1 + + +def teardown_function(function): + + print "" + global auth + auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) + + assert tmp_backup_directory_is_empty() + + reset_ssowat_conf() + delete_all_backups() + uninstall_test_apps_if_needed() + + markers = function.__dict__.keys() + + if "clean_opt_dir" in markers: + shutil.rmtree("/opt/test_backup_output_directory") + + +############################################################################### +# Helpers # +############################################################################### + +def app_is_installed(app): + + # These are files we know should be installed by the app + app_files = [] + app_files.append("/etc/nginx/conf.d/%s.d/%s.conf" % (maindomain, app)) + app_files.append("/var/www/%s/index.html" % app) + app_files.append("/etc/importantfile") + + return _is_installed(app) and all(os.path.exists(f) for f in app_files) + + +def backup_test_dependencies_are_met(): + + # We need archivemount installed for the backup features to work + assert os.system("which archivemount >/dev/null") == 0 + + # Dummy test apps (or backup archives) + assert os.path.exists("./tests/apps/backup_wordpress_from_2p4") + assert os.path.exists("./tests/apps/backup_legacy_app_ynh") + assert os.path.exists("./tests/apps/backup_recommended_app_ynh") + + return True + +def tmp_backup_directory_is_empty(): + + if not os.path.exists("/home/yunohost.backup/tmp/"): + return True + else: + return len(os.listdir('/home/yunohost.backup/tmp/')) == 0 + +def clean_tmp_backup_directory(): + + if tmp_backup_directory_is_empty(): + return + + mount_lines = subprocess.check_output("mount").split("\n") + + points_to_umount = [ line.split(" ")[2] + for line in mount_lines + if len(line) >= 3 + and line.split(" ")[2].startswith("/home/yunohost.backup/tmp") ] + + for point in reversed(points_to_umount): + os.system("umount %s" % point) + + for f in os.listdir('/home/yunohost.backup/tmp/'): + shutil.rmtree("/home/yunohost.backup/tmp/%s" % f) + + shutil.rmtree("/home/yunohost.backup/tmp/") + +def reset_ssowat_conf(): + + # Make sure we have a ssowat + os.system("mkdir -p /etc/ssowat/") + app_ssowatconf(auth) + + +def delete_all_backups(): + + for archive in backup_list()["archives"]: + backup_delete(archive) + + +def uninstall_test_apps_if_needed(): + + if _is_installed("backup_legacy_app"): + app_remove(auth, "backup_legacy_app") + + if _is_installed("backup_recommended_app"): + app_remove(auth, "backup_recommended_app") + + if _is_installed("wordpress"): + app_remove(auth, "wordpress") + + +def install_app(app, path, additionnal_args=""): + + app_install(auth, "./tests/apps/%s" % app, + args="domain=%s&path=%s%s" % (maindomain, path, + additionnal_args)) + + +def add_archive_wordpress_from_2p4(): + + os.system("mkdir -p /home/yunohost.backup/archives") + + os.system("cp ./tests/apps/backup_wordpress_from_2p4/backup.info.json \ + /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json") + + os.system("cp ./tests/apps/backup_wordpress_from_2p4/backup.tar.gz \ + /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz") + + +def add_archive_system_from_2p4(): + + os.system("mkdir -p /home/yunohost.backup/archives") + + os.system("cp ./tests/apps/backup_system_from_2p4/backup.info.json \ + /home/yunohost.backup/archives/backup_system_from_2p4.info.json") + + os.system("cp ./tests/apps/backup_system_from_2p4/backup.tar.gz \ + /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz") + +############################################################################### +# System backup # +############################################################################### + +def test_backup_only_ldap(): + + # Create the backup + backup_create(ignore_system=False, ignore_apps=True, system=["conf_ldap"]) + + archives = backup_list()["archives"] + assert len(archives) == 1 + + archives_info = backup_info(archives[0], with_details=True) + assert archives_info["apps"] == {} + assert len(archives_info["system"].keys()) == 1 + assert "conf_ldap" in archives_info["system"].keys() + + +def test_backup_system_part_that_does_not_exists(mocker): + + mocker.spy(m18n, "n") + + # Create the backup + with pytest.raises(MoulinetteError): + backup_create(ignore_system=False, ignore_apps=True, system=["yolol"]) + + m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") + m18n.n.assert_any_call('backup_nothings_done') + +############################################################################### +# System backup and restore # +############################################################################### + +def test_backup_and_restore_all_sys(): + + # Create the backup + backup_create(ignore_system=False, ignore_apps=True) + + archives = backup_list()["archives"] + assert len(archives) == 1 + + archives_info = backup_info(archives[0], with_details=True) + assert archives_info["apps"] == {} + assert (len(archives_info["system"].keys()) == + len(os.listdir("/usr/share/yunohost/hooks/backup/"))) + + # Remove ssowat conf + assert os.path.exists("/etc/ssowat/conf.json") + os.system("rm -rf /etc/ssowat/") + assert not os.path.exists("/etc/ssowat/conf.json") + + # Restore the backup + backup_restore(auth, name=archives[0], force=True, + ignore_system=False, ignore_apps=True) + + # Check ssowat conf is back + assert os.path.exists("/etc/ssowat/conf.json") + + +def test_backup_and_restore_archivemount_failure(monkeypatch, mocker): + + # Create the backup + backup_create(ignore_system=False, ignore_apps=True) + + archives = backup_list()["archives"] + assert len(archives) == 1 + + archives_info = backup_info(archives[0], with_details=True) + assert archives_info["apps"] == {} + assert (len(archives_info["system"].keys()) == + len(os.listdir("/usr/share/yunohost/hooks/backup/"))) + + # Remove ssowat conf + assert os.path.exists("/etc/ssowat/conf.json") + os.system("rm -rf /etc/ssowat/") + assert not os.path.exists("/etc/ssowat/conf.json") + + def custom_subprocess_call(*args, **kwargs): + import subprocess as subprocess2 + if args[0] and args[0][0]=="archivemount": + monkeypatch.undo() + return 1 + return subprocess.call(*args, **kwargs) + + monkeypatch.setattr("subprocess.call", custom_subprocess_call) + mocker.spy(m18n, "n") + + # Restore the backup + backup_restore(auth, name=archives[0], force=True, + ignore_system=False, ignore_apps=True) + + # Check ssowat conf is back + assert os.path.exists("/etc/ssowat/conf.json") + + +############################################################################### +# System restore from 2.4 # +############################################################################### + +@pytest.mark.with_system_archive_from_2p4 +def test_restore_system_from_Ynh2p4(monkeypatch, mocker): + + # Backup current system + backup_create(ignore_system=False, ignore_apps=True) + archives = backup_list()["archives"] + assert len(archives) == 2 + + # Restore system archive from 2.4 + try: + backup_restore(auth, name=backup_list()["archives"][1], + ignore_system=False, + ignore_apps=True, + force=True) + finally: + # Restore system as it was + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=False, + ignore_apps=True, + force=True) + + +@pytest.mark.with_system_archive_from_2p4 +def test_restore_system_from_Ynh2p4_archivemount_failure(monkeypatch, mocker): + + # Backup current system + backup_create(ignore_system=False, ignore_apps=True) + archives = backup_list()["archives"] + assert len(archives) == 2 + + def custom_subprocess_call(*args, **kwargs): + import subprocess as subprocess2 + if args[0] and args[0][0]=="archivemount": + monkeypatch.undo() + return 1 + return subprocess.call(*args, **kwargs) + + monkeypatch.setattr("subprocess.call", custom_subprocess_call) + + try: + # Restore system from 2.4 + backup_restore(auth, name=backup_list()["archives"][1], + ignore_system=False, + ignore_apps=True, + force=True) + finally: + # Restore system as it was + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=False, + ignore_apps=True, + force=True) + + +############################################################################### +# App backup # +############################################################################### + +@pytest.mark.with_backup_recommended_app_installed +def test_backup_script_failure_handling(monkeypatch, mocker): + + def custom_hook_exec(name, *args, **kwargs): + + if os.path.basename(name).startswith("backup_"): + raise Exception + else: + return True + + # Create a backup of this app and simulate a crash (patching the backup + # call with monkeypatch). We also patch m18n to check later it's been called + # with the expected error message key + monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) + mocker.spy(m18n, "n") + + with pytest.raises(MoulinetteError): + backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + + m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app') + +@pytest.mark.with_backup_recommended_app_installed +def test_backup_not_enough_free_space(monkeypatch, mocker): + + def custom_disk_usage(path): + return 99999999999999999 + + def custom_free_space_in_directory(dirpath): + return 0 + + monkeypatch.setattr("yunohost.backup.disk_usage", custom_disk_usage) + monkeypatch.setattr("yunohost.backup.free_space_in_directory", + custom_free_space_in_directory) + + mocker.spy(m18n, "n") + + with pytest.raises(MoulinetteError): + backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + + m18n.n.assert_any_call('not_enough_disk_space', path=ANY) + + +def test_backup_app_not_installed(mocker): + + assert not _is_installed("wordpress") + + mocker.spy(m18n, "n") + + with pytest.raises(MoulinetteError): + backup_create(ignore_system=True, ignore_apps=False, apps=["wordpress"]) + + m18n.n.assert_any_call("unbackup_app", app="wordpress") + m18n.n.assert_any_call('backup_nothings_done') + + +@pytest.mark.with_backup_recommended_app_installed +def test_backup_app_with_no_backup_script(mocker): + + backup_script = "/etc/yunohost/apps/backup_recommended_app/scripts/backup" + os.system("rm %s" % backup_script) + assert not os.path.exists(backup_script) + + mocker.spy(m18n, "n") + + with pytest.raises(MoulinetteError): + backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + + m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app") + m18n.n.assert_any_call('backup_nothings_done') + + +@pytest.mark.with_backup_recommended_app_installed +def test_backup_app_with_no_restore_script(mocker): + + restore_script = "/etc/yunohost/apps/backup_recommended_app/scripts/restore" + os.system("rm %s" % restore_script) + assert not os.path.exists(restore_script) + + mocker.spy(m18n, "n") + + # Backuping an app with no restore script will only display a warning to the + # user... + + backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + + m18n.n.assert_any_call("backup_with_no_restore_script_for_app", app="backup_recommended_app") + + +@pytest.mark.clean_opt_dir +def test_backup_with_different_output_directory(): + + # Create the backup + backup_create(ignore_system=False, ignore_apps=True, system=["conf_ssh"], + output_directory="/opt/test_backup_output_directory", + name="backup") + + assert os.path.exists("/opt/test_backup_output_directory/backup.tar.gz") + + archives = backup_list()["archives"] + assert len(archives) == 1 + + archives_info = backup_info(archives[0], with_details=True) + assert archives_info["apps"] == {} + assert len(archives_info["system"].keys()) == 1 + assert "conf_ssh" in archives_info["system"].keys() + +@pytest.mark.clean_opt_dir +def test_backup_with_no_compress(): + # Create the backup + backup_create(ignore_system=False, ignore_apps=True, system=["conf_nginx"], + output_directory="/opt/test_backup_output_directory", + no_compress=True, + name="backup") + + assert os.path.exists("/opt/test_backup_output_directory/info.json") + + +############################################################################### +# App restore # +############################################################################### + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_wordpress_from_Ynh2p4(): + + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_script_failure_handling(monkeypatch, mocker): + + def custom_hook_exec(name, *args, **kwargs): + if os.path.basename(name).startswith("restore"): + monkeypatch.undo() + raise Exception + + monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) + mocker.spy(m18n, "n") + + assert not _is_installed("wordpress") + + with pytest.raises(MoulinetteError): + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + m18n.n.assert_any_call('restore_app_failed', app='wordpress') + m18n.n.assert_any_call('restore_nothings_done') + assert not _is_installed("wordpress") + + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_not_enough_free_space(monkeypatch, mocker): + + def custom_free_space_in_directory(dirpath): + return 0 + + monkeypatch.setattr("yunohost.backup.free_space_in_directory", + custom_free_space_in_directory) + mocker.spy(m18n, "n") + + assert not _is_installed("wordpress") + + with pytest.raises(MoulinetteError): + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + m18n.n.assert_any_call('restore_not_enough_disk_space', + free_space=0, + margin=ANY, + needed_space=ANY) + assert not _is_installed("wordpress") + + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_not_in_backup(mocker): + + assert not _is_installed("wordpress") + assert not _is_installed("yoloswag") + + mocker.spy(m18n, "n") + + with pytest.raises(MoulinetteError): + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["yoloswag"]) + + m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag") + assert not _is_installed("wordpress") + assert not _is_installed("yoloswag") + + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_archivemount_failure(monkeypatch, mocker): + + def custom_subprocess_call(*args, **kwargs): + import subprocess as subprocess2 + if args[0] and args[0][0]=="archivemount": + monkeypatch.undo() + return 1 + return subprocess.call(*args, **kwargs) + + monkeypatch.setattr("subprocess.call", custom_subprocess_call) + mocker.spy(m18n, "n") + + assert not _is_installed("wordpress") + + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + assert _is_installed("wordpress") + + +@pytest.mark.with_wordpress_archive_from_2p4 +def test_restore_app_already_installed(mocker): + + assert not _is_installed("wordpress") + + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + assert _is_installed("wordpress") + + mocker.spy(m18n, "n") + with pytest.raises(MoulinetteError): + backup_restore(auth, name=backup_list()["archives"][0], + ignore_system=True, + ignore_apps=False, + apps=["wordpress"]) + + m18n.n.assert_any_call('restore_already_installed_app', app="wordpress") + m18n.n.assert_any_call('restore_nothings_done') + + assert _is_installed("wordpress") + + +@pytest.mark.with_backup_legacy_app_installed +def test_backup_and_restore_legacy_app(): + + _test_backup_and_restore_app("backup_legacy_app") + + +@pytest.mark.with_backup_recommended_app_installed +def test_backup_and_restore_recommended_app(): + + _test_backup_and_restore_app("backup_recommended_app") + + +@pytest.mark.with_backup_recommended_app_installed_with_ynh_restore +def test_backup_and_restore_with_ynh_restore(): + + _test_backup_and_restore_app("backup_recommended_app") + + +def _test_backup_and_restore_app(app): + + # Create a backup of this app + backup_create(ignore_system=True, ignore_apps=False, apps=[app]) + + archives = backup_list()["archives"] + assert len(archives) == 1 + + archives_info = backup_info(archives[0], with_details=True) + assert archives_info["system"] == {} + assert len(archives_info["apps"].keys()) == 1 + assert app in archives_info["apps"].keys() + + # Uninstall the app + app_remove(auth, app) + assert not app_is_installed(app) + + # Restore the app + backup_restore(auth, name=archives[0], ignore_system=True, + ignore_apps=False, apps=[app]) + + assert app_is_installed(app) + + + From 8948694610cf8a18c8428397d26db54172737989 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Fri, 2 Jun 2017 14:55:06 +0200 Subject: [PATCH 0360/1066] Update from Weblate. (#318) * [i18n] Translated using Weblate (Spanish) Currently translated at 97.9% (281 of 287 strings) * [i18n] Translated using Weblate (German) Currently translated at 96.8% (278 of 287 strings) * [i18n] Translated using Weblate (French) Currently translated at 95.3% (287 of 301 strings) * [i18n] Translated using Weblate (French) Currently translated at 100.0% (301 of 301 strings) --- locales/de.json | 6 +++++- locales/es.json | 20 +++++++++++++++++--- locales/fr.json | 21 ++++++++++++++++++--- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/locales/de.json b/locales/de.json index 2dc585128..4af251e14 100644 --- a/locales/de.json +++ b/locales/de.json @@ -284,5 +284,9 @@ "appslist_migrating": "Migriere Anwendungsliste {appslist:s} ...", "appslist_could_not_migrate": "Konnte Anwendungsliste {appslist:s} nicht migrieren. Konnte URL nicht verarbeiten... Der alte Cron-Job wurde unter {bkp_file:s} beibehalten.", "appslist_corrupted_json": "Konnte die Anwendungslisten. Es scheint, dass {filename:s} beschädigt ist.", - "yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt." + "yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt.", + "app_already_installed_cant_change_url": "Diese Application ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", + "app_change_no_change_url_script": "Die Application {app_name:s} unterstützt das anpassen der URL noch nicht. Sie muss gegebenenfalls erweitert werden.", + "app_change_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun." } diff --git a/locales/es.json b/locales/es.json index 130951494..f1c125d02 100644 --- a/locales/es.json +++ b/locales/es.json @@ -267,12 +267,26 @@ "certmanager_self_ca_conf_file_not_found": "No se ha encontrado el archivo de configuración para la autoridad de autofirma (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "No se puede procesar el nombre de la autoridad de autofirma (file: {file:s} 1)", "domains_available": "Dominios disponibles:", - "backup_archive_broken_link": "Acceso a la copia de seguridad imposible (enlace roto {path:s})", - "certmanager_domain_not_resolved_locally": "Su servidor Yunohost no consigue resolver el dominio {domain:s}. Esto puede suceder si modificó su registro DNS. Si es el caso, espere unas horas que se propague la modificación. Si el problema persiste, considere añadir {domain:s} a /etc/hosts. (Si sabe usted lo que está haciendo, use --no-checks para deshabilitar estas verificaciones.)", + "backup_archive_broken_link": "Imposible acceder a la copia de seguridad (enlace roto {path:s})", + "certmanager_domain_not_resolved_locally": "Su servidor Yunohost no consigue resolver el dominio {domain:s}. Esto puede suceder si ha modificado su registro DNS. Si es el caso, espere unas horas hasta que se propague la modificación. Si el problema persiste, considere añadir {domain:s} a /etc/hosts. (Si sabe lo que está haciendo, use --no-checks para deshabilitar estas verificaciones.)", "certmanager_acme_not_configured_for_domain": "El certificado para el dominio {domain:s} no parece instalado correctamente. Ejecute primero cert-install para este dominio.", "certmanager_http_check_timeout": "Plazo expirado, el servidor no ha podido contactarse a si mismo a través de HTTP usando su dirección IP pública (dominio {domain:s} con ip {ip:s}). Puede ser debido a hairpinning o a una mala configuración del cortafuego/router al que está conectado su servidor.", "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde.", "appslist_retrieve_bad_format": "El archivo recuperado no es una lista de aplicaciones válida", "domain_hostname_failed": "Error al establecer nuevo nombre de host", - "yunohost_ca_creation_success": "Se ha creado la autoridad de certificación local." + "yunohost_ca_creation_success": "Se ha creado la autoridad de certificación local.", + "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. No se puede cambiar el URL únicamente mediante esta función. Compruebe si está disponible la opción 'app changeurl'.", + "app_change_no_change_url_script": "La aplicacion {app_name:s} aún no permite cambiar su URL, es posible que deba actualizarla.", + "app_change_url_failed_nginx_reload": "No se pudo recargar nginx. Compruebe la salida de 'nginx -t':\n{nginx_error:s}", + "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain: s} {path: s}'), no se realizarán cambios.", + "app_change_url_no_script": "Esta aplicación '{app_name:s}' aún no permite modificar su URL. Quizás debería actualizar la aplicación.", + "app_change_url_success": "El URL de la aplicación {app:s} ha sido cambiado correctamente a {domain:s} {path:s}", + "app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada.", + "app_already_up_to_date": "La aplicación {app:s} ya está actualizada", + "appslist_name_already_tracked": "Ya existe una lista de aplicaciones registrada con nombre {name:s}.", + "appslist_url_already_tracked": "Ya existe una lista de aplicaciones registrada con el URL {url:s}.", + "appslist_migrating": "Migrando la lista de aplicaciones {applist:s} ...", + "appslist_could_not_migrate": "No se pudo migrar la lista de aplicaciones {appslist:s}! No se pudo analizar el URL ... El antiguo cronjob se ha mantenido en {bkp_file:s}.", + "appslist_corrupted_json": "No se pudieron cargar las listas de aplicaciones. Parece que {filename: s} está dañado.", + "invalid_url_format": "Formato de URL no válido" } diff --git a/locales/fr.json b/locales/fr.json index ce6e8192f..b856e4a55 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -109,8 +109,8 @@ "format_datetime_short": "%d/%m/%Y %H:%M", "hook_argument_missing": "Argument manquant : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'", - "hook_exec_failed": "Échec de l'exécution du script « {path:s} »", - "hook_exec_not_terminated": "L'exécution du script « {path:s} » ne s'est pas terminée", + "hook_exec_failed": "Échec de l’exécution du script « {path:s} »", + "hook_exec_not_terminated": "L’exécution du script « {path:s} » ne s’est pas terminée", "hook_list_by_invalid": "La propriété de tri des actions est invalide", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", @@ -296,5 +296,20 @@ "app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}", "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante", "app_already_up_to_date": "{app:s} est déjà à jour", - "invalid_url_format": "Format d’URL non valide" + "invalid_url_format": "Format d’URL non valide", + "global_settings_bad_choice_for_enum": "La valeur du paramètre {setting:s} est incorrecte. Reçu : {received_type:s}; attendu : {expected_type:s}", + "global_settings_bad_type_for_setting": "Le type du paramètre {setting:s} est incorrect. Reçu : {received_type:s}; attendu : {expected_type:s}.", + "global_settings_cant_open_settings": "Échec de l’ouverture du ficher de configurations, cause : {reason:s}", + "global_settings_cant_serialize_setings": "Échec de sérialisation des données de configurations, cause : {reason:s}", + "global_settings_cant_write_settings": "Échec d’écriture du fichier de configurations, cause : {reason:s}", + "global_settings_key_doesnt_exists": "La clef « {settings_key:s} » n’existe pas dans les configurations globales, vous pouvez voir toutes les clefs disponibles en saisissant « yunohost settings list »", + "global_settings_reset_success": "Réussite ! Vos configurations précédentes ont été sauvegardées dans {path:s}", + "global_settings_setting_example_bool": "Exemple d’option booléenne", + "global_settings_setting_example_int": "Exemple d’option de type entier", + "global_settings_setting_example_string": "Exemple d’option de type chaîne", + "global_settings_setting_example_enum": "Exemple d’option de type énumération", + "global_settings_unknown_type": "Situation inattendue, la configuration {setting:s} semble avoir le type {unknown_type:s} mais ce n’est pas un type pris en charge par le système.", + "global_settings_unknown_setting_from_settings_file": "Clef inconnue dans les configurations : {setting_key:s}, rejet de cette clef et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", + "service_conf_new_managed_file": "Le fichier de configuration « {conf} » est désormais géré par le service {service}.", + "service_conf_file_kept_back": "Le fichier de configuration « {conf} » devrait être supprimé par le service {service} mais a été conservé." } From a687b850256acb9671d1bd09fecd79a0bbef37a3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 2 Jun 2017 09:16:08 -0400 Subject: [PATCH 0361/1066] Update changelog for 2.6.3 release --- debian/changelog | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/debian/changelog b/debian/changelog index 81b816e39..09e097dcb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,27 @@ +yunohost (2.6.3) testing; urgency=low + + Major changes + ------------- + + * [love] Add missing contributors & translators. + * [enh] Introduce global settings (#229) + * [enh] Refactor backup management to pave the way to borg (#275) + * [enh] Changing nginx ciphers to intermediate compatiblity (#298) + * [enh] Use ssl-cert group for certificates, instead of metronome (#222) + * [enh] Allow regen-conf to manage new files already present on the system (#311) + * [apps] New helpers + * ynh_secure_remove (#281) + * ynh_setup_source (#282) + * ynh_webpath_available and ynh_webpath_register (#235) + * ynh_mysql_generate_db and ynh_mysql_remove_db (#236) + * ynh_store_file_checksum and ynh_backup_if_checksum_is_different (#286) + * Misc fixes here and there + * [i18n] Update Spanish, German and French translations (#318) + + Thanks to all contributors : Bram, ljf, opi, Maniack C, Alex, JimboJoe, Moul, Jibec, JuanuSt and franzos ! + + -- Alexandre Aubin Fri, 02 Jun 2017 09:15:05 -0400 + yunohost (2.6.2) testing; urgency=low New Features From 5e4cce3f1f7ea5f68d0d9c1e85158e52e69fcf5c Mon Sep 17 00:00:00 2001 From: opi Date: Wed, 7 Jun 2017 11:53:45 +0200 Subject: [PATCH 0362/1066] [fix] Do not break if disk is not a dict (fixes #redmine-919) (#315) --- src/yunohost/tools.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index cf3152dce..b0e647238 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -516,11 +516,15 @@ def tools_diagnosis(auth, private=False): else: diagnosis['system']['disks'] = {} for disk in disks: - diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % ( - disks[disk]['mnt_point'], - disks[disk]['size'], - disks[disk]['avail'] - ) + if isinstance(disk, str): + diagnosis['system']['disks'] = disk + else: + diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % ( + disks[disk]['mnt_point'], + disks[disk]['size'], + disks[disk]['avail'] + ) + try: system = monitor_system(units=['cpu', 'memory'], human_readable=True) From de3a0b4f694fc18a08bf78d9d8b9dfa9a6847312 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 9 Jun 2017 00:46:36 +0200 Subject: [PATCH 0363/1066] [fix] ynh_install_app_dependencies uses grep instead of python (#313) Proposition to discuss --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 66982cf25..a3e89f9d8 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -115,7 +115,7 @@ ynh_install_app_dependencies () { if [ ! -e "$manifest_path" ]; then manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place fi - version=$(sudo python3 -c "import sys, json;print(json.load(open(\"$manifest_path\"))['version'])") # Retrieve the version number in the manifest file. + version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. dep_app=${app//_/-} # Replace all '_' by '-' if ynh_package_is_installed "${dep_app}-ynh-deps"; then From 6042e71003710a7d8bac874cd21bab9bcc5fd869 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 12 Jun 2017 16:58:22 +0200 Subject: [PATCH 0364/1066] [fix] Don't attempt to change hostname in LXC (#301) * Don't attempt to change hostname in LXC * Detect any kind of container, not just lxc --- src/yunohost/tools.py | 59 +++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index b0e647238..d2c7d5fe6 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -164,14 +164,40 @@ def tools_maindomain(auth, new_domain=None): logger.warning("%s" % e, exc_info=1) raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) - # Clear nsswitch cache for hosts to make sure hostname is resolved ... + _set_hostname(new_domain) + + # Generate SSOwat configuration file + app_ssowatconf(auth) + + # Regen configurations + try: + with open('/etc/yunohost/installed', 'r') as f: + service_regen_conf() + except IOError: + pass + + logger.success(m18n.n('maindomain_changed')) + + +def _set_hostname(hostname, pretty_hostname=None): + """ + Change the machine hostname using hostnamectl + """ + + if _is_inside_container(): + logger.warning("You are inside a container and hostname cannot easily be changed") + return + + if not pretty_hostname: + pretty_hostname = "(YunoHost/%s)" % hostname + + # First clear nsswitch cache for hosts to make sure hostname is resolved... subprocess.call(['nscd', '-i', 'hosts']) - # Set hostname - pretty_hostname = "(YunoHost/%s)" % new_domain + # Then call hostnamectl commands = [ - "sudo hostnamectl --static set-hostname".split() + [new_domain], - "sudo hostnamectl --transient set-hostname".split() + [new_domain], + "sudo hostnamectl --static set-hostname".split() + [hostname], + "sudo hostnamectl --transient set-hostname".split() + [hostname], "sudo hostnamectl --pretty set-hostname".split() + [pretty_hostname] ] @@ -189,17 +215,22 @@ def tools_maindomain(auth, new_domain=None): else: logger.info(out) - # Generate SSOwat configuration file - app_ssowatconf(auth) - # Regen configurations - try: - with open('/etc/yunohost/installed', 'r') as f: - service_regen_conf() - except IOError: - pass +def _is_inside_container(): + """ + Check if we're inside a container (i.e. LXC) - logger.success(m18n.n('maindomain_changed')) + Returns True or False + """ + + # See https://stackoverflow.com/a/37016302 + p = subprocess.Popen("sudo cat /proc/1/sched".split(), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + out, _ = p.communicate() + + return out.split()[1] != "(1," def tools_postinstall(domain, password, ignore_dyndns=False): From edcabddb6390e358cb254c78e61969aaa3e97097 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 12 Jun 2017 17:03:40 +0200 Subject: [PATCH 0365/1066] [enh] Do not change the db_pwd (#319) * [enh] Do not change the db_pwd For restore script, do not change the password. Keep the primary password and recreate a new database with it. * A small comment ;) Thanks, opi, sometimes I forget that's not so obvious. And I don't want to someone has to search through internet to understand a helper. --- data/helpers.d/mysql | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index f5105a4e4..1c0ece114 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -114,13 +114,16 @@ ynh_mysql_drop_user() { # After executing this helper, the password of the created database will be available in $db_pwd # It will also be stored as "mysqlpwd" into the app settings. # -# usage: ynh_mysql_setup_db user name +# usage: ynh_mysql_setup_db user name [pwd] # | arg: user - Owner of the database # | arg: name - Name of the database +# | arg: pwd - Password of the database. If not given, a password will be generated ynh_mysql_setup_db () { local db_user="$1" local db_name="$2" - db_pwd=$(ynh_string_random) # Generate a random password + local new_db_pwd=$(ynh_string_random) # Generate a random password + # If $3 is not given, use new_db_pwd instead for db_pwd. + db_pwd="${3:-$new_db_pwd}" ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database ynh_app_setting_set $app mysqlpwd $db_pwd # Store the password in the app's config } From a01f7ffd2b691daf84b02c2137815b08ae4eab7d Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 13 Jun 2017 22:18:48 +0200 Subject: [PATCH 0366/1066] [Fix] Forgot an asterisk in logrotate helper (#320) --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index d6754bbfa..2b199fd90 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -17,7 +17,7 @@ ynh_use_logrotate () { logfile=$1/.log # Else, uses the directory and all logfile into it. fi else - logfile="/var/log/${app}/.log" # Without argument, use a defaut directory in /var/log + logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log fi cat > ./${app}-logrotate << EOF # Build a config file for logrotate $logfile { From 3320aee86eacbe5a496d0cfc7691a13a8400aeac Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 17 Jun 2017 19:22:10 +0200 Subject: [PATCH 0367/1066] [enh] New helper autopurge Add an autoremove with purge. Some deb packages, like transmission let a lot of shit behind itself. In case of remove then install, transmission doesn't work anymore. --- data/helpers.d/package | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index a3e89f9d8..36777aa52 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -63,6 +63,14 @@ ynh_package_autoremove() { ynh_apt autoremove $@ } +# Purge package(s) and their uneeded dependencies +# +# usage: ynh_package_autopurge name [name [...]] +# | arg: name - the package name to autoremove and purge +ynh_package_autopurge() { + ynh_apt autoremove --purge $@ +} + # Build and install a package from an equivs control file # # example: generate an empty control file with `equivs-control`, adjust its @@ -145,5 +153,5 @@ EOF # usage: ynh_remove_app_dependencies ynh_remove_app_dependencies () { dep_app=${app//_/-} # Replace all '_' by '-' - ynh_package_autoremove ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. + ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. } From d9761aad7bf566afa684804549e6fc2636db0865 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 21 Jun 2017 21:04:25 +0200 Subject: [PATCH 0368/1066] [fix] Move archivemount to suggested dependency (#322) --- debian/control | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/debian/control b/debian/control index e9a0a3d47..4cd128cbe 100644 --- a/debian/control +++ b/debian/control @@ -26,7 +26,6 @@ Depends: ${python:Depends}, ${misc:Depends} , ssowat, metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , haveged - , archivemount Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper @@ -34,7 +33,7 @@ Recommends: yunohost-admin , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl -Suggests: htop, vim, rsync, acpi-support-base, udisks2 +Suggests: htop, vim, rsync, acpi-support-base, udisks2, archivemount Conflicts: iptables-persistent , moulinette-yunohost, yunohost-config , yunohost-config-others, yunohost-config-postfix From a8eac8ac420fbe5675282144e853a5793e7ab325 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Wed, 21 Jun 2017 21:32:18 +0200 Subject: [PATCH 0369/1066] Update from Weblate. (#323) * [i18n] Translated using Weblate (Spanish) * [i18n] Translated using Weblate (German) * [i18n] Translated using Weblate (French) --- locales/de.json | 27 +++++++++++++++++++-------- locales/fr.json | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/locales/de.json b/locales/de.json index 4af251e14..14a9cb4b9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -12,7 +12,7 @@ "app_install_files_invalid": "Ungültige Installationsdateien", "app_location_already_used": "Eine andere App ist bereits an diesem Ort installiert", "app_location_install_failed": "Die App kann nicht an diesem Ort installiert werden", - "app_manifest_invalid": "Ungültiges App-Verzeichnis", + "app_manifest_invalid": "Ungültiges App-Manifest", "app_no_upgrade": "Keine Aktualisierungen für Apps verfügbar", "app_not_installed": "{app:s} ist nicht installiert", "app_recent_version_required": "Für {:s} benötigt eine aktuellere Version von moulinette", @@ -23,8 +23,8 @@ "app_upgraded": "{app:s} wurde erfolgreich aktualisiert", "appslist_fetched": "Appliste {appslist:s} wurde erfolgreich heruntergelanden", "appslist_removed": "Appliste {appslist:s} wurde erfolgreich entfernt", - "appslist_retrieve_error": "Entfernte Appliste kann nicht empfangen werden: {error}", - "appslist_unknown": "Unbekannte Appliste", + "appslist_retrieve_error": "Entfernte Appliste {appslist:s} kann nicht empfangen werden: {error:s}", + "appslist_unknown": "Appliste {appslist:s} ist unbekannt.", "ask_current_admin_password": "Derzeitiges Administrator-Kennwort", "ask_email": "E-Mail-Adresse", "ask_firstname": "Vorname", @@ -153,7 +153,7 @@ "restore_complete": "Wiederherstellung abgeschlossen", "restore_confirm_yunohost_installed": "Möchtest du die Wiederherstellung wirklich starten? [{answers:s}]", "restore_failed": "System kann nicht Wiederhergestellt werden", - "restore_hook_unavailable": "Der Wiederherstellungshook '{hook:s}' steht auf deinem System nicht zur Verfügung", + "restore_hook_unavailable": "Das Wiederherstellungsskript für '{part:s}' steht weder in deinem System noch im Archiv zur Verfügung", "restore_nothings_done": "Es wurde nicht wiederhergestellt", "restore_running_app_script": "Wiederherstellung wird ausfeührt für App '{app:s}'...", "restore_running_hooks": "Wiederherstellung wird gestartet...", @@ -275,18 +275,29 @@ "app_package_need_update": "Es ist notwendig das Paket zu aktualisieren, um Aktualisierungen für YunoHost zu erhalten", "service_regenconf_dry_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server {service} notwendig sind...", "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...", - "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine misskonfigurierte Firewall/Router deines Servers dafür verantwortlich.", + "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine falsch konfigurierte Firewall/Router deines Servers dafür verantwortlich.", "certmanager_couldnt_fetch_intermediate_cert": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte die Teilzertifikate von Let's Encrypt zusammenzusetzen. Die Installation/Erneuerung des Zertifikats wurde abgebrochen - bitte versuche es später erneut.", - "appslist_retrieve_bad_format": "Die empfangene Datei ist keine gültige Appliste", + "appslist_retrieve_bad_format": "Die empfangene Datei der Appliste {appslist:s} ist ungültig", "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen", "appslist_name_already_tracked": "Es gibt bereits eine registrierte App-Liste mit Namen {name:s}.", "appslist_url_already_tracked": "Es gibt bereits eine registrierte Anwendungsliste mit dem URL {url:s}.", "appslist_migrating": "Migriere Anwendungsliste {appslist:s} ...", - "appslist_could_not_migrate": "Konnte Anwendungsliste {appslist:s} nicht migrieren. Konnte URL nicht verarbeiten... Der alte Cron-Job wurde unter {bkp_file:s} beibehalten.", + "appslist_could_not_migrate": "Konnte Anwendungsliste {appslist:s} nicht migrieren. Konnte die URL nicht verarbeiten... Der alte Cron-Job wurde unter {bkp_file:s} beibehalten.", "appslist_corrupted_json": "Konnte die Anwendungslisten. Es scheint, dass {filename:s} beschädigt ist.", "yunohost_ca_creation_success": "Die lokale Zertifizierungs-Authorität wurde angelegt.", "app_already_installed_cant_change_url": "Diese Application ist bereits installiert. Die URL kann durch diese Funktion nicht modifiziert werden. Überprüfe ob `app changeurl` verfügbar ist.", "app_change_no_change_url_script": "Die Application {app_name:s} unterstützt das anpassen der URL noch nicht. Sie muss gegebenenfalls erweitert werden.", "app_change_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}", - "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun." + "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun.", + "app_already_up_to_date": "{app:s} ist schon aktuell", + "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", + "backup_applying_method_tar": "Erstellen des Backup-tar Archives...", + "backup_applying_method_copy": "Kopiere alle Dateien ins Backup...", + "app_change_url_no_script": "Die Anwendung '{app_name:s}' unterstützt bisher keine URL-Modufikation. Vielleicht gibt es eine Aktualisierung der Anwendung.", + "app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Anwendung genutzt", + "backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method:s}' auf...", + "backup_archive_system_part_not_available": "Der System-Teil '{part:s}' ist in diesem Backup nicht enthalten", + "backup_archive_mount_failed": "Das Einbinden des Backup-Archives ist fehlgeschlagen", + "backup_archive_writing_error": "Die Dateien konnten nicht in der komprimierte Archiv-Backup hinzugefügt werden", + "app_change_url_success": "Erfolgreiche Änderung der URL von {app:s} zu {domain:s}{path:s}" } diff --git a/locales/fr.json b/locales/fr.json index b856e4a55..86df7b10d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -176,7 +176,7 @@ "restore_complete": "Restauration terminée", "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]", "restore_failed": "Impossible de restaurer le système", - "restore_hook_unavailable": "Le script de restauration « {hook:s} » n'est pas disponible sur votre système", + "restore_hook_unavailable": "Le script de restauration « {part:s} » n'est pas disponible sur votre système, et n’est pas non plus dans l’archive", "restore_nothings_done": "Rien n'a été restauré", "restore_running_app_script": "Lancement du script de restauration pour l'application « {app:s} »...", "restore_running_hooks": "Exécution des scripts de restauration...", @@ -311,5 +311,38 @@ "global_settings_unknown_type": "Situation inattendue, la configuration {setting:s} semble avoir le type {unknown_type:s} mais ce n’est pas un type pris en charge par le système.", "global_settings_unknown_setting_from_settings_file": "Clef inconnue dans les configurations : {setting_key:s}, rejet de cette clef et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", "service_conf_new_managed_file": "Le fichier de configuration « {conf} » est désormais géré par le service {service}.", - "service_conf_file_kept_back": "Le fichier de configuration « {conf} » devrait être supprimé par le service {service} mais a été conservé." + "service_conf_file_kept_back": "Le fichier de configuration « {conf} » devrait être supprimé par le service {service} mais a été conservé.", + "backup_abstract_method": "Cette méthode de sauvegarde n’a pas encore été implémentée", + "backup_applying_method_tar": "Création de l’archive tar de la sauvegarde…", + "backup_applying_method_copy": "Copie de tous les fichiers dans la sauvegarde…", + "backup_applying_method_borg": "Envoi de tous les fichiers dans la sauvegarde dans de référentiel borg-backup…", + "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée « {method:s} »…", + "backup_archive_system_part_not_available": "La partie « {part:s} » du système n’est pas disponible dans cette sauvegarde", + "backup_archive_mount_failed": "Le montage de l’archive de sauvegarde a échoué", + "backup_archive_writing_error": "Impossible d’ajouter les fichiers à la sauvegarde dans l’archive compressée", + "backup_ask_for_copying_if_needed": "Votre système ne prend pas complètement en charge la méthode rapide d’organisation des fichiers dans l’archive, voulez-vous les organiser en copiant {size:s} Mio ?", + "backup_borg_not_implemented": "La méthode de sauvegarde Bord n’est pas encore implémentée", + "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l’archive décompressée", + "backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser l’archive", + "backup_csv_creation_failed": "Impossible de créer le fichier CSV nécessaire aux opérations futures de restauration", + "backup_csv_addition_failed": "Impossible d’ajouter des fichiers à sauvegarder dans le fichier CSV", + "backup_custom_need_mount_error": "Échec de la méthode de sauvegarde personnalisée à l’étape « need_mount »", + "backup_custom_backup_error": "Échec de la méthode de sauvegarde personnalisée à l’étape « backup »", + "backup_custom_mount_error": "Échec de la méthode de sauvegarde personnalisée à l’étape « mount »", + "backup_no_uncompress_archive_dir": "Le dossier de l’archive décompressée n’existe pas", + "backup_method_tar_finished": "L’archive tar de la sauvegarde a été créée", + "backup_method_copy_finished": "La copie de la sauvegarde est terminée", + "backup_method_borg_finished": "La sauvegarde dans Borg est terminée", + "backup_method_custom_finished": "La méthode se sauvegarde personnalisée « {method:s} » est terminée", + "backup_system_part_failed": "Impossible de sauvegarder la partie « {part:s} » du système", + "backup_unable_to_organize_files": "Impossible d’organiser les fichiers dans l’archive avec la méthode rapide", + "backup_with_no_backup_script_for_app": "L’application {app:s} n’a pas de script de sauvegarde. Ignorer.", + "backup_with_no_restore_script_for_app": "L’application {app:s} n’a pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.", + "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage, cause : {reason:s}", + "restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire", + "restore_extracting": "Extraction des fichiers nécessaires depuis l’archive…", + "restore_mounting_archive": "Montage de l’archive dans « {path:s} »", + "restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment d’espace disponible (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", + "restore_not_enough_disk_space": "Espace disponible insuffisant (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", + "restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système" } From 9bbdaac58b8d56a28cf16c2a1f8d9de0de1b9fda Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 21 Jun 2017 17:34:41 -0400 Subject: [PATCH 0370/1066] Update changelog for 2.6.4 release --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 09e097dcb..cfedc4baf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (2.6.4) stable; urgency=low + + Changes + ------------- + + * Misc fixes here and there + * [i18n] Update Spanish, German and French translations (#323) + + Thanks to all contributors : opi, Maniack C, Alex, JuanuSt, franzos, Jibec, Jeroen and beyercenter ! + + -- ljf Wed, 21 Jun 2017 17:18:00 -0400 + yunohost (2.6.3) testing; urgency=low Major changes From f0c9e697fe14dc27dafd90a09e07869397c8775c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2017 17:45:34 +0200 Subject: [PATCH 0371/1066] [fix] Do not crash if archivemount is not there (#325) --- src/yunohost/backup.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 03598ec48..0934f82e9 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1776,8 +1776,13 @@ class TarBackupMethod(BackupMethod): tar.close() # Mount the tarball - ret = subprocess.call(['archivemount', '-o', 'readonly', - self._archive_file, self.work_dir]) + try: + ret = subprocess.call(['archivemount', '-o', 'readonly', + self._archive_file, self.work_dir]) + except: + ret = -1 + + # If archivemount failed, extract the archive if ret != 0: logger.warning(m18n.n('backup_archive_mount_failed')) From 9782f058877adbbb8e46ba7ed7c6fb1e415f6a99 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 10 Jul 2017 17:48:33 +0200 Subject: [PATCH 0372/1066] New helpers ynh_add_fpm_config and ynh_remove_fpm_config (#284) * New helpers ynh_add_fpm_config and ynh_remove_fpm_config Standard configuration of php-fpm. Use local files stored in conf/, so it's still possible to use a specific config * ynh_substitute_char was renamed to ynh_replace_string * Update checksum command and quiet the second secure_remove * Fix helpers name --- data/helpers.d/backend | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 2b199fd90..dbb739aaf 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -51,3 +51,37 @@ ynh_remove_logrotate () { sudo rm "/etc/logrotate.d/$app" fi } + +# Create a dedicated php-fpm config +# +# usage: ynh_add_fpm_config +ynh_add_fpm_config () { + finalphpconf="/etc/php5/fpm/pool.d/$app.conf" + ynh_backup_if_checksum_is_different "$finalphpconf" + sudo cp ../conf/php-fpm.conf "$finalphpconf" + ynh_replace_string "__NAMETOCHANGE__" "$app" "$finalphpconf" + ynh_replace_string "__FINALPATH__" "$final_path" "$finalphpconf" + ynh_replace_string "__USER__" "$app" "$finalphpconf" + sudo chown root: "$finalphpconf" + ynh_store_file_checksum "$finalphpconf" + + if [ -e "../conf/php-fpm.ini" ] + then + finalphpini="/etc/php5/fpm/conf.d/20-$app.ini" + ynh_backup_if_checksum_is_different "$finalphpini" + sudo cp ../conf/php-fpm.ini "$finalphpini" + sudo chown root: "$finalphpini" + ynh_store_file_checksum "$finalphpini" + fi + + sudo systemctl reload php5-fpm +} + +# Remove the dedicated php-fpm config +# +# usage: ynh_remove_fpm_config +ynh_remove_fpm_config () { + ynh_secure_remove "/etc/php5/fpm/pool.d/$app.conf" + ynh_secure_remove "/etc/php5/fpm/conf.d/20-$app.ini" 2>&1 + sudo systemctl reload php5-fpm +} From 20f4a39bf081960655645db8810e0884c74b6a8b Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 13 Jul 2017 19:36:31 +0200 Subject: [PATCH 0374/1066] [fix] Errors in backup custom methods (#326) --- data/actionsmap/yunohost.yml | 3 +++ src/yunohost/backup.py | 43 ++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index cccc1adbb..c6f8f2458 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -748,6 +748,9 @@ backup: full: --no-compress help: Do not create an archive file action: store_true + --methods: + help: List of backup methods to apply (copy or tar by default) + nargs: "*" --system: help: List of system parts to backup (all by default) nargs: "*" diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 0934f82e9..a195763b4 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1468,7 +1468,7 @@ class BackupMethod(object): if self.manager.is_tmp_work_dir: filesystem.rm(self.work_dir, True, True) - def _recursive_umount(directory): + def _recursive_umount(self, directory): """ Recursively umount sub directories of a directory @@ -1525,7 +1525,7 @@ class BackupMethod(object): """ paths_needed_to_be_copied = [] for path in self.manager.paths_to_backup: - src = path['src'] + src = path['source'] if self.manager is RestoreManager: # TODO Support to run this before a restore (and not only before @@ -1534,14 +1534,17 @@ class BackupMethod(object): src = os.path.join(self.unorganized_work_dir, src) dest = os.path.join(self.work_dir, path['dest']) + if dest == src: + continue dest_dir = os.path.dirname(dest) # Be sure the parent dir of destination exists - filesystem.mkdir(dest_dir, parent=True) + if not os.path.isdir(dest_dir): + filesystem.mkdir(dest_dir, parents=True) # Try to bind files if os.path.isdir(src): - filesystem.mkdir(dest, parent=True) + filesystem.mkdir(dest, parents=True, force=True) ret = subprocess.call(["mount", "-r", "--rbind", src, dest]) if ret == 0: continue @@ -1560,13 +1563,12 @@ class BackupMethod(object): if len(paths_needed_to_be_copied) == 0: return - # Manage the case where we are not able to use mount bind abilities # It could be just for some small files on different filesystems or due # to mounting error # Compute size to copy - size = sum(disk_usage(path['src']) for path in paths_needed_to_be_copied) + size = sum(disk_usage(path['source']) for path in paths_needed_to_be_copied) size /= (1024 * 1024) # Convert bytes to megabytes # Ask confirmation for copying @@ -1583,10 +1585,11 @@ class BackupMethod(object): # Copy unbinded path logger.info(m18n.n('backup_copying_to_organize_the_archive', size=size)) for path in paths_needed_to_be_copied: - if os.path.isdir(src): - shutil.copytree(src, dest, symlinks=True) + dest = os.path.join(self.work_dir, path['dest']) + if os.path.isdir(path['source']): + shutil.copytree(path['source'], dest, symlinks=True) else: - shutil.copy(src, dest) + shutil.copy(path['source'], dest) @classmethod def create(cls, method, *args): @@ -1616,7 +1619,7 @@ class BackupMethod(object): if method in ["copy", "tar", "borg"]: return bm_class[method](*args) else: - return CustomBackupMethod(*args) + return CustomBackupMethod(method=method, *args) class CopyBackupMethod(BackupMethod): @@ -1861,9 +1864,10 @@ class CustomBackupMethod(BackupMethod): backup/restore operations. A user can add his own hook inside /etc/yunohost/hooks.d/backup_method/ """ - def __init__(self, repo = None, **kwargs): + def __init__(self, repo = None, method = None,**kwargs): super(CustomBackupMethod, self).__init__(repo) self.args = kwargs + self.method = method self._need_mount = None @@ -1878,13 +1882,14 @@ class CustomBackupMethod(BackupMethod): Exceptions: backup_custom_need_mount_error -- Raised if the hook failed """ - ret = hook_callback('backup_method', method, + if self._need_mount is not None: + return self._need_mount + + ret = hook_callback('backup_method', [self.method], args=self._get_args('need_mount')) - if ret['succeed']: - return True - else: - raise MoulinetteError(errno.EIO, - m18n.n('backup_custom_need_mount_error')) + + self._need_mount = True if ret['succeed'] else False + return self._need_mount def backup(self): @@ -1895,7 +1900,7 @@ class CustomBackupMethod(BackupMethod): backup_custom_backup_error -- Raised if the custom script failed """ - ret = hook_callback('backup_method', method, + ret = hook_callback('backup_method', [self.method], args=self._get_args('backup')) if ret['failed']: raise MoulinetteError(errno.EIO, @@ -1909,7 +1914,7 @@ class CustomBackupMethod(BackupMethod): backup_custom_mount_error -- Raised if the custom script failed """ super(CustomBackupMethod, self).mount(restore_manager) - ret = hook_callback('backup_method', method, + ret = hook_callback('backup_method', [self.method], args=self._get_args('mount')) if ret['failed']: raise MoulinetteError(errno.EIO, From baf1d44c1f36a65def0699de38745dfe5c3503d3 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Mon, 17 Jul 2017 20:39:39 +0200 Subject: [PATCH 0375/1066] Run change_url scripts as root as a matter of homogeneity (#329) --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 044b81632..c2ddc3a7c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -487,7 +487,7 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) # XXX journal - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict) != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl From 89189ed52f9f39bc0375b7ec39f55a1a0764f5c6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 21 Jul 2017 00:28:37 +0200 Subject: [PATCH 0376/1066] [fix] Refactor DNS conf management for domains (#299) * Add an helper that build a dict describing the dns conf * Synchronize the dyndns dns conf with the one from domain.py * [mod] try to make code more lisible * [mod] try to make code a bit more lisible * [mod/fix] try to simplify and clean the code (and remove what looks like a debug return) * [fix] First delete records, then add the new records --- src/yunohost/domain.py | 190 +++++++++++++++++++++++++++++------------ src/yunohost/dyndns.py | 168 ++++++++++++++++++------------------ 2 files changed, 221 insertions(+), 137 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index e869df6d0..284457e4d 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -198,67 +198,26 @@ def domain_dns_conf(domain, ttl=None): ttl -- Time to live """ + ttl = 3600 if ttl is None else ttl - ip4 = ip6 = None - # A/AAAA records - ip4 = get_public_ip() - result = ( - "@ {ttl} IN A {ip4}\n" - "* {ttl} IN A {ip4}\n" - ).format(ttl=ttl, ip4=ip4) + dns_conf = _build_dns_conf(domain, ttl) - try: - ip6 = get_public_ip(6) - except: - pass - else: - result += ( - "@ {ttl} IN AAAA {ip6}\n" - "* {ttl} IN AAAA {ip6}\n" - ).format(ttl=ttl, ip6=ip6) + result = "" - # Jabber/XMPP - result += ("\n" - "_xmpp-client._tcp {ttl} IN SRV 0 5 5222 {domain}.\n" - "_xmpp-server._tcp {ttl} IN SRV 0 5 5269 {domain}.\n" - "muc {ttl} IN CNAME @\n" - "pubsub {ttl} IN CNAME @\n" - "vjud {ttl} IN CNAME @\n" - ).format(ttl=ttl, domain=domain) + result += "# Basic ipv4/ipv6 records" + for record in dns_conf["basic"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) - # Email - result += ('\n' - '@ {ttl} IN MX 10 {domain}.\n' - '@ {ttl} IN TXT "v=spf1 a mx ip4:{ip4}' - ).format(ttl=ttl, domain=domain, ip4=ip4) - if ip6 is not None: - result += ' ip6:{ip6}'.format(ip6=ip6) - result += ' -all"' + result += "\n\n" + result += "# XMPP" + for record in dns_conf["xmpp"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) - # DKIM - try: - with open('/etc/dkim/{domain}.mail.txt'.format(domain=domain)) as f: - dkim_content = f.read() - except IOError: - pass - else: - dkim = re.match(( - r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' - '(?=.*(;[\s]*|")v=(?P[^";]+))' - '(?=.*(;[\s]*|")k=(?P[^";]+))' - '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M | re.S - ) - if dkim: - result += '\n{host}. {ttl} IN TXT "v={v}; k={k}; p={p}"'.format( - host='{0}.{1}'.format(dkim.group('host'), domain), ttl=ttl, - v=dkim.group('v'), k=dkim.group('k'), p=dkim.group('p') - ) - - # If DKIM is set, add dummy DMARC support - result += '\n_dmarc {ttl} IN TXT "v=DMARC1; p=none"'.format( - ttl=ttl - ) + result += "\n\n" + result += "# Mail" + for record in dns_conf["mail"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) return result @@ -322,6 +281,7 @@ def get_public_ip(protocol=4): url = 'https://ip6.yunohost.org' else: raise ValueError("invalid protocol version") + try: return urlopen(url).read().strip() except IOError: @@ -357,3 +317,123 @@ def _normalize_domain_path(domain, path): path = "/" + path.strip("/") return domain, path + + +def _build_dns_conf(domain, ttl=3600): + """ + Internal function that will returns a data structure containing the needed + information to generate/adapt the dns configuration + + The returned datastructure will have the following form: + { + "basic": [ + # if ipv4 available + {"type": "A", "name": "@", "value": "123.123.123.123", "ttl": 3600}, + {"type": "A", "name": "*", "value": "123.123.123.123", "ttl": 3600}, + # if ipv6 available + {"type": "AAAA", "name": "@", "value": "valid-ipv6", "ttl": 3600}, + {"type": "AAAA", "name": "*", "value": "valid-ipv6", "ttl": 3600}, + ], + "xmpp": [ + {"type": "SRV", "name": "_xmpp-client._tcp", "value": "0 5 5222 domain.tld.", "ttl": 3600}, + {"type": "SRV", "name": "_xmpp-server._tcp", "value": "0 5 5269 domain.tld.", "ttl": 3600}, + {"type": "CNAME", "name": "muc", "value": "@", "ttl": 3600}, + {"type": "CNAME", "name": "pubsub", "value": "@", "ttl": 3600}, + {"type": "CNAME", "name": "vjud", "value": "@", "ttl": 3600} + ], + "mail": [ + {"type": "MX", "name": "@", "value": "10 domain.tld.", "ttl": 3600}, + {"type": "TXT", "name": "@", "value": "\"v=spf1 a mx ip4:123.123.123.123 ipv6:valid-ipv6 -all\"", "ttl": 3600 }, + {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, + {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} + ], + } + """ + + try: + ipv4 = get_public_ip() + except: + ipv4 = None + + try: + ipv6 = get_public_ip(6) + except: + ipv6 = None + + basic = [] + + # Basic ipv4/ipv6 records + if ipv4: + basic += [ + ["@", ttl, "A", ipv4], + ["*", ttl, "A", ipv4], + ] + + if ipv6: + basic += [ + ["@", ttl, "AAAA", ipv6], + ["*", ttl, "AAAA", ipv6], + ] + + # XMPP + xmpp = [ + ["_xmpp-client._tcp", ttl, "SRV", "0 5 5222 %s." % domain], + ["_xmpp-server._tcp", ttl, "SRV", "0 5 5269 %s." % domain], + ["muc", ttl, "CNAME", "@"], + ["pubsub", ttl, "CNAME", "@"], + ["vjud", ttl, "CNAME", "@"], + ] + + # SPF record + spf_record = '"v=spf1 a mx' + if ipv4: + spf_record += ' ip4:{ip4}'.format(ip4=ipv4) + if ipv6: + spf_record += ' ip6:{ip6}'.format(ip6=ipv6) + spf_record += ' -all"' + + # Email + mail = [ + ["@", ttl, "MX", "10 %s." % domain], + ["@", ttl, "TXT", spf_record], + ] + + # DKIM/DMARC record + dkim_host, dkim_publickey = _get_DKIM(domain) + + if dkim_host: + mail += [ + [dkim_host, ttl, "TXT", dkim_publickey], + ["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'], + ] + + return { + "basic": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in basic], + "xmpp": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in xmpp], + "mail": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in mail], + } + + +def _get_DKIM(domain): + DKIM_file = '/etc/dkim/{domain}.mail.txt'.format(domain=domain) + + if not os.path.isfile(DKIM_file): + return (None, None) + + with open(DKIM_file) as f: + dkim_content = f.read() + + dkim = re.match(( + r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' + '(?=.*(;[\s]*|")v=(?P[^";]+))' + '(?=.*(;[\s]*|")k=(?P[^";]+))' + '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M | re.S + ) + + if not dkim: + return (None, None) + + return ( + dkim.group('host'), + '"v={v}; k={k}; p={p}"'.format(v=dkim.group('v'), k=dkim.group('k'), p=dkim.group('p')) + ) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index fca687b60..5dc507431 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -35,7 +35,7 @@ import subprocess from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.domain import get_public_ip, _get_maindomain +from yunohost.domain import get_public_ip, _get_maindomain, _build_dns_conf logger = getActionLogger('yunohost.dyndns') @@ -168,91 +168,95 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, except IOError: old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000' - if old_ip != ipv4 or old_ipv6 != ipv6: - if domain is None: - # Retrieve the first registered domain - for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): - match = re_dyndns_private_key.match(path) - if not match: + # no need to update + if old_ip == ipv4 and old_ipv6 == ipv6: + return + + if domain is None: + # Retrieve the first registered domain + for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): + match = re_dyndns_private_key.match(path) + if not match: + continue + _domain = match.group('domain') + + try: + # Check if domain is registered + if requests.get('https://{0}/test/{1}'.format( + dyn_host, _domain)).status_code == 200: continue - _domain = match.group('domain') - try: - # Check if domain is registered - if requests.get('https://{0}/test/{1}'.format( - dyn_host, _domain)).status_code == 200: - continue - except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, - m18n.n('no_internet_connection')) - domain = _domain - key = path - break - if not domain: - raise MoulinetteError(errno.EINVAL, - m18n.n('dyndns_no_domain_registered')) + except requests.ConnectionError: + raise MoulinetteError(errno.ENETUNREACH, + m18n.n('no_internet_connection')) + domain = _domain + key = path + break + if not domain: + raise MoulinetteError(errno.EINVAL, + m18n.n('dyndns_no_domain_registered')) - if key is None: - keys = glob.glob( - '/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) - if len(keys) > 0: - key = keys[0] - if not key: - raise MoulinetteError(errno.EIO, - m18n.n('dyndns_key_not_found')) + if key is None: + keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) - host = domain.split('.')[1:] - host = '.'.join(host) - lines = [ - 'server %s' % dyn_host, - 'zone %s' % host, - 'update delete %s. A' % domain, - 'update delete %s. AAAA' % domain, - 'update delete %s. MX' % domain, - 'update delete %s. TXT' % domain, - 'update delete pubsub.%s. A' % domain, - 'update delete pubsub.%s. AAAA' % domain, - 'update delete muc.%s. A' % domain, - 'update delete muc.%s. AAAA' % domain, - 'update delete vjud.%s. A' % domain, - 'update delete vjud.%s. AAAA' % domain, - 'update delete _xmpp-client._tcp.%s. SRV' % domain, - 'update delete _xmpp-server._tcp.%s. SRV' % domain, - 'update add %s. 1800 A %s' % (domain, ipv4), - 'update add %s. 14400 MX 5 %s.' % (domain, domain), - 'update add %s. 14400 TXT "v=spf1 a mx -all"' % domain, - 'update add pubsub.%s. 1800 A %s' % (domain, ipv4), - 'update add muc.%s. 1800 A %s' % (domain, ipv4), - 'update add vjud.%s. 1800 A %s' % (domain, ipv4), - 'update add _xmpp-client._tcp.%s. 14400 SRV 0 5 5222 %s.' % (domain, domain), - 'update add _xmpp-server._tcp.%s. 14400 SRV 0 5 5269 %s.' % (domain, domain) - ] - if ipv6 is not None: - lines += [ - 'update add %s. 1800 AAAA %s' % (domain, ipv6), - 'update add pubsub.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add muc.%s. 1800 AAAA %s' % (domain, ipv6), - 'update add vjud.%s. 1800 AAAA %s' % (domain, ipv6), - ] - lines += [ - 'show', - 'send' - ] - with open('/etc/yunohost/dyndns/zone', 'w') as zone: - for line in lines: - zone.write(line + '\n') + if not keys: + raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) - if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) == 0: - logger.success(m18n.n('dyndns_ip_updated')) - with open('/etc/yunohost/dyndns/old_ip', 'w') as f: - f.write(ipv4) - if ipv6 is not None: - with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f: - f.write(ipv6) - else: - os.system('rm -f /etc/yunohost/dyndns/old_ip') - os.system('rm -f /etc/yunohost/dyndns/old_ipv6') - raise MoulinetteError(errno.EPERM, - m18n.n('dyndns_ip_update_failed')) + key = keys[0] + + host = domain.split('.')[1:] + host = '.'.join(host) + + lines = [ + 'server %s' % dyn_host, + 'zone %s' % host, + ] + + dns_conf = _build_dns_conf(domain) + + # Delete the old records for all domain/subdomains + + # every dns_conf.values() is a list of : + # [{"name": "...", "ttl": "...", "type": "...", "value": "..."}] + for records in dns_conf.values(): + for record in records: + action = "update delete {name}.{domain}.".format(domain=domain, **record) + action = action.replace(" @.", " ") + lines.append(action) + + # Add the new records for all domain/subdomains + + for records in dns_conf.values(): + for record in records: + # (For some reason) here we want the format with everytime the + # entire, full domain shown explicitly, not just "muc" or "@", it + # should be muc.the.domain.tld. or the.domain.tld + if record["value"] == "@": + record["value"] = domain + + action = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record) + action = action.replace(" @.", " ") + lines.append(action) + + lines += [ + 'show', + 'send' + ] + + with open('/etc/yunohost/dyndns/zone', 'w') as zone: + zone.write('\n'.join(lines)) + + if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) != 0: + os.system('rm -f /etc/yunohost/dyndns/old_ip') + os.system('rm -f /etc/yunohost/dyndns/old_ipv6') + raise MoulinetteError(errno.EPERM, + m18n.n('dyndns_ip_update_failed')) + + logger.success(m18n.n('dyndns_ip_updated')) + with open('/etc/yunohost/dyndns/old_ip', 'w') as f: + f.write(ipv4) + if ipv6 is not None: + with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f: + f.write(ipv6) def dyndns_installcron(): From c96d5f90ecdab51c9b62a7877ffb9a2e3b6ee9b7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 21 Jul 2017 00:45:05 +0200 Subject: [PATCH 0377/1066] Don't verify SSL during changeurl tests :/ (#332) --- src/yunohost/tests/test_changeurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index 506060cbb..737b68a6d 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -38,7 +38,7 @@ def check_changeurl_app(path): assert appmap[maindomain][path + "/"]["id"] == "change_url_app" - r = requests.get("https://%s%s/" % (maindomain, path)) + r = requests.get("https://%s%s/" % (maindomain, path), verify=False) assert r.status_code == 200 From 84153f63904a618753b1dbf086943a338f39600a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 21 Jul 2017 13:23:32 -0400 Subject: [PATCH 0378/1066] [mod] Microdecision to go with Moulinette#141 --- src/yunohost/firewall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index a4e68eac5..08ea041ee 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -233,7 +233,7 @@ def firewall_reload(skip_upnp=False): ] # Execute each rule - if process.check_commands(rules, callback=_on_rule_command_error): + if process.run_commands(rules, callback=_on_rule_command_error): errors = True reloaded = True @@ -262,7 +262,7 @@ def firewall_reload(skip_upnp=False): ] # Execute each rule - if process.check_commands(rules, callback=_on_rule_command_error): + if process.run_commands(rules, callback=_on_rule_command_error): errors = True reloaded = True From 3afbf0f7b9f68a7527bc67a856f3d1a688b848fe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 23 Jul 2017 01:56:05 +0200 Subject: [PATCH 0379/1066] [fix] Depreciation warning for --hooks was always shown (#333) --- src/yunohost/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a195763b4..f4b6b7fe7 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2093,7 +2093,7 @@ def backup_restore(auth, name, logger.warning("--ignore-hooks is deprecated and will be removed in the" "future. Please use --ignore-system instead.") ignore_system = ignore_hooks - if hooks != []: + if hooks != [] and hooks is not None: logger.warning("--hooks is deprecated and will be removed in the" "future. Please use --system instead.") system = hooks From 390835e96adf042dafa0df26012d8c16fecd2ec7 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 24 Jul 2017 02:50:48 +0200 Subject: [PATCH 0380/1066] [fix] Logrotate append (#328) * [fix] Logrotate append * Typo fix by JimboJoe --- data/helpers.d/backend | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index dbb739aaf..edd8ad7f4 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -1,16 +1,26 @@ # Use logrotate to manage the logfile # -# usage: ynh_use_logrotate [logfile] +# usage: ynh_use_logrotate [logfile] [--non-append] # | arg: logfile - absolute path of logfile +# | option: --non-append - Replace the config file instead of appending this new config. # # If no argument provided, a standard directory will be use. /var/log/${app} # You can provide a path with the directory only or with the logfile. # /parentdir/logdir/ # /parentdir/logdir/logfile.log # -# It's possible to use this helper several times, each config will added to same logrotate config file. +# It's possible to use this helper several times, each config will be added to the same logrotate config file. +# Unless you use the option --non-append ynh_use_logrotate () { - if [ "$#" -gt 0 ]; then + local customtee="tee -a" + if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then + customtee="tee" + # Destroy this argument for the next command. + shift + elif [ $# -gt 1 ] && [ "$2" == "--non-append" ]; then + customtee="tee" + fi + if [ $# -gt 0 ]; then if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile logfile=$1 # In this case, focus logrotate on the logfile else @@ -40,7 +50,7 @@ $logfile { } EOF sudo mkdir -p $(dirname "$logfile") # Create the log directory, if not exist - cat ${app}-logrotate | sudo tee -a /etc/logrotate.d/$app > /dev/null # Append this config to the others for this app. If a config file already exist + cat ${app}-logrotate | sudo $customtee /etc/logrotate.d/$app > /dev/null # Append this config to the existing config file, or replace the whole config file (depending on $customtee) } # Remove the app's logrotate config. From a513d0ecbe01803e5a0b976bac3a8f87f944c738 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 24 Jul 2017 20:11:58 +0200 Subject: [PATCH 0381/1066] [enh] Check that url is available and normalize path before app install (#304) --- src/yunohost/app.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c2ddc3a7c..f27001027 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1725,7 +1725,8 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - from yunohost.domain import domain_list, _get_maindomain + from yunohost.domain import (domain_list, _get_maindomain, + domain_url_available, _normalize_domain_path) from yunohost.user import user_info args_dict = OrderedDict() @@ -1830,6 +1831,33 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): m18n.n('app_argument_choice_invalid', name=arg_name, choices='0, 1')) args_dict[arg_name] = arg_value + + # END loop over action_args... + + # If there's only one "domain" and "path", validate that domain/path + # is an available url and normalize the path. + + domain_args = [arg["name"] for arg in action_args + if arg.get("type","string") == "domain"] + path_args = [arg["name"] for arg in action_args + if arg.get("type","string") == "path"] + + if len(domain_args) == 1 and len(path_args) == 1: + + domain = args_dict[domain_args[0]] + path = args_dict[path_args[0]] + domain, path = _normalize_domain_path(domain, path) + + # Check the url is available + if not domain_url_available(auth, domain, path): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_location_unavailable')) + + # (We save this normalized path so that the install script have a + # standard path format to deal with no matter what the user inputted) + args_dict[path_args[0]] = path + + return args_dict def _make_environment_dict(args_dict): From baf0d098f5c3547076f9f8477d383c4a3a5e21cb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 25 Jul 2017 21:32:20 +0200 Subject: [PATCH 0382/1066] [mod] explicitely import previously globals magic variables --- src/yunohost/app.py | 1 + src/yunohost/backup.py | 1 + src/yunohost/certificate.py | 1 + src/yunohost/domain.py | 1 + src/yunohost/dyndns.py | 1 + src/yunohost/firewall.py | 1 + src/yunohost/hook.py | 1 + src/yunohost/monitor.py | 1 + src/yunohost/service.py | 1 + src/yunohost/settings.py | 1 + src/yunohost/tests/test_backuprestore.py | 1 + src/yunohost/tools.py | 1 + src/yunohost/user.py | 1 + src/yunohost/utils/packages.py | 2 ++ 14 files changed, 15 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c2ddc3a7c..64afde6c7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -38,6 +38,7 @@ import pwd import grp from collections import OrderedDict +from moulinette import msignals, m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index f4b6b7fe7..2ddc292f2 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -36,6 +36,7 @@ import tempfile from glob import glob from collections import OrderedDict +from moulinette import msignals, m18n from moulinette.core import MoulinetteError from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a7d3b9389..a6a084d9a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -46,6 +46,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain +from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 284457e4d..3223183bb 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -33,6 +33,7 @@ import requests from urllib import urlopen +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 5dc507431..27aeedd31 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -32,6 +32,7 @@ import errno import requests import subprocess +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 08ea041ee..97451511f 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -33,6 +33,7 @@ except ImportError: sys.stderr.write('Error: Yunohost CLI Require miniupnpc lib\n') sys.exit(1) +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils import process from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 7d53a2d5f..cf9990a4d 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -28,6 +28,7 @@ import re import errno from glob import iglob +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils import log diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 137e57b6e..d99ac1688 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -37,6 +37,7 @@ import dns.resolver import cPickle as pickle from datetime import datetime +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 91e654f0b..2ea953873 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -33,6 +33,7 @@ import shutil import hashlib from difflib import unified_diff +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index f11371aee..aba6e32b3 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -5,6 +5,7 @@ import errno from datetime import datetime from collections import OrderedDict +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 245dee758..5fd1bd4a9 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -6,6 +6,7 @@ import shutil import subprocess from mock import ANY +from moulinette import m18n from moulinette.core import init_authenticator from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index d2c7d5fe6..0183aebd2 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -37,6 +37,7 @@ from collections import OrderedDict import apt import apt.progress +from moulinette import msettings, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 7c8776063..d35b082d7 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -32,6 +32,7 @@ import errno import subprocess import re +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.service import service_status diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 2372e7442..9242d22d1 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -25,6 +25,8 @@ from collections import OrderedDict import apt from apt_pkg import version_compare +from moulinette import m18n + logger = logging.getLogger('yunohost.utils.packages') From 46cb9637b23c3248074a402bad3e6ed3aac3a237 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 26 Jul 2017 05:43:08 +0200 Subject: [PATCH 0383/1066] [fix] missing m18n import --- bin/yunohost | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/yunohost b/bin/yunohost index 0bf2c004c..1522b7118 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -202,6 +202,8 @@ if __name__ == '__main__': if not os.path.isfile('/etc/yunohost/installed') and \ (len(args) < 2 or (args[0] +' '+ args[1] != 'tools postinstall' and \ args[0] +' '+ args[1] != 'backup restore')): + + from moulinette import m18n # Init i18n m18n.load_namespace('yunohost') m18n.set_locale(get_locale()) From 2c67d062c6c25b522264ac0e9d7eef3dc818f6c3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Jul 2017 17:45:34 +0200 Subject: [PATCH 0384/1066] [fix] Do not crash if archivemount is not there (#325) --- src/yunohost/backup.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 03598ec48..0934f82e9 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1776,8 +1776,13 @@ class TarBackupMethod(BackupMethod): tar.close() # Mount the tarball - ret = subprocess.call(['archivemount', '-o', 'readonly', - self._archive_file, self.work_dir]) + try: + ret = subprocess.call(['archivemount', '-o', 'readonly', + self._archive_file, self.work_dir]) + except: + ret = -1 + + # If archivemount failed, extract the archive if ret != 0: logger.warning(m18n.n('backup_archive_mount_failed')) From 91bfd5e266cc0d037368572091a5712ac893aff8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 26 Jul 2017 11:57:12 -0400 Subject: [PATCH 0385/1066] Update changelog for 2.6.5 release --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index cfedc4baf..0c99f1091 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (2.6.5) stable; urgency=low + + Minor fix + --------- + + * Do not crash backup restore if archivemount is not there (#325) + + -- Alexandre Aubin Wed, 26 Jul 2017 11:56:09 -0400 + yunohost (2.6.4) stable; urgency=low Changes From bf45cd7c4b1ff575ef679fdcc44ee77170fd6cb1 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 28 Jul 2017 18:53:43 +0200 Subject: [PATCH 0386/1066] =?UTF-8?q?New=20helpers=20ynh=5Fbackup=5Fafter?= =?UTF-8?q?=5Ffailed=5Fupgrade=20and=20ynh=5Fbackup=5Fbefore=5Fupg?= =?UTF-8?q?=E2=80=A6=20(#289)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New helpers ynh_backup_after_failed_upgrade and ynh_backup_before_upgrade Create a backup at the beginning of upgrade script. Then, if the upgrade script failed, remove the app and restore it. The backup is kept, to provide a way to restore if the upgrade is finally a bad thing. * Implement @JimboJoe's comments * Indentation * --ignore-hooks -> --ignore-system --- data/helpers.d/utils | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 8ff9e5791..bfa6ebbf2 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -25,6 +25,66 @@ ynh_get_plain_key() { done } +# Restore a previous backup if the upgrade process failed +# +# usage: +# ynh_backup_before_upgrade +# ynh_clean_setup () { +# ynh_backup_after_failed_upgrade +# } +# ynh_abort_if_errors +# +ynh_backup_after_failed_upgrade () { + echo "Upgrade failed." >&2 + app_bck=${app//_/-} # Replace all '_' by '-' + + # Check if an existing backup can be found before removing and restoring the application. + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number + then + # Remove the application then restore it + sudo yunohost app remove $app + # Restore the backup + sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force + ynh_die "The app was restored to the way it was before the failed upgrade." + fi +} + +# Make a backup in case of failed upgrade +# +# usage: +# ynh_backup_before_upgrade +# ynh_clean_setup () { +# ynh_backup_after_failed_upgrade +# } +# ynh_abort_if_errors +# +ynh_backup_before_upgrade () { + backup_number=1 + old_backup_number=2 + app_bck=${app//_/-} # Replace all '_' by '-' + + # Check if a backup already exists with the prefix 1 + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1 + then + # Prefix becomes 2 to preserve the previous backup + backup_number=2 + old_backup_number=1 + fi + + # Create backup + sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number + if [ "$?" -eq 0 ] + then + # If the backup succeeded, remove the previous backup + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number + then + # Remove the previous backup only if it exists + sudo yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null + fi + else + ynh_die "Backup failed, the upgrade process was aborted." + fi + # Download, check integrity, uncompress and patch the source from app.src # # The file conf/app.src need to contains: From 80242a8edc8e2529ffe9c9d9fd683d9e0921f2b2 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 28 Jul 2017 19:11:28 +0200 Subject: [PATCH 0387/1066] New helpers ynh_add_nginx_config and ynh_remove_nginx_config (#285) * New helpers ynh_add_nginx_config and ynh_remove_nginx_config Standard configuration of nginx Use local files stored in conf/, so it's still possible to use a specific config * ynh_substitute_char was renamed ynh_replace_string * ynh_compare_checksum_config -> ynh_backup_if_checksum_is_different * Upgrade helpers * Add some description of which keywords are replaced by which variable --- data/helpers.d/backend | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index edd8ad7f4..a6b14fc84 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -62,6 +62,54 @@ ynh_remove_logrotate () { fi } +# Create a dedicated nginx config +# +# This will use a template in ../conf/nginx.conf +# and will replace the following keywords with +# global variables that should be defined before calling +# this helper : +# +# __PATH__ by $path_url +# __DOMAIN__ by $domain +# __PORT__ by $port +# __NAME__ by $app +# __FINALPATH__ by $final_path +# +# usage: ynh_add_nginx_config +ynh_add_nginx_config () { + finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + ynh_backup_if_checksum_is_different "$finalnginxconf" + sudo cp ../conf/nginx.conf "$finalnginxconf" + + # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. + # Substitute in a nginx config file only if the variable is not empty + if test -n "${path_url:-}"; then + ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf" + fi + if test -n "${domain:-}"; then + ynh_replace_string "__DOMAIN__" "$domain" "$finalnginxconf" + fi + if test -n "${port:-}"; then + ynh_replace_string "__PORT__" "$port" "$finalnginxconf" + fi + if test -n "${app:-}"; then + ynh_replace_string "__NAME__" "$app" "$finalnginxconf" + fi + if test -n "${final_path:-}"; then + ynh_replace_string "__FINALPATH__" "$final_path" "$finalnginxconf" + fi + ynh_store_file_checksum "$finalnginxconf" + + sudo systemctl reload nginx +} + +# Remove the dedicated nginx config +# +# usage: ynh_remove_nginx_config +ynh_remove_nginx_config () { + ynh_secure_remove "/etc/nginx/conf.d/$domain.d/$app.conf" + sudo systemctl reload nginx + # Create a dedicated php-fpm config # # usage: ynh_add_fpm_config From a006868cbe86d812a5c7530897d6f36cab373cfd Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 28 Jul 2017 19:21:22 +0200 Subject: [PATCH 0388/1066] New helpers ynh_add_systemd_config and ynh_remove_systemd_config (#287) * New helpers ynh_add_systemd_config and ynh_remove_systemd_config Standard file for systemd service. * ynh_subsistute_char was renamed ynh_replace_string * Upgrade helpers * Add some description of which keywords are replaced by which variable --- data/helpers.d/backend | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index a6b14fc84..ca954a838 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -62,13 +62,51 @@ ynh_remove_logrotate () { fi } -# Create a dedicated nginx config +# Create a dedicated systemd config # -# This will use a template in ../conf/nginx.conf +# This will use a template in ../conf/systemd.service # and will replace the following keywords with # global variables that should be defined before calling # this helper : # +# __APP__ by $app +# __FINALPATH__ by $final_path +# +# usage: ynh_add_systemd_config +ynh_add_systemd_config () { + finalsystemdconf="/etc/systemd/system/$app.service" + ynh_backup_if_checksum_is_different "$finalsystemdconf" + sudo cp ../conf/systemd.service "$finalsystemdconf" + + # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. + # Substitute in a nginx config file only if the variable is not empty + if test -n "${final_path:-}"; then + ynh_replace_string "__FINALPATH__" "$final_path" "$finalsystemdconf" + fi + if test -n "${app:-}"; then + ynh_replace_string "__APP__" "$app" "$finalsystemdconf" + fi + ynh_store_file_checksum "$finalsystemdconf" + + sudo chown root: "$finalsystemdconf" + sudo systemctl enable $app + sudo systemctl daemon-reload +} + +# Remove the dedicated systemd config +# +# usage: ynh_remove_systemd_config +ynh_remove_systemd_config () { + finalsystemdconf="/etc/systemd/system/$app.service" + if [ -e "$finalsystemdconf" ]; then + sudo systemctl stop $app + sudo systemctl disable $app + ynh_secure_remove "$finalsystemdconf" + fi + +# Create a dedicated nginx config +# +# This will use a template in ../conf/nginx.conf # __PATH__ by $path_url # __DOMAIN__ by $domain # __PORT__ by $port From 3a777ccbc79c46b3f2741226a60585c2eef5c690 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 28 Jul 2017 14:22:04 -0400 Subject: [PATCH 0389/1066] [microdecision] Renaming ynh_backup_after_failed_upgrade to ynh_restore_upgradebackup --- data/helpers.d/utils | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index bfa6ebbf2..4f3f36c85 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -30,11 +30,11 @@ ynh_get_plain_key() { # usage: # ynh_backup_before_upgrade # ynh_clean_setup () { -# ynh_backup_after_failed_upgrade +# ynh_restore_upgradebackup # } # ynh_abort_if_errors # -ynh_backup_after_failed_upgrade () { +ynh_restore_upgradebackup () { echo "Upgrade failed." >&2 app_bck=${app//_/-} # Replace all '_' by '-' @@ -54,7 +54,7 @@ ynh_backup_after_failed_upgrade () { # usage: # ynh_backup_before_upgrade # ynh_clean_setup () { -# ynh_backup_after_failed_upgrade +# ynh_restore_upgradebackup # } # ynh_abort_if_errors # From 8f173543bb2070dc2d870d6cf2288d04f22dbc61 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 2 Aug 2017 18:44:16 +0200 Subject: [PATCH 0390/1066] Fix syntax error: unexpected end of file (#338) --- data/helpers.d/utils | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 4f3f36c85..44c679471 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -84,6 +84,7 @@ ynh_backup_before_upgrade () { else ynh_die "Backup failed, the upgrade process was aborted." fi +} # Download, check integrity, uncompress and patch the source from app.src # From f4fabe1edbbacadb7d70c7f99fb211b29a9093f8 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 2 Aug 2017 18:44:32 +0200 Subject: [PATCH 0391/1066] Fix syntax error: unexpected end of file (#337) --- data/helpers.d/backend | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index ca954a838..2a91e3566 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -147,6 +147,7 @@ ynh_add_nginx_config () { ynh_remove_nginx_config () { ynh_secure_remove "/etc/nginx/conf.d/$domain.d/$app.conf" sudo systemctl reload nginx +} # Create a dedicated php-fpm config # From 7d5779fcab2af4784f0b1dea8056d68f1f49a4fa Mon Sep 17 00:00:00 2001 From: anmol26s Date: Sat, 5 Aug 2017 15:41:53 +0530 Subject: [PATCH 0392/1066] [enh] Check that user is legitimate to use an email adress when sending mail (#330) --- data/templates/postfix/main.cf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index 0501b56da..bdd364250 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -72,6 +72,7 @@ virtual_alias_domains = virtual_minimum_uid = 100 virtual_uid_maps = static:vmail virtual_gid_maps = static:mail +smtpd_sender_login_maps= ldap:/etc/postfix/ldap-accounts.cf # Dovecot LDA virtual_transport = dovecot @@ -113,7 +114,8 @@ smtpd_helo_restrictions = permit # Requirements for the sender address -smtpd_sender_restrictions = +smtpd_sender_restrictions = + reject_sender_login_mismatch, permit_mynetworks, permit_sasl_authenticated, reject_non_fqdn_sender, From 88a6c3d83a3830b917b421b825ca1589112b3859 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 6 Aug 2017 02:37:17 +0200 Subject: [PATCH 0393/1066] Missing parenthesis in helper :| --- data/helpers.d/backend | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 2a91e3566..c54e82754 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -103,6 +103,7 @@ ynh_remove_systemd_config () { sudo systemctl disable $app ynh_secure_remove "$finalsystemdconf" fi +} # Create a dedicated nginx config # From 3235f5f3fe63257e1bc29bd8182fab0f7517bcb3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 7 Aug 2017 06:58:17 +0200 Subject: [PATCH 0394/1066] [mod] autopep8 --- src/yunohost/app.py | 141 ++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 61326fe3b..aa76cfc7f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -47,12 +47,12 @@ from yunohost.utils import packages logger = getActionLogger('yunohost.app') -REPO_PATH = '/var/cache/yunohost/repo' -APPS_PATH = '/usr/share/yunohost/apps' +REPO_PATH = '/var/cache/yunohost/repo' +APPS_PATH = '/usr/share/yunohost/apps' APPS_SETTING_PATH = '/etc/yunohost/apps/' -INSTALL_TMP = '/var/cache/yunohost' -APP_TMP_FOLDER = INSTALL_TMP + '/from_file' -APPSLISTS_JSON = '/etc/yunohost/appslists.json' +INSTALL_TMP = '/var/cache/yunohost' +APP_TMP_FOLDER = INSTALL_TMP + '/from_file' +APPSLISTS_JSON = '/etc/yunohost/appslists.json' re_github_repo = re.compile( r'^(http[s]?://|git@)github.com[/:]' @@ -362,7 +362,7 @@ def app_info(app, show_status=False, raw=False): 'license': manifest.get('license', m18n.n('license_undefined')), # FIXME: Temporarly allow undefined version 'version': manifest.get('version', '-'), - #TODO: Add more info + # TODO: Add more info } if show_status: info['status'] = status @@ -386,7 +386,7 @@ def app_map(app=None, raw=False, user=None): if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) - apps = [app,] + apps = [app, ] else: apps = os.listdir(APPS_SETTING_PATH) @@ -397,11 +397,11 @@ def app_map(app=None, raw=False, user=None): if 'domain' not in app_settings: continue if user is not None: - if ('mode' not in app_settings \ - or ('mode' in app_settings \ - and app_settings['mode'] == 'private')) \ - and 'allowed_users' in app_settings \ - and user not in app_settings['allowed_users'].split(','): + if ('mode' not in app_settings + or ('mode' in app_settings + and app_settings['mode'] == 'private')) \ + and 'allowed_users' in app_settings \ + and user not in app_settings['allowed_users'].split(','): continue domain = app_settings['domain'] @@ -578,8 +578,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Check requirements _check_manifest_requirements(manifest) - app_setting_path = APPS_SETTING_PATH +'/'+ app_instance_name - + app_setting_path = APPS_SETTING_PATH + '/' + app_instance_name + # Retrieve current app status status = _get_app_status(app_instance_name) status['remote'] = manifest.get('remote', None) @@ -610,8 +610,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Clean hooks and add new ones hook_remove(app_instance_name) if 'hooks' in os.listdir(extracted_app_folder): - for hook in os.listdir(extracted_app_folder +'/hooks'): - hook_add(app_instance_name, extracted_app_folder +'/hooks/'+ hook) + for hook in os.listdir(extracted_app_folder + '/hooks'): + hook_add(app_instance_name, extracted_app_folder + '/hooks/' + hook) # Store app status with open(app_setting_path + '/status.json', 'w+') as f: @@ -632,6 +632,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) + def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -646,8 +647,10 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): from yunohost.hook import hook_add, hook_remove, hook_exec # Fetch or extract sources - try: os.listdir(INSTALL_TMP) - except OSError: os.makedirs(INSTALL_TMP) + try: + os.listdir(INSTALL_TMP) + except OSError: + os.makedirs(INSTALL_TMP) status = { 'installed_at': int(time.time()), @@ -676,7 +679,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 - if instance_number > 1 : + if instance_number > 1: if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']): raise MoulinetteError(errno.EEXIST, m18n.n('app_already_installed', app=app_id)) @@ -714,7 +717,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): app_settings['install_time'] = status['installed_at'] _set_app_settings(app_instance_name, app_settings) - os.system('chown -R admin: '+ extracted_app_folder) + os.system('chown -R admin: ' + extracted_app_folder) # Execute App install script os.system('chown -hR admin: %s' % INSTALL_TMP) @@ -763,8 +766,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Clean hooks and add new ones hook_remove(app_instance_name) if 'hooks' in os.listdir(extracted_app_folder): - for file in os.listdir(extracted_app_folder +'/hooks'): - hook_add(app_instance_name, extracted_app_folder +'/hooks/'+ file) + for file in os.listdir(extracted_app_folder + '/hooks'): + hook_add(app_instance_name, extracted_app_folder + '/hooks/' + file) # Store app status with open(app_setting_path + '/status.json', 'w+') as f: @@ -797,10 +800,11 @@ def app_remove(auth, app): app_setting_path = APPS_SETTING_PATH + app - #TODO: display fail messages from script + # TODO: display fail messages from script try: shutil.rmtree('/tmp/yunohost_remove') - except: pass + except: + pass os.system('cp -a %s /tmp/yunohost_remove && chown -hR admin: /tmp/yunohost_remove' % app_setting_path) os.system('chown -R admin: /tmp/yunohost_remove') @@ -817,7 +821,8 @@ def app_remove(auth, app): if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) - if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) + if os.path.exists(app_setting_path): + shutil.rmtree(app_setting_path) shutil.rmtree('/tmp/yunohost_remove') hook_remove(app) app_ssowatconf(auth) @@ -840,9 +845,9 @@ def app_addaccess(auth, apps, users=[]): if not users: users = user_list(auth)['users'].keys() elif not isinstance(users, list): - users = [users,] + users = [users, ] if not isinstance(apps, list): - apps = [apps,] + apps = [apps, ] for app in apps: app_settings = _get_app_settings(app) @@ -875,7 +880,7 @@ def app_addaccess(auth, apps, users=[]): app_ssowatconf(auth) - return { 'allowed_users': result } + return {'allowed_users': result} def app_removeaccess(auth, apps, users=[]): @@ -896,9 +901,9 @@ def app_removeaccess(auth, apps, users=[]): if not users: remove_all = True elif not isinstance(users, list): - users = [users,] + users = [users, ] if not isinstance(apps, list): - apps = [apps,] + apps = [apps, ] for app in apps: app_settings = _get_app_settings(app) @@ -926,7 +931,7 @@ def app_removeaccess(auth, apps, users=[]): app_ssowatconf(auth) - return { 'allowed_users': result } + return {'allowed_users': result} def app_clearaccess(auth, apps): @@ -939,7 +944,8 @@ def app_clearaccess(auth, apps): """ from yunohost.hook import hook_callback - if not isinstance(apps, list): apps = [apps] + if not isinstance(apps, list): + apps = [apps] for app in apps: app_settings = _get_app_settings(app) @@ -971,12 +977,12 @@ def app_debug(app): 'name': manifest['id'], 'label': manifest['name'], 'services': [{ - "name": x, - "logs": [{ - "file_name": y, - "file_content": "\n".join(z), - } for (y, z) in sorted(service_log(x).items(), key=lambda x: x[0])], - } for x in sorted(manifest.get("services", []))] + "name": x, + "logs": [{ + "file_name": y, + "file_content": "\n".join(z), + } for (y, z) in sorted(service_log(x).items(), key=lambda x: x[0])], + } for x in sorted(manifest.get("services", []))] } @@ -993,7 +999,7 @@ def app_makedefault(auth, app, domain=None): app_settings = _get_app_settings(app) app_domain = app_settings['domain'] - app_path = app_settings['path'] + app_path = app_settings['path'] if domain is None: domain = app_domain @@ -1016,7 +1022,7 @@ def app_makedefault(auth, app, domain=None): if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} - ssowat_conf['redirected_urls'][domain +'/'] = app_domain + app_path + ssowat_conf['redirected_urls'][domain + '/'] = app_domain + app_path try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: @@ -1025,7 +1031,6 @@ def app_makedefault(auth, app, domain=None): raise MoulinetteError(errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) - os.system('chmod 644 /etc/ssowat/conf.json.persistent') logger.success(m18n.n('ssowat_conf_updated')) @@ -1055,7 +1060,7 @@ def app_setting(app, key, value=None, delete=False): del app_settings[key] else: # FIXME: Allow multiple values for some keys? - if key in ['redirected_urls','redirected_regex']: + if key in ['redirected_urls', 'redirected_regex']: value = yaml.load(value) app_settings[key] = value _set_app_settings(app, app_settings) @@ -1221,8 +1226,8 @@ def app_ssowatconf(auth): unprotected_regex = [] protected_urls = [] protected_regex = [] - redirected_regex = { main_domain +'/yunohost[\/]?$': 'https://'+ main_domain +'/yunohost/sso/' } - redirected_urls ={} + redirected_regex = {main_domain + '/yunohost[\/]?$': 'https://' + main_domain + '/yunohost/sso/'} + redirected_urls = {} try: apps_list = app_list()['apps'] @@ -1235,7 +1240,7 @@ def app_ssowatconf(auth): for app in apps_list: if _is_installed(app['id']): - with open(APPS_SETTING_PATH + app['id'] +'/settings.yml') as f: + with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f: app_settings = yaml.load(f) for item in _get_setting(app_settings, 'skipped_uris'): if item[-1:] == '/': @@ -1354,7 +1359,7 @@ def _get_app_status(app_id, format_date=False): status = { 'installed_at': app_setting(app_id, 'install_time'), 'upgraded_at': app_setting(app_id, 'update_time'), - 'remote': { 'type': None }, + 'remote': {'type': None}, } with open(app_setting_path + '/status.json', 'w+') as f: json.dump(status, f) @@ -1384,20 +1389,23 @@ def _extract_app_from_file(path, remove=False): """ logger.info(m18n.n('extracting')) - if os.path.exists(APP_TMP_FOLDER): shutil.rmtree(APP_TMP_FOLDER) + if os.path.exists(APP_TMP_FOLDER): + shutil.rmtree(APP_TMP_FOLDER) os.makedirs(APP_TMP_FOLDER) path = os.path.abspath(path) if ".zip" in path: extract_result = os.system('unzip %s -d %s > /dev/null 2>&1' % (path, APP_TMP_FOLDER)) - if remove: os.remove(path) + if remove: + os.remove(path) elif ".tar" in path: extract_result = os.system('tar -xf %s -C %s > /dev/null 2>&1' % (path, APP_TMP_FOLDER)) - if remove: os.remove(path) + if remove: + os.remove(path) elif os.path.isdir(path): shutil.rmtree(APP_TMP_FOLDER) - if path[len(path)-1:] != '/': + if path[len(path) - 1:] != '/': path = path + '/' extract_result = os.system('cp -a "%s" %s' % (path, APP_TMP_FOLDER)) else: @@ -1410,7 +1418,7 @@ def _extract_app_from_file(path, remove=False): extracted_app_folder = APP_TMP_FOLDER if len(os.listdir(extracted_app_folder)) == 1: for folder in os.listdir(extracted_app_folder): - extracted_app_folder = extracted_app_folder +'/'+ folder + extracted_app_folder = extracted_app_folder + '/' + folder with open(extracted_app_folder + '/manifest.json') as json_manifest: manifest = json.loads(str(json_manifest.read())) manifest['lastUpdate'] = int(time.time()) @@ -1492,7 +1500,7 @@ def _fetch_app_from_git(app): tree_index = url.rfind('/tree/') if tree_index > 0: url = url[:tree_index] - branch = app[tree_index+6:] + branch = app[tree_index + 6:] try: # We use currently git 2.1 so we can't use --shallow-submodules # option. When git will be in 2.9 (with the new debian version) @@ -1502,8 +1510,8 @@ def _fetch_app_from_git(app): 'git', 'clone', '--depth=1', '--recursive', url, extracted_app_folder]) subprocess.check_call([ - 'git', 'reset', '--hard', branch - ], cwd=extracted_app_folder) + 'git', 'reset', '--hard', branch + ], cwd=extracted_app_folder) with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: @@ -1519,7 +1527,8 @@ def _fetch_app_from_git(app): manifest['remote'] = {'type': 'git', 'url': url, 'branch': branch} try: revision = _get_git_last_commit_hash(url, branch) - except: pass + except: + pass else: manifest['remote']['revision'] = revision else: @@ -1557,9 +1566,9 @@ def _fetch_app_from_git(app): 'git', 'clone', app_info['git']['url'], '-b', app_info['git']['branch'], extracted_app_folder]) subprocess.check_call([ - 'git', 'reset', '--hard', - str(app_info['git']['revision']) - ], cwd=extracted_app_folder) + 'git', 'reset', '--hard', + str(app_info['git']['revision']) + ], cwd=extracted_app_folder) with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: @@ -1712,6 +1721,7 @@ def _check_manifest_requirements(manifest): pkgname=pkgname, version=version, spec=spec)) + def _parse_args_from_manifest(manifest, action, args={}, auth=None): """Parse arguments needed for an action from the manifest @@ -1861,6 +1871,7 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): return args_dict + def _make_environment_dict(args_dict): """ Convert a dictionnary containing manifest arguments @@ -1872,9 +1883,10 @@ def _make_environment_dict(args_dict): """ env_dict = {} for arg_name, arg_value in args_dict.items(): - env_dict[ "YNH_APP_ARG_%s" % arg_name.upper() ] = arg_value + env_dict["YNH_APP_ARG_%s" % arg_name.upper()] = arg_value return env_dict + def _parse_app_instance_name(app_instance_name): """ Parse a Yunohost app instance name and extracts the original appid @@ -2017,9 +2029,9 @@ def _write_appslist_list(appslist_lists): with open(APPSLISTS_JSON, "w") as f: json.dump(appslist_lists, f) except Exception as e: - raise MoulinetteError(errno.EIO, - "Error while writing list of appslist %s: %s" % - (APPSLISTS_JSON, str(e))) + raise MoulinetteError(errno.EIO, + "Error while writing list of appslist %s: %s" % + (APPSLISTS_JSON, str(e))) def _register_new_appslist(url, name): @@ -2068,7 +2080,7 @@ def is_true(arg): if isinstance(arg, bool): return arg elif isinstance(arg, basestring): - true_list = ['yes', 'Yes', 'true', 'True' ] + true_list = ['yes', 'Yes', 'true', 'True'] for string in true_list: if arg == string: return True @@ -2086,7 +2098,8 @@ def random_password(length=8): length -- The string length to generate """ - import string, random + import string + import random char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase return ''.join(random.sample(char_set, length)) From 535f36b1c18451b9c6eeef23a177c64a593396b5 Mon Sep 17 00:00:00 2001 From: opi Date: Mon, 7 Aug 2017 15:29:53 +0200 Subject: [PATCH 0395/1066] [fix] Properly catch Invalid manifest json with ValueError. (#324) * [fix] Properly catch Invalid manifest json with ValueError. * [fix] display detailed error in every case for invalid manifest --- locales/en.json | 2 +- src/yunohost/app.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index e4f785002..50ef2ddf1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -20,7 +20,7 @@ "app_location_already_used": "An app is already installed in this location", "app_location_install_failed": "Unable to install the app in this location", "app_location_unavailable": "This url is not available or conflicts with an already installed app", - "app_manifest_invalid": "Invalid app manifest", + "app_manifest_invalid": "Invalid app manifest: {error}", "app_no_upgrade": "No app to upgrade", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", "app_not_installed": "{app:s} is not installed", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index aa76cfc7f..bb81efeeb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1424,6 +1424,9 @@ def _extract_app_from_file(path, remove=False): manifest['lastUpdate'] = int(time.time()) except IOError: raise MoulinetteError(errno.EIO, m18n.n('app_install_files_invalid')) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_manifest_invalid', error=e.strerror)) logger.info(m18n.n('done')) @@ -1517,9 +1520,9 @@ def _fetch_app_from_git(app): except subprocess.CalledProcessError: raise MoulinetteError(errno.EIO, m18n.n('app_sources_fetch_failed')) - except IOError: + except ValueError as e: raise MoulinetteError(errno.EIO, - m18n.n('app_manifest_invalid')) + m18n.n('app_manifest_invalid', error=e.strerror)) else: logger.info(m18n.n('done')) @@ -1574,9 +1577,9 @@ def _fetch_app_from_git(app): except subprocess.CalledProcessError: raise MoulinetteError(errno.EIO, m18n.n('app_sources_fetch_failed')) - except IOError: + except ValueError as e: raise MoulinetteError(errno.EIO, - m18n.n('app_manifest_invalid')) + m18n.n('app_manifest_invalid', error=e.strerror)) else: logger.info(m18n.n('done')) From 36770b0edad7016893c8a844792eb7d8daebb7ef Mon Sep 17 00:00:00 2001 From: e-lie <1651436+e-lie@users.noreply.github.com> Date: Mon, 7 Aug 2017 15:30:25 +0200 Subject: [PATCH 0396/1066] [fix] No default backup method (redmine 968) (#339) --- src/yunohost/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 2ddc292f2..9bed264c6 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2003,7 +2003,7 @@ def backup_create(name=None, description=None, methods=[], m18n.n('backup_output_directory_required')) # Define methods (retro-compat) - if methods == []: + if not methods: if no_compress: methods = ['copy'] else: From a441f374546b960335ffd1282f8defb01df228cb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 7 Aug 2017 15:55:18 +0200 Subject: [PATCH 0397/1066] Migration framework (#195) * [enh] list migrations * [enh] first version of the migrate command * [mod] add todo comment * [mod] migrate command shouldn't return anything * [mod] rename yunohost_migrations to data_migrations * [mod] better regex * [enh] had base class for migration * [fix] inverted condition * [enh] save last runned migration * [enh] add migrations state command * [mod] add todo comments * [mod] error handling * [mod] DRY * [doc] more comment * [enh] handle exceptions on migration * [mod] error handling * [mod] DRY * [enh] error handling * [mod] this is done earlier * [doc] docstring * [enh] handle fail to load migration case * [doc] add TODO Comment * [fix] typos, thx ju * [enh] add a migration to remove archivemount (as an example) * [fix] check_call is boring * [enh] support forward/backward migrations * [mod] I don't need auth * [fix] apt is expecting input... * [mod] save it as int * [mod] add some logging * [doc] update todo * [fix] I also need to run backward latest runed migration * [enh] add target cli argument * [enh] fake migration * [enh] uniformly convert to int at the same place * [fix] we need to filename now * [enh] validate input * [enh] handle 0 special case * [mod] rename fake to skip * [mod] s/runed/run/g * [doc] anglich typo in comments * [mod] more explicit error message * [mod] more typo * [doc] put comment in the right place * [mod] typo * [fix] forgot to cape migrations by target * [fix] typo * [mod] uses moulinette helpers * [enh] launch migrations during package upgrade * [mod] remove unused import * [mod] sort translation keys * [enh] i18n * [fix] missing __init__.py in data_migrations * [mod] move to a subcategory * Typo / caps / consistency * [fix] forgot that migrations is now in tools, in postinst * Skip migrations during postinstall * Remove archivemount example migration It relied on apt-get, which can't be used during 'postinst' debian scripts because we're already inside a apt * Add migration for cert group from 'metronome' to 'ssl-cert' --- data/actionsmap/yunohost.yml | 31 +++ debian/postinst | 3 + locales/en.json | 126 ++++++------ .../0001_change_cert_group_to_sslcert.py | 17 ++ src/yunohost/data_migrations/__init__.py | 0 src/yunohost/tools.py | 188 ++++++++++++++++++ 6 files changed, 308 insertions(+), 57 deletions(-) create mode 100644 src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py create mode 100644 src/yunohost/data_migrations/__init__.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index c6f8f2458..af072c1bc 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1478,6 +1478,37 @@ tools: extra: pattern: *pattern_port + subcategories: + + migrations: + subcategory_help: Manage migrations + actions: + + ### tools_migrations_list() + list: + action_help: List migrations + api: GET /migrations + + ### tools_migrations_migrate() + migrate: + action_help: Perform migrations + api: POST /migrations/migrate + arguments: + -t: + help: target migration number (or 0), latest one by default + type: int + full: --target + -s: + help: skip the migration(s), use it only if you know what you are doing + full: --skip + action: store_true + + + ### tools_migrations_state() + state: + action_help: Show current migrations state + api: GET /migrations/state + ############################# # Hook # diff --git a/debian/postinst b/debian/postinst index 124657a10..7e91ffbb3 100644 --- a/debian/postinst +++ b/debian/postinst @@ -14,6 +14,9 @@ do_configure() { echo "Regenerating configuration, this might take a while..." yunohost service regen-conf --output-as none + echo "Launching migrations.." + yunohost tools migrations migrate + # restart yunohost-firewall if it's running service yunohost-firewall status >/dev/null \ && restart_yunohost_firewall \ diff --git a/locales/en.json b/locales/en.json index 50ef2ddf1..a59f87b4d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -5,6 +5,7 @@ "admin_password_changed": "The administration password has been changed", "app_already_installed": "{app:s} is already installed", "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", + "app_already_up_to_date": "{app:s} is already up to date", "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", "app_argument_required": "Argument '{name:s}' is required", @@ -35,17 +36,16 @@ "app_unsupported_remote_type": "Unsupported remote type used for the app", "app_upgrade_failed": "Unable to upgrade {app:s}", "app_upgraded": "{app:s} has been upgraded", - "app_already_up_to_date": "{app:s} is already up to date", - "appslist_fetched": "The application list {appslist:s} has been fetched", - "appslist_removed": "The application list {appslist:s} has been removed", - "appslist_unknown": "Application list {appslist:s} unknown.", - "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", - "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", - "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", - "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", - "appslist_migrating": "Migrating application list {appslist:s} ...", - "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", + "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", + "appslist_fetched": "The application list {appslist:s} has been fetched", + "appslist_migrating": "Migrating application list {appslist:s} ...", + "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", + "appslist_removed": "The application list {appslist:s} has been removed", + "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", + "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", + "appslist_unknown": "Application list {appslist:s} unknown.", + "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", "ask_current_admin_password": "Current administration password", "ask_email": "Email address", "ask_firstname": "First name", @@ -57,51 +57,75 @@ "backup_abstract_method": "This backup method hasn't yet been implemented", "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", - "backup_applying_method_tar": "Creating the backup tar archive...", - "backup_applying_method_copy": "Copying all files to backup...", "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", + "backup_applying_method_copy": "Copying all files to backup...", "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", + "backup_applying_method_tar": "Creating the backup tar archive...", "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", - "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", + "backup_archive_mount_failed": "Mounting the backup archive failed", "backup_archive_name_exists": "The backup's archive name already exists", "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", "backup_archive_open_failed": "Unable to open the backup archive", - "backup_archive_mount_failed": "Mounting the backup archive failed", + "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", "backup_ask_for_copying_if_needed": "Your system don't support completely the quick method to organize files in the archive, do you want to organize its by copying {size:s}MB?", "backup_borg_not_implemented": "Borg backup method is not yet implemented", "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", - "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", + "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", "backup_created": "Backup created", "backup_creating_archive": "Creating the backup archive...", "backup_creation_failed": "Backup creation failed", - "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", "backup_csv_addition_failed": "Unable to add files to backup into the CSV file", - "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", + "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", "backup_custom_backup_error": "Custom backup method failure on 'backup' step", "backup_custom_mount_error": "Custom backup method failure on 'mount' step", + "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", "backup_delete_error": "Unable to delete '{path:s}'", "backup_deleted": "The backup has been deleted", "backup_extracting_archive": "Extracting the backup archive...", "backup_hook_unknown": "Backup hook '{hook:s}' unknown", "backup_invalid_archive": "Invalid backup archive", + "backup_method_borg_finished": "Backup into borg finished", + "backup_method_copy_finished": "Backup copy finished", + "backup_method_custom_finished": "Custom backup method '{method:s}' finished", + "backup_method_tar_finished": "Backup tar archive created", "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", "backup_nothings_done": "There is nothing to save", - "backup_method_tar_finished": "Backup tar archive created", - "backup_method_copy_finished": "Backup copy finished", - "backup_method_borg_finished": "Backup into borg finished", - "backup_method_custom_finished": "Custom backup method '{method:s}' finished", "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", "backup_system_part_failed": "Unable to backup the '{part:s}' system part", - "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", + "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.", "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.", + "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", + "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", + "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_signing_failed": "Signing the new certificate failed", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", + "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", + "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", @@ -111,6 +135,8 @@ "diagnosis_monitor_system_error": "Can't monitor system: {error}", "diagnosis_no_apps": "No installed application", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", + "domain_cert_gen_failed": "Unable to generate certificate", "domain_created": "The domain has been created", "domain_creation_failed": "Unable to create domain", "domain_deleted": "The domain has been deleted", @@ -124,8 +150,8 @@ "domain_unknown": "Unknown domain", "domain_zone_exists": "DNS zone file already exists", "domain_zone_not_found": "DNS zone file not found for domain {:s}", - "done": "Done", "domains_available": "Available domains:", + "done": "Done", "downloading": "Downloading...", "dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", @@ -154,13 +180,13 @@ "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'", "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", "global_settings_setting_example_bool": "Example boolean option", + "global_settings_setting_example_enum": "Example enum option", "global_settings_setting_example_int": "Example int option", "global_settings_setting_example_string": "Example string option", - "global_settings_setting_example_enum": "Example enum option", - "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", + "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", "hook_exec_failed": "Script execution failed: {path:s}", - "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", + "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", "hook_list_by_invalid": "Invalid property to list hook by", "hook_name_unknown": "Unknown hook name '{name:s}'", "installation_complete": "Installation complete", @@ -168,8 +194,8 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", - "ldap_initialized": "LDAP has been initialized", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", + "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", @@ -177,6 +203,18 @@ "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", + "migrations_backward": "Migrating backward.", + "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", + "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", + "migrations_current_target": "Migration target is {}", + "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", + "migrations_forward": "Migrating forward", + "migrations_loading_migration": "Loading migration {number} {name}...", + "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", + "migrations_no_migrations_to_run": "No migrations to run", + "migrations_show_currently_running_migration": "Running migration {number} {name}...", + "migrations_show_last_migration": "Last ran migration is {}", + "migrations_skip_migration": "Skipping migration {number} {name}...", "monitor_disabled": "The server monitoring has been disabled", "monitor_enabled": "The server monitoring has been enabled", "monitor_glances_con_failed": "Unable to connect to Glances server", @@ -224,17 +262,17 @@ "restore_action_required": "You must specify something to restore", "restore_already_installed_app": "An app is already installed with the id '{app:s}'", "restore_app_failed": "Unable to restore the app '{app:s}'", - "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", "restore_complete": "Restore complete", "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", "restore_extracting": "Extracting needed files from the archive...", "restore_failed": "Unable to restore the system", "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", - "restore_mounting_archive": "Mounting archive into '{path:s}'", "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_mounting_archive": "Mounting archive into '{path:s}'", "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", "restore_nothings_done": "Nothing has been restored", + "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", @@ -245,13 +283,13 @@ "service_cmd_exec_failed": "Unable to execute command '{command:s}'", "service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'", "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'", + "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created", "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", "service_conf_file_removed": "The configuration file '{conf}' has been removed", "service_conf_file_updated": "The configuration file '{conf}' has been updated", "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", - "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", @@ -304,31 +342,5 @@ "yunohost_ca_creation_success": "The local certification authority has been created.", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", - "domain_cert_gen_failed": "Unable to generate certificate", - "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", - "certmanager_domain_unknown": "Unknown domain {domain:s}", - "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", - "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", - "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", - "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", - "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", - "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", - "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", - "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", - "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", - "certmanager_cert_signing_failed": "Signing the new certificate failed", - "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", - "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", - "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", - "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", - "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", - "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", - "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})" + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" } diff --git a/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py b/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py new file mode 100644 index 000000000..cd39df9fa --- /dev/null +++ b/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py @@ -0,0 +1,17 @@ +import subprocess +import glob +from yunohost.tools import Migration +from moulinette.utils.filesystem import chown + +class MyMigration(Migration): + "Change certificates group permissions from 'metronome' to 'ssl-cert'" + + all_certificate_files = glob.glob("/etc/yunohost/certs/*/*.pem") + + def forward(self): + for filename in self.all_certificate_files: + chown(filename, uid="root", gid="ssl-cert") + + def backward(self): + for filename in self.all_certificate_files: + chown(filename, uid="root", gid="metronome") diff --git a/src/yunohost/data_migrations/__init__.py b/src/yunohost/data_migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 0183aebd2..6dc3fcba4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -23,6 +23,7 @@ Specific tools """ +import re import os import yaml import requests @@ -33,6 +34,7 @@ import subprocess import pwd import socket from collections import OrderedDict +from importlib import import_module import apt import apt.progress @@ -40,6 +42,7 @@ import apt.progress from moulinette import msettings, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json, write_to_json from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import dyndns_subscribe @@ -50,6 +53,7 @@ from yunohost.utils.packages import ynh_packages_version # FIXME this is a duplicate from apps.py APPS_SETTING_PATH= '/etc/yunohost/apps/' +MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations_state.json" logger = getActionLogger('yunohost.tools') @@ -373,6 +377,9 @@ def tools_postinstall(domain, password, ignore_dyndns=False): _install_appslist_fetch_cron() + # Init migrations (skip them, no need to run them on a fresh system) + tools_migrations_migrate(skip=True) + os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time @@ -623,3 +630,184 @@ def tools_port_available(port): return True else: return False + + +def tools_migrations_list(): + """ + List existing migrations + """ + + migrations = {"migrations": []} + + for migration in _get_migrations_list(): + migrations["migrations"].append({ + "number": int(migration.split("_", 1)[0]), + "name": migration.split("_", 1)[1], + "file_name": migration, + }) + + return migrations + + +def tools_migrations_migrate(target=None, skip=False): + """ + Perform migrations + """ + + # state is a datastructure that represents the last run migration + # it has this form: + # { + # "last_run_migration": { + # "number": "00xx", + # "name": "some name", + # } + # } + state = tools_migrations_state() + + last_run_migration_number = state["last_run_migration"]["number"] if state["last_run_migration"] else 0 + + migrations = [] + + # loading all migrations + for migration in tools_migrations_list()["migrations"]: + logger.debug(m18n.n('migrations_loading_migration', + number=migration["number"], + name=migration["name"], + )) + + try: + # this is python builtin method to import a module using a name, we + # use that to import the migration as a python object so we'll be + # able to run it in the next loop + module = import_module("yunohost.data_migrations.{file_name}".format(**migration)) + except Exception: + import traceback + traceback.print_exc() + + raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', + number=migration["number"], + name=migration["name"], + )) + break + + migrations.append({ + "number": migration["number"], + "name": migration["name"], + "module": module, + }) + + migrations = sorted(migrations, key=lambda x: x["number"]) + + if not migrations: + logger.info(m18n.n('migrations_no_migrations_to_run')) + return + + all_migration_numbers = [x["number"] for x in migrations] + + if target is None: + target = migrations[-1]["number"] + + # validate input, target must be "0" or a valid number + elif target != 0 and target not in all_migration_numbers: + raise MoulinetteError(errno.EINVAL, m18n.n('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers)))) + + logger.debug(m18n.n('migrations_current_target', target)) + + # no new migrations to run + if target == last_run_migration_number: + logger.warn(m18n.n('migrations_no_migrations_to_run')) + return + + logger.debug(m18n.n('migrations_show_last_migration', last_run_migration_number)) + + # we need to run missing migrations + if last_run_migration_number < target: + logger.debug(m18n.n('migrations_forward')) + # drop all already run migrations + migrations = filter(lambda x: target >= x["number"] > last_run_migration_number, migrations) + mode = "forward" + + # we need to go backward on already run migrations + elif last_run_migration_number > target: + logger.debug(m18n.n('migrations_backward')) + # drop all not already run migrations + migrations = filter(lambda x: target < x["number"] <= last_run_migration_number, migrations) + mode = "backward" + + else: # can't happen, this case is handle before + raise Exception() + + # effectively run selected migrations + for migration in migrations: + if not skip: + logger.warn(m18n.n('migrations_show_currently_running_migration', **migration)) + + try: + if mode == "forward": + migration["module"].MyMigration().migrate() + elif mode == "backward": + migration["module"].MyMigration().backward() + else: # can't happen + raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) + except Exception as e: + # migration failed, let's stop here but still update state because + # we managed to run the previous ones + logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) + break + + else: # if skip + logger.warn(m18n.n('migrations_skip_migration', **migration)) + + # update the state to include the latest run migration + state["last_run_migration"] = { + "number": migration["number"], + "name": migration["name"], + } + + # special case where we want to go back from the start + if target == 0: + state["last_run_migration"] = None + + write_to_json(MIGRATIONS_STATE_PATH, state) + + +def tools_migrations_state(): + """ + Show current migration state + """ + if not os.path.exists(MIGRATIONS_STATE_PATH): + return {"last_run_migration": None} + + return read_json(MIGRATIONS_STATE_PATH) + + +def _get_migrations_list(): + migrations = [] + + try: + import data_migrations + except ImportError: + # not data migrations present, return empty list + return migrations + + migrations_path = data_migrations.__path__[0] + + if not os.path.exists(migrations_path): + logger.warn(m18n.n('migrations_cant_reach_migration_file', migrations_path)) + return migrations + + for migration in filter(lambda x: re.match("^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)): + migrations.append(migration[:-len(".py")]) + + return sorted(migrations) + + +class Migration(object): + def migrate(self): + self.forward() + + def forward(self): + raise NotImplementedError() + + def backward(self): + pass From d6d4717bb23f5cf43d6984d400c338c40ef277df Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Aug 2017 12:16:31 -0400 Subject: [PATCH 0398/1066] [fix] Tests were broken from recent changes about m18n --- src/yunohost/tests/conftest.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 65c1d3ace..6958ae679 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -7,18 +7,6 @@ sys.path.append("..") def pytest_addoption(parser): parser.addoption("--yunodebug", action="store_true", default=False) -############################################################################### -# Tweak moulinette init to have yunohost namespace # -############################################################################### - - -old_init = moulinette.core.Moulinette18n.__init__ -def monkey_path_i18n_init(self, package, default_locale="en"): - old_init(self, package, default_locale) - self.load_namespace("yunohost") -moulinette.core.Moulinette18n.__init__ = monkey_path_i18n_init - - ############################################################################### # Tweak translator to raise exceptions if string keys are not defined # ############################################################################### @@ -34,9 +22,9 @@ def new_translate(self, key, *args, **kwargs): moulinette.core.Translator.translate = new_translate def new_m18nn(self, key, *args, **kwargs): - return self._namespace.translate(key, *args, **kwargs) -moulinette.core.Moulinette18n.n = new_m18nn + return self._namespaces[self._current_namespace].translate(key, *args, **kwargs) +moulinette.core.Moulinette18n.n = new_m18nn ############################################################################### # Init the moulinette to have the cli loggers stuff # @@ -105,3 +93,4 @@ def pytest_cmdline_main(config): # Initialize moulinette moulinette.init(logging_config=logging, _from_source=False) + moulinette.m18n.load_namespace('yunohost') From 7054f8a5bfa0603486a6b51ca1b7f0778487d27f Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Mon, 7 Aug 2017 18:23:53 +0200 Subject: [PATCH 0399/1066] Update from Weblate. (#340) * Added translation using Weblate (Russian) * [i18n] Translated using Weblate (Russian) Currently translated at 2.4% (8 of 332 strings) --- locales/ru.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 locales/ru.json diff --git a/locales/ru.json b/locales/ru.json new file mode 100644 index 000000000..2658446bc --- /dev/null +++ b/locales/ru.json @@ -0,0 +1,10 @@ +{ + "action_invalid": "Неверное действие '{action:s}'", + "admin_password": "Пароль администратора", + "admin_password_change_failed": "Невозможно изменить пароль", + "admin_password_changed": "Пароль администратора был изменен", + "app_already_installed": "{app:s} уже установлено", + "app_already_installed_cant_change_url": "Это приложение уже установлено. URL не может быть изменен только с помощью этой функции. Изучите `app changeurl`, если это доступно.", + "app_argument_choice_invalid": "Неверный выбор для аргумента '{name:s}', Это должно быть '{choices:s}'", + "app_argument_invalid": "Недопустимое значение аргумента '{name:s}': {error:s}'" +} From 0d6a0d6f7d1458ed4879b2c371864b9c69bceeae Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Aug 2017 13:28:33 -0400 Subject: [PATCH 0400/1066] Update changelog for 2.7.0 release --- debian/changelog | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0c99f1091..305638f7e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,40 @@ +yunohost (2.7.0) testing; urgency=low + +Thanks to all contributors <3 ! (Bram, Maniack C, ljf, Aleks, JimboJoe, anmol26s, e-lie, Ozhiganov) + +Major fixes / improvements +========================== + + * [enh] Add a migration framework (#195) + * [enh] Remove m18n (and other globals) black magic (#336) + * [fix] Refactor DNS conf management for domains (#299) + * [enh] Support custom backup methods (#326) + +App helpers +=========== + + * New helper autopurge (#321) + * New helpers ynh_add_fpm_config and ynh_remove_fpm_config (#284) + * New helpers ynh_restore_upgradebackup and ynh_backup_before_upgrade (#289) + * New helpers ynh_add_nginx_config and ynh_remove_nginx_config (#285) + * New helpers ynh_add_systemd_config and ynh_remove_systemd_config (#287) + +Smaller fixes / improvements +============================ + + * [fix] Run change_url scripts as root as a matter of homogeneity (#329) + * [fix] Don't verify SSL during changeurl tests :/ (#332) + * [fix] Depreciation warning for --hooks was always shown (#333) + * [fix] Logrotate append (#328) + * [enh] Check that url is available and normalize path before app install (#304) + * [enh] Check that user is legitimate to use an email adress when sending mail (#330) + * [fix] Properly catch Invalid manifest json with ValueError. (#324) + * [fix] No default backup method (redmine 968) (#339) + * [enh] Add a script to test m18n keys usage (#308) + * [i18] Started russian translation (#340) + + -- Alexandre Aubin Mon, 07 Aug 2017 13:16:08 -0400 + yunohost (2.6.5) stable; urgency=low Minor fix From 31105b8c8627c5318d38333a4d317b1cd996a67a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 8 Aug 2017 13:24:16 +0200 Subject: [PATCH 0401/1066] [fix] timeout on request to avoid blocking process --- src/yunohost/dyndns.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 27aeedd31..593c765e6 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -184,11 +184,14 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, try: # Check if domain is registered if requests.get('https://{0}/test/{1}'.format( - dyn_host, _domain)).status_code == 200: + dyn_host, _domain), timeout=30).status_code == 200: continue except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + except requests.exceptions.Timeout: + logger.warning("Correction timed out on {}, skip it".format( + 'https://{0}/test/{1}'.format(dyn_host, _domain))) domain = _domain key = path break From a5331063cc5bf5096a261e352af100f254914617 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Aug 2017 16:12:35 +0200 Subject: [PATCH 0402/1066] Put request url in an intermediate variable --- src/yunohost/dyndns.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 593c765e6..621043c3e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -183,15 +183,15 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, try: # Check if domain is registered - if requests.get('https://{0}/test/{1}'.format( - dyn_host, _domain), timeout=30).status_code == 200: + request_url = 'https://{0}/test/{1}'.format(dyn_host, _domain) + if requests.get(request_url, timeout=30).status_code == 200: continue except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) except requests.exceptions.Timeout: logger.warning("Correction timed out on {}, skip it".format( - 'https://{0}/test/{1}'.format(dyn_host, _domain))) + request_url)) domain = _domain key = path break From 02ea0c0656f3060f9067c0bf7ea2829a2ebe5d77 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 Aug 2017 16:16:09 +0200 Subject: [PATCH 0403/1066] [enh] Add reboot/shutdown actions in tools (#190) * [enh] Implements shutdown/reboot helpers. * [enh] Improve reboot/shutdown help. --- data/actionsmap/yunohost.yml | 20 ++++++++++++++++++++ locales/en.json | 4 ++++ src/yunohost/tools.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index af072c1bc..24b099451 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1478,6 +1478,26 @@ tools: extra: pattern: *pattern_port + ### tools_shutdown() + shutdown: + action_help: Shutdown the server + api: PUT /shutdown + arguments: + -f: + help: skip the shutdown confirmation + full: --force + action: store_true + + ### tools_reboot() + reboot: + action_help: Reboot the server + api: PUT /reboot + arguments: + -f: + help: skip the reboot confirmation + full: --force + action: store_true + subcategories: migrations: diff --git a/locales/en.json b/locales/en.json index a59f87b4d..3d70ab2b3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -276,6 +276,10 @@ "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", + "server_shutdown": "The server will shutdown", + "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", + "server_reboot": "The server will reboot", + "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", "service_add_failed": "Unable to add service '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 6dc3fcba4..5b74883f1 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -39,7 +39,7 @@ from importlib import import_module import apt import apt.progress -from moulinette import msettings, m18n +from moulinette import msettings, msignals, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_json, write_to_json @@ -632,6 +632,39 @@ def tools_port_available(port): return False +def tools_shutdown(force=False): + shutdown = force + if not shutdown: + try: + # Ask confirmation for server shutdown + i = msignals.prompt(m18n.n('server_shutdown_confirm', answers='y/N')) + except NotImplemented: + pass + else: + if i.lower() == 'y' or i.lower() == 'yes': + shutdown = True + + if shutdown: + logger.warn(m18n.n('server_shutdown')) + subprocess.check_call(['systemctl', 'poweroff']) + + +def tools_reboot(force=False): + reboot = force + if not reboot: + try: + # Ask confirmation for restoring + i = msignals.prompt(m18n.n('server_reboot_confirm', answers='y/N')) + except NotImplemented: + pass + else: + if i.lower() == 'y' or i.lower() == 'yes': + reboot = True + if reboot: + logger.warn(m18n.n('server_reboot')) + subprocess.check_call(['systemctl', 'reboot']) + + def tools_migrations_list(): """ List existing migrations From b89228426667105eb1bc5164c7dd30fa5ba1f6a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 13 Aug 2017 12:59:39 -0400 Subject: [PATCH 0404/1066] [fix] Remove check that domain is resolved locally --- src/yunohost/certificate.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a6a084d9a..6a5397e99 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -819,13 +819,6 @@ def _check_domain_is_ready_for_ACME(domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_http_not_working', domain=domain)) - # Check if domain is resolved locally (Might happen despite the previous - # checks because of dns propagation ?... Acme-tiny won't work in that case, - # because it explicitly requests() the domain.) - if not _domain_is_resolved_locally(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_not_resolved_locally', domain=domain)) - def _dns_ip_match_public_ip(public_ip, domain): try: @@ -854,17 +847,6 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True -def _domain_is_resolved_locally(public_ip, domain): - try: - ip = socket.gethostbyname(domain) - except socket.error as e: - logger.debug("Couldn't get domain '%s' ip because: %s" % (domain, e)) - return False - - logger.debug("Domain '%s' IP address is resolved to %s, expect it to be %s or in the 127.0.0.0/8 address block" % (domain, public_ip, ip)) - return ip.startswith("127.") or ip == public_ip - - def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") From 1f7d56940e537aaf64f1f47a80da4c9ff7119031 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 13 Aug 2017 14:00:15 -0400 Subject: [PATCH 0405/1066] [fix] Tell user that domain dns-conf shows a recommendation only --- locales/en.json | 1 + src/yunohost/domain.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 3d70ab2b3..035b93f95 100644 --- a/locales/en.json +++ b/locales/en.json @@ -141,6 +141,7 @@ "domain_creation_failed": "Unable to create domain", "domain_deleted": "The domain has been deleted", "domain_deletion_failed": "Unable to delete domain", + "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", "domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 3223183bb..589345196 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -33,7 +33,7 @@ import requests from urllib import urlopen -from moulinette import m18n +from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -220,6 +220,10 @@ def domain_dns_conf(domain, ttl=None): for record in dns_conf["mail"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) + is_cli = True if msettings.get('interface') == 'cli' else False + if is_cli: + logger.warning(m18n.n("domain_dns_conf_is_just_a_recommendation")) + return result From 3ede5fc39de47b15f0f41f8f8372354a0670d8d7 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 13 Aug 2017 22:26:28 +0200 Subject: [PATCH 0406/1066] [fix] Backup without info.json (#342) * [fix] Backup without info.json * Add test of archive restore with no info.json * Fix exception handling in backup_delete when info.json is missng --- src/yunohost/backup.py | 31 ++++++++++++++++-------- src/yunohost/tests/test_backuprestore.py | 16 ++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 9bed264c6..12e2adeff 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2193,7 +2193,11 @@ def backup_list(with_info=False, human_readable=False): if result and with_info: d = OrderedDict() for a in result: - d[a] = backup_info(a, human_readable=human_readable) + try: + d[a] = backup_info(a, human_readable=human_readable) + except MoulinetteError, e: + logger.warning('%s: %s' % (a, e.strerror)) + result = d return {'archives': result} @@ -2231,9 +2235,16 @@ def backup_info(name, with_details=False, human_readable=False): if not os.path.exists(info_file): tar = tarfile.open(archive_file, "r:gz") info_dir = info_file + '.d' - tar.extract('info.json', path=info_dir) - tar.close() - shutil.move(os.path.join(info_dir, 'info.json'), info_file) + try: + tar.extract('info.json', path=info_dir) + except KeyError: + logger.debug("unable to retrieve '%s' inside the archive", + info_file, exc_info=1) + raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) + else: + shutil.move(os.path.join(info_dir, 'info.json'), info_file) + finally: + tar.close() os.rmdir(info_dir) try: @@ -2281,21 +2292,21 @@ def backup_delete(name): name -- Name of the local backup archive """ + if name not in backup_list()["archives"]: + raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', + name=name)) + hook_callback('pre_backup_delete', args=[name]) archive_file = '%s/%s.tar.gz' % (ARCHIVES_PATH, name) - info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) + for backup_file in [archive_file, info_file]: - if not os.path.isfile(backup_file) and not os.path.islink(backup_file): - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_name_unknown', name=backup_file)) try: os.remove(backup_file) except: logger.debug("unable to delete '%s'", backup_file, exc_info=1) - raise MoulinetteError(errno.EIO, - m18n.n('backup_delete_error', path=backup_file)) + logger.warning(m18n.n('backup_delete_error', path=backup_file)) hook_callback('post_backup_delete', args=[name]) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 5fd1bd4a9..9132b9611 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -634,5 +634,21 @@ def _test_backup_and_restore_app(app): assert app_is_installed(app) +############################################################################### +# Some edge cases # +############################################################################### +def test_restore_archive_with_no_json(mocker): + + # Create a backup with no info.json associated + os.system("touch /tmp/afile") + os.system("tar -czvf /home/yunohost.backup/archives/badbackup.tar.gz /tmp/afile") + + assert "badbackup" in backup_list()["archives"] + + mocker.spy(m18n, "n") + with pytest.raises(MoulinetteError): + backup_restore(auth, name="badbackup", force=True, + ignore_system=False, ignore_apps=False) + m18n.n.assert_any_call('backup_invalid_archive') From bb4af396d8859b8cf350e50c5eb08a1b5f73946f Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 13 Aug 2017 22:28:36 +0200 Subject: [PATCH 0407/1066] [fix] Make read-only mount bind actually read-only (#343) * [fix] Mount bind readonly not really readonly * Attempt to clarify and fix some issues with the readonly mount * Fix some missing messages and exception handling * Get rid of horrible bash command * Use subproces.check_call to avoid security hazard * Revert comment about hard link * Add test that mount binds are readonly --- locales/en.json | 3 +- src/yunohost/backup.py | 50 ++++++++++++++++-------- src/yunohost/tests/test_backuprestore.py | 25 +++++++++++- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3d70ab2b3..7ce7d2980 100644 --- a/locales/en.json +++ b/locales/en.json @@ -69,11 +69,12 @@ "backup_archive_open_failed": "Unable to open the backup archive", "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", - "backup_ask_for_copying_if_needed": "Your system don't support completely the quick method to organize files in the archive, do you want to organize its by copying {size:s}MB?", + "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?", "backup_borg_not_implemented": "Borg backup method is not yet implemented", "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", + "backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.", "backup_created": "Backup created", "backup_creating_archive": "Creating the backup archive...", "backup_creation_failed": "Backup creation failed", diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 12e2adeff..45def947c 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -40,6 +40,7 @@ from moulinette import msignals, m18n from moulinette.core import MoulinetteError from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file from yunohost.app import ( app_info, _is_installed, _parse_app_instance_name @@ -1543,23 +1544,37 @@ class BackupMethod(object): if not os.path.isdir(dest_dir): filesystem.mkdir(dest_dir, parents=True) - # Try to bind files + # For directory, attempt to mount bind if os.path.isdir(src): filesystem.mkdir(dest, parents=True, force=True) - ret = subprocess.call(["mount", "-r", "--rbind", src, dest]) - if ret == 0: - continue + + try: + subprocess.check_call(["mount", "--rbind", src, dest]) + subprocess.check_call(["mount", "-o", "remount,ro,bind", dest]) + except Exception as e: + logger.warning(m18n.n("backup_couldnt_bind", src=src, dest=dest)) + # To check if dest is mounted, use /proc/mounts that + # escape spaces as \040 + raw_mounts = read_file("/proc/mounts").strip().split('\n') + mounts = [ m.split()[1] for m in raw_mounts ] + mounts = [ m.replace("\\040", " ") for m in mounts ] + if dest in mounts: + subprocess.check_call(["umount", "-R", dest]) else: - logger.warning(m18n.n("bind_mouting_disable")) - subprocess.call(["mountpoint", "-q", dest, - "&&", "umount", "-R", dest]) - elif os.path.isfile(src) or os.path.islink(src): - # Create a hardlink if src and dest are on the filesystem - if os.stat(src).st_dev == os.stat(dest_dir).st_dev: - os.link(src, dest) + # Success, go to next file to organize continue - # Add to the list to copy + # For files, create a hardlink + elif os.path.isfile(src) or os.path.islink(src): + # Can create a hard link only if files are on the same fs + # (i.e. we can't if it's on a different fs) + if os.stat(src).st_dev == os.stat(dest_dir).st_dev: + os.link(src, dest) + # Success, go to next file to organize + continue + + # If mountbind or hardlink couldnt be created, + # prepare a list of files that need to be copied paths_needed_to_be_copied.append(path) if len(paths_needed_to_be_copied) == 0: @@ -1576,15 +1591,18 @@ class BackupMethod(object): if size > MB_ALLOWED_TO_ORGANIZE: try: i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', - answers='y/N', size=size)) + answers='y/N', size=str(size))) except NotImplemented: - logger.error(m18n.n('backup_unable_to_organize_files')) + raise MoulinetteError(errno.EIO, + m18n.n('backup_unable_to_organize_files')) else: if i != 'y' and i != 'Y': - logger.error(m18n.n('backup_unable_to_organize_files')) + raise MoulinetteError(errno.EIO, + m18n.n('backup_unable_to_organize_files')) # Copy unbinded path - logger.info(m18n.n('backup_copying_to_organize_the_archive', size=size)) + logger.info(m18n.n('backup_copying_to_organize_the_archive', + size=str(size))) for path in paths_needed_to_be_copied: dest = os.path.join(self.work_dir, path['dest']) if os.path.isdir(path['source']): diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 9132b9611..8c860fc60 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -643,7 +643,7 @@ def test_restore_archive_with_no_json(mocker): # Create a backup with no info.json associated os.system("touch /tmp/afile") os.system("tar -czvf /home/yunohost.backup/archives/badbackup.tar.gz /tmp/afile") - + assert "badbackup" in backup_list()["archives"] mocker.spy(m18n, "n") @@ -652,3 +652,26 @@ def test_restore_archive_with_no_json(mocker): ignore_system=False, ignore_apps=False) m18n.n.assert_any_call('backup_invalid_archive') + +def test_backup_binds_are_readonly(monkeypatch): + + def custom_mount_and_backup(self, backup_manager): + self.manager = backup_manager + self._organize_files() + + confssh = os.path.join(self.work_dir, "conf/ssh") + output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh, + shell=True) + + assert "Read-only file system" in output + + if self._recursive_umount(self.work_dir) > 0: + raise Exception("Backup cleaning failed !") + + self.clean() + + monkeypatch.setattr("yunohost.backup.BackupMethod.mount_and_backup", + custom_mount_and_backup) + + # Create the backup + backup_create(ignore_system=False, ignore_apps=True) From 0a8362c764f504cc39407619f0ed56d212375eaf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 13 Aug 2017 17:01:06 -0400 Subject: [PATCH 0408/1066] Remove old 'lock' configuration --- data/actionsmap/yunohost.yml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 24b099451..8067d3de0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -50,7 +50,6 @@ _global: uri: ldap://localhost:389 base_dn: dc=yunohost,dc=org argument_auth: true - lock: true arguments: -v: full: --version @@ -259,7 +258,6 @@ domain: action_help: Create a custom domain api: POST /domains configuration: - lock: false authenticate: all arguments: domain: @@ -278,7 +276,6 @@ domain: action_help: Delete domains api: DELETE /domains/ configuration: - lock: false authenticate: all arguments: domain: @@ -291,7 +288,6 @@ domain: action_help: Generate DNS configuration for a domain api: GET /domains//dns configuration: - lock: false authenticate: - api arguments: @@ -498,7 +494,6 @@ app: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: app: help: Name, local path or git URL of the app to install @@ -520,7 +515,6 @@ app: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: app: help: App(s) to delete @@ -532,7 +526,6 @@ app: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: app: help: App(s) to upgrade (default all) @@ -551,7 +544,6 @@ app: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: app: help: Target app instance name @@ -728,8 +720,6 @@ backup: create: action_help: Create a backup local archive api: POST /backup - configuration: - lock: false arguments: -n: full: --name @@ -778,7 +768,6 @@ backup: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: name: help: Name of the local backup archive @@ -808,8 +797,6 @@ backup: list: action_help: List available local backup archives api: GET /backup/archives - configuration: - lock: false arguments: -i: full: --with-info @@ -824,8 +811,6 @@ backup: info: action_help: Show info about a local backup archive api: GET /backup/archives/ - configuration: - lock: false arguments: name: help: Name of the local backup archive @@ -842,8 +827,6 @@ backup: delete: action_help: Delete a backup archive api: DELETE /backup/archives/ - configuration: - lock: false arguments: name: help: Name of the archive to delete @@ -1144,8 +1127,6 @@ service: regen-conf: action_help: Regenerate the configuration file(s) for a service api: PUT /services/regenconf - configuration: - lock: false deprecated_alias: - regenconf arguments: @@ -1390,7 +1371,6 @@ tools: - PUT /domains/main configuration: authenticate: all - lock: false arguments: -n: full: --new-domain @@ -1404,7 +1384,6 @@ tools: api: POST /postinstall configuration: authenticate: false - lock: false arguments: -d: full: --domain @@ -1428,8 +1407,6 @@ tools: update: action_help: YunoHost update api: PUT /update - configuration: - lock: false arguments: --ignore-apps: help: Ignore apps cache update and changelog @@ -1445,7 +1422,6 @@ tools: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: --ignore-apps: help: Ignore apps upgrade @@ -1461,7 +1437,6 @@ tools: configuration: authenticate: all authenticator: ldap-anonymous - lock: false arguments: -p: full: --private From a050b405591ffa1f802671a12cc7847d27320919 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 13 Aug 2017 17:07:20 -0400 Subject: [PATCH 0409/1066] Removed unusted socket import --- src/yunohost/certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 6a5397e99..a2726e84f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,7 +31,6 @@ import grp import smtplib import requests import subprocess -import socket import dns.resolver import glob From ee7fd825d406aee0ebbe724f9e8887fac3ff446d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:14:05 +0200 Subject: [PATCH 0410/1066] [mod] remove unused variables --- src/yunohost/domain.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 3223183bb..6a3b2f3aa 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -86,9 +86,6 @@ def domain_add(auth, domain, dyndns=False): attr_dict = {'objectClass': ['mailDomain', 'top']} - now = datetime.datetime.now() - timestamp = str(now.year) + str(now.month) + str(now.day) - if domain in domain_list(auth)['domains']: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) From f41c2dd763ca40c274df7a319aa042dafed16c88 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:14:40 +0200 Subject: [PATCH 0411/1066] [mod] remove unused import --- src/yunohost/domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 6a3b2f3aa..4665f9905 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -24,7 +24,6 @@ Manage domains """ import os -import datetime import re import json import yaml From 951589ad07e8c35dd7a9a54e162a044eb885f285 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 13 Aug 2017 17:16:07 -0400 Subject: [PATCH 0412/1066] Regen dnsmasq conf if it's not up to date :| --- src/yunohost/certificate.py | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a2726e84f..b6fb0e275 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -47,7 +47,7 @@ import yunohost.domain from moulinette import m18n from yunohost.app import app_ssowatconf -from yunohost.service import _run_service_command +from yunohost.service import _run_service_command, service_regen_conf logger = getActionLogger('yunohost.certmanager') @@ -528,6 +528,9 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) _set_permissions(TMP_FOLDER, "root", "root", 0640) + # Regen conf for dnsmasq if needed + _regen_dnsmasq_if_needed() + # Prepare certificate signing request logger.info( "Prepare key and certificate signing request (CSR) for %s...", domain) @@ -846,6 +849,48 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True +# FIXME / TODO : ideally this should not be needed. There should be a proper +# mechanism to regularly check the value of the public IP and trigger +# corresponding hooks (e.g. dyndns update and dnsmasq regen-conf) +def _regen_dnsmasq_if_needed(): + """ + Update the dnsmasq conf if some IPs are not up to date... + """ + try: + ipv4 = yunohost.domain.get_public_ip() + except: + ipv4 = None + try: + ipv6 = yunohost.domain.get_public_ip(6) + except: + ipv6 = None + + do_regen = False + + # For all domain files in DNSmasq conf... + domainsconf = glob.glob("/etc/dnsmasq.d/*.*") + for domainconf in domainsconf: + + # Look for the IP, it's in the lines with this format : + # address=/the.domain.tld/11.22.33.44 + for line in open(domainconf).readlines(): + if not line.startswith("address"): + continue + ip = line.strip().split("/")[2] + + # Compared found IP to current IPv4 / IPv6 + # IPv6 IPv4 + if (":" in ip and ip != ipv6) or (ip != ipv4): + do_regen = True + break + + if do_regen: + break + + if do_regen: + service_regen_conf(["dnsmasq"]) + + def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") From 56513bbdb121aa654cff9bbb28241addbf0fb7ab Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:18:18 +0200 Subject: [PATCH 0413/1066] [mod] we never used those filtering/offset things --- data/actionsmap/yunohost.yml | 12 ------------ src/yunohost/domain.py | 17 ++++------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 24b099451..b3df2aa58 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -241,18 +241,6 @@ domain: configuration: authenticate: all authenticator: ldap-anonymous - arguments: - -f: - full: --filter - help: LDAP filter used to search - -l: - full: --limit - help: Maximum number of domain fetched - type: int - -o: - full: --offset - help: Starting number for domain fetching - type: int ### domain_add() add: diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 4665f9905..d3884ce46 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -43,7 +43,7 @@ from yunohost.service import service_regen_conf logger = getActionLogger('yunohost.domain') -def domain_list(auth, filter=None, limit=None, offset=None): +def domain_list(auth): """ List domains @@ -55,19 +55,10 @@ def domain_list(auth, filter=None, limit=None, offset=None): """ result_list = [] - # Set default arguments values - if offset is None: - offset = 0 - if limit is None: - limit = 1000 - if filter is None: - filter = 'virtualdomain=*' + result = auth.search('ou=domains,dc=yunohost,dc=org', 'virtualdomain=*', ['virtualdomain']) - result = auth.search('ou=domains,dc=yunohost,dc=org', filter, ['virtualdomain']) - - if len(result) > offset and limit > 0: - for domain in result[offset:offset + limit]: - result_list.append(domain['virtualdomain'][0]) + for domain in result: + result_list.append(domain['virtualdomain'][0]) return {'domains': result_list} From 286449386c366d49b3fae6228a73d2c8e52aca8b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:25:11 +0200 Subject: [PATCH 0414/1066] [mod] clean code, this is what we indeed want to check --- src/yunohost/domain.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index d3884ce46..191a2e79a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -114,13 +114,10 @@ def domain_add(auth, domain, dyndns=False): if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) - try: - with open('/etc/yunohost/installed', 'r') as f: - service_regen_conf(names=[ - 'nginx', 'metronome', 'dnsmasq', 'rmilter']) - os.system('yunohost app ssowatconf > /dev/null 2>&1') - except IOError: - pass + if os.path.exists('/etc/yunohost/installed'): + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) + os.system('yunohost app ssowatconf > /dev/null 2>&1') + except: # Force domain removal silently try: From 4c069c5c3f7dc2204e07974196d4fdf6f2cd206a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:32:50 +0200 Subject: [PATCH 0415/1066] [mod] fails when registering dynette domain and that dynette can't be reached --- locales/en.json | 1 + src/yunohost/domain.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/locales/en.json b/locales/en.json index 7ce7d2980..d2f3d7137 100644 --- a/locales/en.json +++ b/locales/en.json @@ -143,6 +143,7 @@ "domain_deleted": "The domain has been deleted", "domain_deletion_failed": "Unable to delete domain", "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", + "domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}", "domain_dyndns_invalid": "Invalid domain to use with DynDNS", "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "Domain already exists", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 191a2e79a..fe4349e58 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -87,19 +87,20 @@ def domain_add(auth, domain, dyndns=False): try: r = requests.get('https://dyndns.yunohost.org/domains') - except requests.ConnectionError: - pass + except requests.ConnectionError as e: + raise MoulinetteError(errno.EHOSTUNREACH, + m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e))) + + dyndomains = json.loads(r.text) + dyndomain = '.'.join(domain.split('.')[1:]) + if dyndomain in dyndomains: + if os.path.exists('/etc/cron.d/yunohost-dyndns'): + raise MoulinetteError(errno.EPERM, + m18n.n('domain_dyndns_already_subscribed')) + dyndns_subscribe(domain=domain) else: - dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) - if dyndomain in dyndomains: - if os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise MoulinetteError(errno.EPERM, - m18n.n('domain_dyndns_already_subscribed')) - dyndns_subscribe(domain=domain) - else: - raise MoulinetteError(errno.EINVAL, - m18n.n('domain_dyndns_root_unknown')) + raise MoulinetteError(errno.EINVAL, + m18n.n('domain_dyndns_root_unknown')) try: yunohost.certificate._certificate_install_selfsigned([domain], False) From 3f65543dc21729c4033b23853368c60b9fa73e8c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:33:41 +0200 Subject: [PATCH 0416/1066] [mod] we need timeout everywhere to avoid blocking code --- src/yunohost/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index fe4349e58..5fb7ed313 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -86,7 +86,7 @@ def domain_add(auth, domain, dyndns=False): from yunohost.dyndns import dyndns_subscribe try: - r = requests.get('https://dyndns.yunohost.org/domains') + r = requests.get('https://dyndns.yunohost.org/domains', timeout=30) except requests.ConnectionError as e: raise MoulinetteError(errno.EHOSTUNREACH, m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e))) From a8e57d9c6a9e444563d0c14887f906151587b2a7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:34:35 +0200 Subject: [PATCH 0417/1066] [mod] don't request the server if a dyndns domain is already subscribed --- src/yunohost/domain.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 5fb7ed313..a8142fe88 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -83,20 +83,21 @@ def domain_add(auth, domain, dyndns=False): if dyndns: if len(domain.split('.')) < 3: raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid')) - from yunohost.dyndns import dyndns_subscribe + if os.path.exists('/etc/cron.d/yunohost-dyndns'): + raise MoulinetteError(errno.EPERM, + m18n.n('domain_dyndns_already_subscribed')) try: r = requests.get('https://dyndns.yunohost.org/domains', timeout=30) except requests.ConnectionError as e: raise MoulinetteError(errno.EHOSTUNREACH, m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e))) + from yunohost.dyndns import dyndns_subscribe + dyndomains = json.loads(r.text) dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: - if os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise MoulinetteError(errno.EPERM, - m18n.n('domain_dyndns_already_subscribed')) dyndns_subscribe(domain=domain) else: raise MoulinetteError(errno.EINVAL, From 555caa9c73ef0ef811ae51460d937341fe2c71e2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:37:12 +0200 Subject: [PATCH 0418/1066] [mod] only check once if the domain exist --- src/yunohost/domain.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index a8142fe88..939ebb754 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -76,7 +76,9 @@ def domain_add(auth, domain, dyndns=False): attr_dict = {'objectClass': ['mailDomain', 'top']} - if domain in domain_list(auth)['domains']: + try: + auth.validate_uniqueness({'virtualdomain': domain}) + except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) # DynDNS domain @@ -106,11 +108,6 @@ def domain_add(auth, domain, dyndns=False): try: yunohost.certificate._certificate_install_selfsigned([domain], False) - try: - auth.validate_uniqueness({'virtualdomain': domain}) - except MoulinetteError: - raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) - attr_dict['virtualdomain'] = domain if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): From 1fe568a60d51bae1b84c3196e42650e6fff281e0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:38:11 +0200 Subject: [PATCH 0419/1066] [mod] style, declare everything as once to make code more readable --- src/yunohost/domain.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 939ebb754..4690b2f3f 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -74,8 +74,6 @@ def domain_add(auth, domain, dyndns=False): """ from yunohost.hook import hook_callback - attr_dict = {'objectClass': ['mailDomain', 'top']} - try: auth.validate_uniqueness({'virtualdomain': domain}) except MoulinetteError: @@ -108,7 +106,10 @@ def domain_add(auth, domain, dyndns=False): try: yunohost.certificate._certificate_install_selfsigned([domain], False) - attr_dict['virtualdomain'] = domain + attr_dict = { + 'objectClass': ['mailDomain', 'top'], + 'virtualdomain': domain, + } if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) From 968259239dbd763bb3ed27c42f1e559a51ce491a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 13 Aug 2017 23:40:44 +0200 Subject: [PATCH 0420/1066] [mod] call directly python code instead of using os.system --- src/yunohost/domain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 4690b2f3f..457157160 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -141,6 +141,7 @@ def domain_remove(auth, domain, force=False): """ from yunohost.hook import hook_callback + from yunohost.app import app_ssowatconf if not force and domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) @@ -167,7 +168,7 @@ def domain_remove(auth, domain, force=False): raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed')) service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) - os.system('yunohost app ssowatconf > /dev/null 2>&1') + app_ssowatconf(auth) hook_callback('post_domain_remove', args=[domain]) From d0666d86d6ecbf60cb078a46c8ae7ca7e5eb921d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:47:16 +0200 Subject: [PATCH 0421/1066] [mod] we don't use those filter/offset/limit stuff --- src/yunohost/user.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index d35b082d7..3b783b3af 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -40,7 +40,7 @@ from yunohost.service import service_status logger = getActionLogger('yunohost.user') -def user_list(auth, fields=None, filter=None, limit=None, offset=None): +def user_list(auth, fields=None): """ List users @@ -59,13 +59,6 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): attrs = ['uid'] users = {} - # Set default arguments values - if offset is None: - offset = 0 - if limit is None: - limit = 1000 - if filter is None: - filter = '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))' if fields: keys = user_attrs.keys() for attr in fields: @@ -77,18 +70,18 @@ def user_list(auth, fields=None, filter=None, limit=None, offset=None): else: attrs = ['uid', 'cn', 'mail', 'mailuserquota'] - result = auth.search('ou=users,dc=yunohost,dc=org', filter, attrs) + result = auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', attrs) + + for user in result: + entry = {} + for attr, values in user.items(): + try: + entry[user_attrs[attr]] = values[0] + except: + pass + uid = entry[user_attrs['uid']] + users[uid] = entry - if len(result) > offset and limit > 0: - for user in result[offset:offset + limit]: - entry = {} - for attr, values in user.items(): - try: - entry[user_attrs[attr]] = values[0] - except: - pass - uid = entry[user_attrs['uid']] - users[uid] = entry return {'users': users} From c8ce4f843a9d501f8ac67366d46f74cb40f13b8a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:47:34 +0200 Subject: [PATCH 0422/1066] [mod] style --- src/yunohost/user.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 3b783b3af..d2834b7bf 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -51,11 +51,14 @@ def user_list(auth, fields=None): fields -- fields to fetch """ - user_attrs = {'uid': 'username', - 'cn': 'fullname', - 'mail': 'mail', - 'maildrop': 'mail-forward', - 'mailuserquota': 'mailbox-quota'} + user_attrs = { + 'uid': 'username', + 'cn': 'fullname', + 'mail': 'mail', + 'maildrop': 'mail-forward', + 'mailuserquota': 'mailbox-quota' + } + attrs = ['uid'] users = {} @@ -70,7 +73,9 @@ def user_list(auth, fields=None): else: attrs = ['uid', 'cn', 'mail', 'mailuserquota'] - result = auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', attrs) + result = auth.search('ou=users,dc=yunohost,dc=org', + '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', + attrs) for user in result: entry = {} From ad285937f30ac98fdc59231ee8bc65f4a33dc522 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:50:35 +0200 Subject: [PATCH 0423/1066] [mod] to the real test --- src/yunohost/user.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index d2834b7bf..6b10af249 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -80,10 +80,9 @@ def user_list(auth, fields=None): for user in result: entry = {} for attr, values in user.items(): - try: + if values: entry[user_attrs[attr]] = values[0] - except: - pass + uid = entry[user_attrs['uid']] users[uid] = entry From d441290b2f550193215ad0640f1131e79c2d075b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:53:13 +0200 Subject: [PATCH 0424/1066] [mod] lisibility --- src/yunohost/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 6b10af249..cb1105bfd 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -123,10 +123,10 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists - if mail[mail.find('@') + 1:] not in domain_list(auth)['domains']: + if mail.split("@")[1] not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', - domain=mail[mail.find('@') + 1:])) + domain=mail.split("@")[1])) # Get random UID/GID uid_check = gid_check = 0 From 062ceff6127337decf699b15c3393b7d1587929a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:53:46 +0200 Subject: [PATCH 0425/1066] [mod] style --- src/yunohost/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index cb1105bfd..7b422c11b 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -24,13 +24,13 @@ Manage users """ import os +import re +import json +import errno import crypt import random import string -import json -import errno import subprocess -import re from moulinette import m18n from moulinette.core import MoulinetteError From 32002d56a0477d5085455e2c77159a061d1ef540 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 00:54:33 +0200 Subject: [PATCH 0426/1066] [mod] inline for code lisibility --- src/yunohost/user.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 7b422c11b..bc65146c9 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -137,7 +137,6 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) - rdn = 'uid=%s,ou=users' % username char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set, 8)) salt = '$1$' + salt + '$' @@ -189,7 +188,7 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) - if auth.add(rdn, attr_dict): + if auth.add('uid=%s,ou=users' % username, attr_dict): # Invalidate passwd to take user creation into account subprocess.call(['nscd', '-i', 'passwd']) From d14bc3177179b0573b56db4b64550f3287cd9ff3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 10:37:05 +0200 Subject: [PATCH 0427/1066] [mod] directly call python instead of using os.system --- src/yunohost/user.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index bc65146c9..ee0655291 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -129,11 +129,13 @@ def user_create(auth, username, firstname, lastname, mail, password, domain=mail.split("@")[1])) # Get random UID/GID - uid_check = gid_check = 0 - while uid_check == 0 and gid_check == 0: + all_uid = {x.pw_uid for x in pwd.getpwall()} + all_gid = {x.pw_gid for x in pwd.getpwall()} + + uid_guid_found = False + while not uid_guid_found: uid = str(random.randint(200, 99999)) - uid_check = os.system("getent passwd %s" % uid) - gid_check = os.system("getent group %s" % uid) + uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) From c14acc0fae8b7c19988dfe3352a297ec22d624e9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 10:38:16 +0200 Subject: [PATCH 0428/1066] [mod] don't use exception when not needed --- src/yunohost/user.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ee0655291..deb5de077 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -115,11 +115,8 @@ def user_create(auth, username, firstname, lastname, mail, password, }) # Validate uniqueness of username in system users - try: - pwd.getpwnam(username) - except KeyError: - pass - else: + all_existing_usernames = {x.pw_name for x in pwd.getpwall()} + if username in all_existing_usernames: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) # Check that the mail domain exists From 8c6db3845dea1675f1d71f919e5c08765a1ef79b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 15:23:51 +0200 Subject: [PATCH 0429/1066] [fix] use real random for hash selection --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index deb5de077..a5b659e04 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -137,7 +137,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set, 8)) + salt = ''.join([random.SystemRandom().choice(char_set) for x in range(8)]) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { From 9d0e615bb46f1b9ab04b216776313f486908aba8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 15:24:45 +0200 Subject: [PATCH 0430/1066] [enh] use the full length of available chars for salt generation --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index a5b659e04..85e890301 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -136,7 +136,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) - char_set = string.ascii_uppercase + string.digits + char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" salt = ''.join([random.SystemRandom().choice(char_set) for x in range(8)]) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) From 49d5c70fd64ace00ef51179afd833161d7493e77 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 15:44:06 +0200 Subject: [PATCH 0431/1066] [mod] add more salt because life is miserable --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 85e890301..07ae50ddd 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -137,7 +137,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" - salt = ''.join([random.SystemRandom().choice(char_set) for x in range(8)]) + salt = ''.join([random.SystemRandom().choice(char_set) for x in range(12)]) salt = '$1$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { From 4419ec10ce27e5ef6ecb957815211e0cf989e3b4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 16:09:46 +0200 Subject: [PATCH 0432/1066] [fix] move to sh512 because it's fucking year 2017 --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 07ae50ddd..97466f7f2 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -138,7 +138,7 @@ def user_create(auth, username, firstname, lastname, mail, password, fullname = '%s %s' % (firstname, lastname) char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" salt = ''.join([random.SystemRandom().choice(char_set) for x in range(12)]) - salt = '$1$' + salt + '$' + salt = '$6$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'], From 6c9156c3aee483facebd5473cbb8fada3149f2da Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 16:49:50 +0200 Subject: [PATCH 0433/1066] [enh] according to https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html we can go up to 16 salt caracters --- src/yunohost/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 97466f7f2..51b7400de 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -137,7 +137,7 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" - salt = ''.join([random.SystemRandom().choice(char_set) for x in range(12)]) + salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)]) salt = '$6$' + salt + '$' user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { From 685f3746ef2dfefcbab0a20d334cd85529239c05 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 14 Aug 2017 23:07:45 +0200 Subject: [PATCH 0434/1066] [fix] forgot to remove args from actionsmap --- data/actionsmap/yunohost.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 24b099451..df13e3bc9 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -78,17 +78,6 @@ user: --fields: help: fields to fetch nargs: "+" - -f: - full: --filter - help: LDAP filter used to search - -l: - full: --limit - help: Maximum number of user fetched - type: int - -o: - full: --offset - help: Starting number for user fetching - type: int ### user_create() create: From 147b305e8ac1fa8236ebeafd33896c1e97901b67 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 00:34:07 +0200 Subject: [PATCH 0435/1066] [mod] extract password generation code to a function --- src/yunohost/user.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 51b7400de..106c113d9 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -136,10 +136,6 @@ def user_create(auth, username, firstname, lastname, mail, password, # Adapt values for LDAP fullname = '%s %s' % (firstname, lastname) - 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 + '$' - user_pwd = '{CRYPT}' + crypt.crypt(str(password), salt) attr_dict = { 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'], 'givenName': firstname, @@ -150,7 +146,7 @@ def user_create(auth, username, firstname, lastname, mail, password, 'mail': mail, 'maildrop': username, 'mailuserquota': mailbox_quota, - 'userPassword': user_pwd, + 'userPassword': _hash_user_password(password), 'gidNumber': uid, 'uidNumber': uid, 'homeDirectory': '/home/' + username, @@ -448,3 +444,10 @@ def _convertSize(num, suffix=''): return "%3.1f%s%s" % (num, unit, suffix) num /= 1024.0 return "%.1f%s%s" % (num, 'Yi', suffix) + + +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) From c5a44b863883442efdf4f4d2a303a7eb7f7aa704 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 00:34:33 +0200 Subject: [PATCH 0436/1066] [fix] also uses sha512 in user_update() --- src/yunohost/user.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 106c113d9..1bcb3d279 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -289,10 +289,7 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname if change_password: - char_set = string.ascii_uppercase + string.digits - salt = ''.join(random.sample(char_set, 8)) - salt = '$1$' + salt + '$' - new_attr_dict['userPassword'] = '{CRYPT}' + crypt.crypt(str(change_password), salt) + new_attr_dict['userPassword'] = _hash_user_password(change_password) if mail: auth.validate_uniqueness({'mail': mail}) From 970d9b0207da923a33ca114b3fc2b10b3952a1c0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 12:52:44 +0200 Subject: [PATCH 0437/1066] [fix] uses strong hash for admin password --- src/yunohost/tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5b74883f1..70c867525 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -122,8 +122,11 @@ def tools_adminpw(auth, new_password): new_password """ + from yunohost.user import _hash_user_password try: - auth.con.passwd_s('cn=admin,dc=yunohost,dc=org', None, new_password) + auth.update("cn=admin", { + "userPassword": _hash_user_password(new_password), + }) except: logger.exception('unable to change admin password') raise MoulinetteError(errno.EPERM, From 52c7a37a785bea5db6a4526bb2548cac35b1888e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 17:18:50 +0200 Subject: [PATCH 0438/1066] [enh] add 'yunohost tools shell' --- data/actionsmap/yunohost.yml | 6 ++++++ src/yunohost/tools.py | 24 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 24b099451..3effd8442 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1478,6 +1478,12 @@ tools: extra: pattern: *pattern_port + ### tools_shell() + shell: + configuration: + authenticate: all + action_help: Launch a development shell + ### tools_shutdown() shutdown: action_help: Shutdown the server diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5b74883f1..294575ed8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -814,6 +814,30 @@ def tools_migrations_state(): return read_json(MIGRATIONS_STATE_PATH) +def tools_shell(auth): + """ + Launch an (i)python shell in the YunoHost context. + + This is entirely aim for development. + """ + + logger.warn("The \033[1;34mauth\033[0m is available in this context") + try: + from IPython import embed + embed() + except ImportError: + logger.warn("You don't have IPython installed, consider installing it as it is way better than the standard shell.") + logger.warn("Falling back on the standard shell.") + + import readline # will allow Up/Down/History in the console + readline # to please pyflakes + import code + vars = globals().copy() + vars.update(locals()) + shell = code.InteractiveConsole(vars) + shell.interact() + + def _get_migrations_list(): migrations = [] From 4e1a8395575379632cdcde26a4d048003410b94a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 17:24:26 +0200 Subject: [PATCH 0439/1066] =?UTF-8?q?[enh]=20can=20do=20a=20'yunohost=20to?= =?UTF-8?q?ols=20shell=20-c=20command'=20=C3=A0=20la=20python=20-c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/actionsmap/yunohost.yml | 4 ++++ src/yunohost/tools.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 3effd8442..46466de3d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1483,6 +1483,10 @@ tools: configuration: authenticate: all action_help: Launch a development shell + arguments: + -c: + help: python command to execute + full: --command ### tools_shutdown() shutdown: diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 294575ed8..3fdee520c 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -814,13 +814,17 @@ def tools_migrations_state(): return read_json(MIGRATIONS_STATE_PATH) -def tools_shell(auth): +def tools_shell(auth, command=None): """ Launch an (i)python shell in the YunoHost context. This is entirely aim for development. """ + if command: + exec(command) + return + logger.warn("The \033[1;34mauth\033[0m is available in this context") try: from IPython import embed From 1cd121e801e416e8c770d4f7dbe2cc7fb8e7d83e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 15 Aug 2017 20:53:50 +0200 Subject: [PATCH 0440/1066] [doc] add comment explaining choices in _hash_user_password --- src/yunohost/user.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 1bcb3d279..4e382c4d8 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -445,6 +445,16 @@ def _convertSize(num, suffix=''): def _hash_user_password(password): char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" + # This 16 number is chosen according to this documentation stating that + # this is the maximum number of salt possible + # https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html + # + # SystemRandom is the cryptographically secure random method provided by python stl + # You can refer to this https://docs.python.org/2/library/random.html for + # confirmation (read the red square), it internally uses /dev/urandom salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)]) + + # Using "$6$" means that we uses sha-512 which is the strongest hash available on the system + # You can refer to this for more explainations https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html salt = '$6$' + salt + '$' return '{CRYPT}' + crypt.crypt(str(password), salt) From d010a288b27564b0c266a745a3f2b8ab3daedd2f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 Aug 2017 21:06:00 +0200 Subject: [PATCH 0441/1066] [enh] Add a complete docstring with explanations and reference --- src/yunohost/user.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 4e382c4d8..11f61d807 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -444,17 +444,29 @@ def _convertSize(num, suffix=''): def _hash_user_password(password): + """ + This function computes and return a salted hash for the password in input. + This implementation is inspired from [1]. + + The hash follows SHA-512 scheme from Linux/glibc. + Hence the {CRYPT} and $6$ prefixes + - {CRYPT} means it relies on the OS' crypt lib + - $6$ corresponds to SHA-512, the strongest hash available on the system + + The salt is generated using random.SystemRandom(). It is the crypto-secure + pseudo-random number generator according to the python doc [2] (c.f. the + red square). It internally relies on /dev/urandom + + The salt is made of 16 characters from the set [./a-zA-Z0-9]. This is the + max sized allowed for salts according to [3] + + [1] https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html + [2] https://docs.python.org/2/library/random.html + [3] https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html + """ + char_set = string.ascii_uppercase + string.ascii_lowercase + string.digits + "./" - # This 16 number is chosen according to this documentation stating that - # this is the maximum number of salt possible - # https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html - # - # SystemRandom is the cryptographically secure random method provided by python stl - # You can refer to this https://docs.python.org/2/library/random.html for - # confirmation (read the red square), it internally uses /dev/urandom salt = ''.join([random.SystemRandom().choice(char_set) for x in range(16)]) - # Using "$6$" means that we uses sha-512 which is the strongest hash available on the system - # You can refer to this for more explainations https://www.redpill-linpro.com/techblog/2016/08/16/ldap-password-hash.html salt = '$6$' + salt + '$' return '{CRYPT}' + crypt.crypt(str(password), salt) From 360ac66bc75f035217d61490967f6275698e6979 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 19 Aug 2017 11:11:49 -0400 Subject: [PATCH 0442/1066] Use app_ssowatconf instead of os.system call --- src/yunohost/domain.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 457157160..d28e32d36 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -73,6 +73,7 @@ def domain_add(auth, domain, dyndns=False): """ from yunohost.hook import hook_callback + from yunohost.app import app_ssowatconf try: auth.validate_uniqueness({'virtualdomain': domain}) @@ -114,9 +115,10 @@ def domain_add(auth, domain, dyndns=False): if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) + # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) - os.system('yunohost app ssowatconf > /dev/null 2>&1') + app_ssowatconf(auth) except: # Force domain removal silently From c05b4e7f50c7d2f11796f062e7a068c73bd10a2f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Aug 2017 02:10:45 +0000 Subject: [PATCH 0443/1066] Update changelog for 2.7.1 release --- debian/changelog | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/debian/changelog b/debian/changelog index 305638f7e..c2f4a6783 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,41 @@ +yunohost (2.7.1) testing; urgency=low + + ## Security: uses sha-512 to store password and auto upgrade old password on login + * [fix] use real random for hash selection (Laurent Peuch) + * [enh] use the full length of available chars for salt generation (Laurent Peuch) + * [mod] add more salt because life is miserable (Laurent Peuch) + * [fix] move to sh512 because it's fucking year 2017 (Laurent Peuch) + * [enh] according to https://www.safaribooksonline.com/library/view/practical-unix-and/0596003234/ch04s03.html we can go up to 16 salt caracters (Laurent Peuch) + * [fix] also uses sha512 in user_update() (Laurent Peuch) + * [fix] uses strong hash for admin password (Laurent Peuch) + + ## Add a reboot/shutdown action + * [enh] Add reboot/shutdown actions in tools (#190) (Laurent Peuch, opi) + + ## Change lock mechanism + * Remove old 'lock' configuration (Alexandre Aubin) + * Removed unusted socket import (Alexandre Aubin) + + ## Various fix + ### backup + * [fix] Remove check that domain is resolved locally (Alexandre Aubin) + * [fix] Tell user that domain dns-conf shows a recommendation only (Alexandre Aubin) + * [fix] Backup without info.json (#342) (ljf) + * [fix] Make read-only mount bind actually read-only (#343) (ljf) + ### dyndns + * Regen dnsmasq conf if it's not up to date :| (Alexandre Aubin) + * [fix] timeout on request to avoid blocking process (Laurent Peuch) + * Put request url in an intermediate variable (Alexandre Aubin) + ### other + * clean users.py (Laurent Peuch) + * clean domains.py (Laurent Peuch) + * [enh] add 'yunohost tools shell' (Laurent Peuch) + * Use app_ssowatconf instead of os.system call (Alexandre Aubin) + + Thanks to all contributors <3 ! (Bram, ljf, Aleks, opi) + + -- Laurent Peuch Sat, 19 Aug 2017 23:16:44 +0000 + yunohost (2.7.0) testing; urgency=low Thanks to all contributors <3 ! (Bram, Maniack C, ljf, Aleks, JimboJoe, anmol26s, e-lie, Ozhiganov) From 40ea2f947743babb1f499dff86364323fb36b29c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 20 Aug 2017 21:18:25 +0200 Subject: [PATCH 0444/1066] [mod] pep8 on tools.py --- src/yunohost/tools.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ec73b87e9..c23bb042e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,7 +52,7 @@ from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version # FIXME this is a duplicate from apps.py -APPS_SETTING_PATH= '/etc/yunohost/apps/' +APPS_SETTING_PATH = '/etc/yunohost/apps/' MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations_state.json" logger = getActionLogger('yunohost.tools') @@ -69,7 +69,7 @@ def tools_ldapinit(): auth = init_authenticator(('ldap', 'default'), {'uri': "ldap://localhost:389", 'base_dn': "dc=yunohost,dc=org", - 'user_rdn': "cn=admin" }) + 'user_rdn': "cn=admin"}) auth.authenticate('yunohost') with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f: @@ -114,6 +114,7 @@ def tools_ldapinit(): logger.success(m18n.n('ldap_initialized')) return auth + def tools_adminpw(auth, new_password): """ Change admin password @@ -265,7 +266,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): pass else: dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) + dyndomain = '.'.join(domain.split('.')[1:]) if dyndomain in dyndomains: if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: @@ -319,7 +320,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): if 'redirected_urls' not in ssowat_conf: ssowat_conf['redirected_urls'] = {} - ssowat_conf['redirected_urls']['/'] = domain +'/yunohost/admin' + ssowat_conf['redirected_urls']['/'] = domain + '/yunohost/admin' try: with open('/etc/ssowat/conf.json.persistent', 'w+') as f: @@ -328,7 +329,6 @@ def tools_postinstall(domain, password, ignore_dyndns=False): raise MoulinetteError(errno.EPERM, m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) - os.system('chmod 644 /etc/ssowat/conf.json.persistent') # Create SSL CA @@ -336,8 +336,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' commands = [ 'echo "01" > %s/serial' % ssl_dir, - 'rm %s/index.txt' % ssl_dir, - 'touch %s/index.txt' % ssl_dir, + 'rm %s/index.txt' % ssl_dir, + 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), 'sed -i s/yunohost.org/%s/g %s/openssl.ca.cnf ' % (domain, ssl_dir), 'openssl req -x509 -new -config %s/openssl.ca.cnf -days 3650 -out %s/ca/cacert.pem -keyout %s/ca/cakey.pem -nodes -batch' % (ssl_dir, ssl_dir, ssl_dir), @@ -436,7 +436,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): app_list_installed = os.listdir(APPS_SETTING_PATH) for app_id in app_list_installed: - app_dict = app_info(app_id, raw=True) + app_dict = app_info(app_id, raw=True) if app_dict["upgradable"] == "yes": apps.append({ @@ -484,7 +484,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): # ... and set a hourly cron up to upgrade critical packages if critical_upgrades: logger.info(m18n.n('packages_upgrade_critical_later', - packages=', '.join(critical_upgrades))) + packages=', '.join(critical_upgrades))) with open('/etc/cron.d/yunohost-upgrade', 'w+') as f: f.write('00 * * * * root PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin apt-get install %s -y && rm -f /etc/cron.d/yunohost-upgrade\n' % ' '.join(critical_upgrades)) @@ -526,7 +526,7 @@ def tools_diagnosis(auth, private=False): Return global info about current yunohost instance to help debugging """ - diagnosis = OrderedDict(); + diagnosis = OrderedDict() # Debian release try: @@ -567,15 +567,14 @@ def tools_diagnosis(auth, private=False): disks[disk]['avail'] ) - try: system = monitor_system(units=['cpu', 'memory'], human_readable=True) except MoulinetteError as e: logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1) else: diagnosis['system']['memory'] = { - 'ram' : '%s (%s free)' % (system['memory']['ram']['total'], system['memory']['ram']['free']), - 'swap' : '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']), + 'ram': '%s (%s free)' % (system['memory']['ram']['total'], system['memory']['ram']['free']), + 'swap': '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']), } # Services status @@ -836,7 +835,7 @@ def tools_shell(auth, command=None): logger.warn("You don't have IPython installed, consider installing it as it is way better than the standard shell.") logger.warn("Falling back on the standard shell.") - import readline # will allow Up/Down/History in the console + import readline # will allow Up/Down/History in the console readline # to please pyflakes import code vars = globals().copy() @@ -867,6 +866,7 @@ def _get_migrations_list(): class Migration(object): + def migrate(self): self.forward() From 83971afb9c90c0be29b3797ff3b11b515fb88e16 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 20 Aug 2017 21:19:04 +0200 Subject: [PATCH 0445/1066] [mod] pep8 on hooks.py --- src/yunohost/hook.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index cf9990a4d..1f971edb6 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -50,14 +50,16 @@ def hook_add(app, file): path, filename = os.path.split(file) priority, action = _extract_filename_parts(filename) - try: os.listdir(CUSTOM_HOOK_FOLDER + action) - except OSError: os.makedirs(CUSTOM_HOOK_FOLDER + action) + try: + os.listdir(CUSTOM_HOOK_FOLDER + action) + except OSError: + os.makedirs(CUSTOM_HOOK_FOLDER + action) - finalpath = CUSTOM_HOOK_FOLDER + action +'/'+ priority +'-'+ app + finalpath = CUSTOM_HOOK_FOLDER + action + '/' + priority + '-' + app os.system('cp %s %s' % (file, finalpath)) os.system('chown -hR admin: %s' % HOOK_FOLDER) - return { 'hook': finalpath } + return {'hook': finalpath} def hook_remove(app): @@ -72,8 +74,9 @@ def hook_remove(app): for action in os.listdir(CUSTOM_HOOK_FOLDER): for script in os.listdir(CUSTOM_HOOK_FOLDER + action): if script.endswith(app): - os.remove(CUSTOM_HOOK_FOLDER + action +'/'+ script) - except OSError: pass + os.remove(CUSTOM_HOOK_FOLDER + action + '/' + script) + except OSError: + pass def hook_info(action, name): @@ -134,11 +137,11 @@ def hook_list(action, list_by='name', show_info=False): def _append_hook(d, priority, name, path): # Use the priority as key and a dict of hooks names # with their info as value - value = { 'path': path } + value = {'path': path} try: d[priority][name] = value except KeyError: - d[priority] = { name: value } + d[priority] = {name: value} else: def _append_hook(d, priority, name, path): # Use the priority as key and the name as value @@ -160,11 +163,12 @@ def hook_list(action, list_by='name', show_info=False): if h['path'] != path: h['path'] = path return - l.append({ 'priority': priority, 'path': path }) + l.append({'priority': priority, 'path': path}) d[name] = l else: if list_by == 'name': result = set() + def _append_hook(d, priority, name, path): # Add only the name d.add(name) @@ -202,7 +206,7 @@ def hook_list(action, list_by='name', show_info=False): logger.debug("custom hook folder not found for action '%s' in %s", action, CUSTOM_HOOK_FOLDER) - return { 'hooks': result } + return {'hooks': result} def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, @@ -224,7 +228,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, (name, priority, path, succeed) as arguments """ - result = { 'succeed': {}, 'failed': {} } + result = {'succeed': {}, 'failed': {}} hooks_dict = {} # Retrieve hooks @@ -242,7 +246,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, for n in hooks: for key in hooks_names.keys(): if key == n or key.startswith("%s_" % n) \ - and key not in all_hooks: + and key not in all_hooks: all_hooks.append(key) # Iterate over given hooks names list @@ -256,7 +260,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, for h in hl: # Update hooks dict d = hooks_dict.get(h['priority'], dict()) - d.update({ n: { 'path': h['path'] }}) + d.update({n: {'path': h['path']}}) hooks_dict[h['priority']] = d if not hooks_dict: return result @@ -343,10 +347,10 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: # use xtrace on fd 7 which is redirected to stdout cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' - + # prepend environment variables cmd = '{0} {1}'.format( - ' '.join(['{0}={1}'.format(k, shell_quote(v)) \ + ' '.join(['{0}={1}'.format(k, shell_quote(v)) for k, v in env.items()]), cmd) command.append(cmd.format(script=cmd_script, args=cmd_args)) @@ -392,6 +396,7 @@ def _extract_filename_parts(filename): _find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.UNICODE).search + def shell_quote(s): """Return a shell-escaped version of the string *s*.""" s = str(s) From 40a5bcc96bb33f721f4fc7fb28531106c5ea4815 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 20 Aug 2017 22:49:21 +0200 Subject: [PATCH 0446/1066] [mod] pep8 on backup.py --- src/yunohost/backup.py | 88 +++++++++++++----------------------------- 1 file changed, 26 insertions(+), 62 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 45def947c..548e4aa6b 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -75,7 +75,6 @@ class BackupRestoreTargetsManager(object): "apps": {} } - def set_result(self, category, element, value): """ Change (or initialize) the current status/result of a given target. @@ -89,7 +88,7 @@ class BackupRestoreTargetsManager(object): "Warning", "Error" and "Skipped" """ - levels = [ "Unknown", "Success", "Warning", "Error", "Skipped" ] + levels = ["Unknown", "Success", "Warning", "Error", "Skipped"] assert value in levels @@ -102,7 +101,6 @@ class BackupRestoreTargetsManager(object): else: self.results[category][element] = value - def set_wanted(self, category, wanted_targets, available_targets, error_if_wanted_target_is_unavailable): @@ -138,12 +136,12 @@ class BackupRestoreTargetsManager(object): # validate that each target is actually available else: self.targets[category] = [part for part in wanted_targets - if part in available_targets] + if part in available_targets] # Display an error for each target asked by the user but which is # unknown unavailable_targets = [part for part in wanted_targets - if part not in available_targets] + if part not in available_targets] for target in unavailable_targets: self.set_result(category, target, "Skipped") @@ -156,7 +154,6 @@ class BackupRestoreTargetsManager(object): return self.list(category, exclude=["Skipped"]) - def list(self, category, include=None, exclude=None): """ List targets in a given category. @@ -170,11 +167,11 @@ class BackupRestoreTargetsManager(object): if include: return [target for target in self.targets[category] - if self.results[category][target] in include] + if self.results[category][target] in include] if exclude: return [target for target in self.targets[category] - if self.results[category][target] not in exclude] + if self.results[category][target] not in exclude] class BackupManager(): @@ -258,7 +255,6 @@ class BackupManager(): } self.targets = BackupRestoreTargetsManager() - # Define backup name if needed if not name: name = self._define_backup_name() @@ -330,7 +326,7 @@ class BackupManager(): self.work_dir) # FIXME May be we should clean the workdir here raise MoulinetteError( - errno.EIO, m18n.n('backup_output_directory_not_empty')) + errno.EIO, m18n.n('backup_output_directory_not_empty')) ########################################################################### # Backup target management # @@ -352,7 +348,6 @@ class BackupManager(): system_parts, hook_list('backup')["hooks"], unknown_error) - def set_apps_targets(self, apps=[]): """ Define and validate targetted apps to be backuped @@ -385,7 +380,6 @@ class BackupManager(): logger.warning(m18n.n('backup_with_no_restore_script_for_app', app=app)) self.targets.set_result("apps", app, "Warning") - ########################################################################### # Management of files to backup / "The CSV" # ########################################################################### @@ -400,7 +394,6 @@ class BackupManager(): """ _call_for_each_path(self, BackupManager._add_to_list_to_backup, tmp_csv) - def _add_to_list_to_backup(self, source, dest=None): """ Mark file or directory to backup @@ -431,7 +424,6 @@ class BackupManager(): dest = os.path.join(dest, os.path.basename(source)) self.paths_to_backup.append({'source': source, 'dest': dest}) - def _write_csv(self): """ Write the backup list into a CSV @@ -473,7 +465,6 @@ class BackupManager(): logger.error(m18n.n('backup_csv_addition_failed')) self.csv_file.close() - ########################################################################### # File collection from system parts and apps # ########################################################################### @@ -516,7 +507,6 @@ class BackupManager(): filesystem.rm(self.work_dir, True, True) raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) - # Add unlisted files from backup tmp dir self._add_to_list_to_backup('backup.csv') self._add_to_list_to_backup('info.json') @@ -537,7 +527,6 @@ class BackupManager(): with open("%s/info.json" % self.work_dir, 'w') as f: f.write(json.dumps(self.info)) - def _get_env_var(self, app=None): """ Define environment variables for apps or system backup scripts. @@ -567,7 +556,6 @@ class BackupManager(): return env_var - def _collect_system_files(self): """ List file to backup for each selected system part @@ -633,7 +621,6 @@ class BackupManager(): logger.error(m18n.n('backup_system_part_failed', part=part)) self.targets.set_result("system", part, "Error") - def _collect_apps_files(self): """ Prepare backup for each selected apps """ @@ -642,7 +629,6 @@ class BackupManager(): for app_instance_name in apps_targets: self._collect_app_files(app_instance_name) - def _collect_app_files(self, app): """ List files to backup for the app into the paths_to_backup dict. @@ -716,7 +702,6 @@ class BackupManager(): filesystem.rm(tmp_script, force=True) filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) - ########################################################################### # Actual backup archive creation / method management # ########################################################################### @@ -733,7 +718,6 @@ class BackupManager(): """ self.methods.append(method) - def backup(self): """Apply backup methods""" @@ -742,7 +726,6 @@ class BackupManager(): method.mount_and_backup(self) logger.info(m18n.n('backup_method_' + method.method_name + '_finished')) - def _compute_backup_size(self): """ Compute backup global size and details size for each apps and system @@ -777,7 +760,7 @@ class BackupManager(): category = splitted_dest[0] if category == 'apps': for app_key in self.apps_return: - if row['dest'].startswith('apps/'+app_key): + if row['dest'].startswith('apps/' + app_key): self.size_details['apps'][app_key] += size break # OR Add size to the correct system element @@ -854,7 +837,6 @@ class RestoreManager(): return len(successful_apps) != 0 \ or len(successful_system) != 0 - def _read_info_files(self): """ Read the info file from inside an archive @@ -878,7 +860,6 @@ class RestoreManager(): logger.debug("restoring from backup '%s' created on %s", self.name, time.ctime(self.info['created_at'])) - def _postinstall_if_needed(self): """ Post install yunohost if needed @@ -903,7 +884,6 @@ class RestoreManager(): logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) - def clean(self): """ End a restore operations by cleaning the working directory and @@ -923,7 +903,6 @@ class RestoreManager(): logger.warning(m18n.n('restore_cleaning_failed')) filesystem.rm(self.work_dir, True, True) - ########################################################################### # Restore target manangement # ########################################################################### @@ -1002,7 +981,6 @@ class RestoreManager(): self.info['apps'].keys(), unknown_error) - ########################################################################### # Archive mounting # ########################################################################### @@ -1138,7 +1116,6 @@ class RestoreManager(): finally: self.clean() - def _restore_system(self): """ Restore user and system parts """ @@ -1166,7 +1143,6 @@ class RestoreManager(): service_regen_conf() - def _restore_apps(self): """Restore all apps targeted""" @@ -1175,7 +1151,6 @@ class RestoreManager(): for app in apps_targets: self._restore_app(app) - def _restore_app(self, app_instance_name): """ Restore an app @@ -1236,7 +1211,7 @@ class RestoreManager(): try: # Restore app settings app_settings_new_path = os.path.join('/etc/yunohost/apps/', - app_instance_name) + app_instance_name) app_scripts_new_path = os.path.join(app_settings_new_path, 'scripts') shutil.copytree(app_settings_in_archive, app_settings_new_path) filesystem.chmod(app_settings_new_path, 0400, 0400, True) @@ -1285,7 +1260,6 @@ class RestoreManager(): # Cleaning app directory shutil.rmtree(app_settings_new_path, ignore_errors=True) - # TODO Cleaning app hooks else: self.targets.set_result("apps", app_instance_name, "Success") @@ -1293,7 +1267,6 @@ class RestoreManager(): # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) - def _get_env_var(self, app=None): """ Define environment variable for hooks call """ env_var = {} @@ -1318,6 +1291,7 @@ class RestoreManager(): # Backup methods # ############################################################################### + class BackupMethod(object): """ BackupMethod is an abstract class that represents a way to backup and @@ -1364,7 +1338,8 @@ class BackupMethod(object): method = BackupMethod.create("copy") method.mount(restore_manager) """ - def __init__(self, repo = None): + + def __init__(self, repo=None): """ BackupMethod constructors @@ -1479,10 +1454,10 @@ class BackupMethod(object): """ mount_lines = subprocess.check_output("mount").split("\n") - points_to_umount = [ line.split(" ")[2] + points_to_umount = [line.split(" ")[2] for line in mount_lines - if len(line) >= 3 - and line.split(" ")[2].startswith(directory) ] + if len(line) >= 3 + and line.split(" ")[2].startswith(directory)] ret = 0 for point in reversed(points_to_umount): ret = subprocess.call(["umount", point]) @@ -1556,8 +1531,8 @@ class BackupMethod(object): # To check if dest is mounted, use /proc/mounts that # escape spaces as \040 raw_mounts = read_file("/proc/mounts").strip().split('\n') - mounts = [ m.split()[1] for m in raw_mounts ] - mounts = [ m.replace("\\040", " ") for m in mounts ] + mounts = [m.split()[1] for m in raw_mounts] + mounts = [m.replace("\\040", " ") for m in mounts] if dest in mounts: subprocess.check_call(["umount", "-R", dest]) else: @@ -1585,7 +1560,7 @@ class BackupMethod(object): # Compute size to copy size = sum(disk_usage(path['source']) for path in paths_needed_to_be_copied) - size /= (1024 * 1024) # Convert bytes to megabytes + size /= (1024 * 1024) # Convert bytes to megabytes # Ask confirmation for copying if size > MB_ALLOWED_TO_ORGANIZE: @@ -1632,7 +1607,7 @@ class BackupMethod(object): bm_class = { 'copy': CopyBackupMethod, - 'tar': TarBackupMethod, + 'tar': TarBackupMethod, 'borg': BorgBackupMethod } if method in ["copy", "tar", "borg"]: @@ -1646,7 +1621,8 @@ class CopyBackupMethod(BackupMethod): This class just do an uncompress copy of each file in a location, and could be the inverse for restoring """ - def __init__(self, repo = None): + + def __init__(self, repo=None): super(CopyBackupMethod, self).__init__(repo) @property @@ -1712,18 +1688,15 @@ class TarBackupMethod(BackupMethod): def __init__(self, repo=None): super(TarBackupMethod, self).__init__(repo) - @property def method_name(self): return 'tar' - @property def _archive_file(self): """Return the compress archive path""" return os.path.join(self.repo, self.name + '.tar.gz') - def backup(self): """ Compress prepared files @@ -1774,7 +1747,6 @@ class TarBackupMethod(BackupMethod): if not os.path.isfile(link): os.symlink(self._archive_file, link) - def mount(self, restore_manager): """ Mount the archive. We avoid copy to be able to restore on system without @@ -1845,7 +1817,6 @@ class TarBackupMethod(BackupMethod): ] tar.extractall(members=subdir_and_files, path=self.work_dir) - # Extract apps backup for app in apps_targets: subdir_and_files = [ @@ -1855,26 +1826,23 @@ class TarBackupMethod(BackupMethod): tar.extractall(members=subdir_and_files, path=self.work_dir) - class BorgBackupMethod(BackupMethod): @property def method_name(self): return 'borg' - def backup(self): """ Backup prepared files with borg """ super(CopyBackupMethod, self).backup() # TODO run borg create command raise MoulinetteError( - errno.EIO, m18n.n('backup_borg_not_implemented')) - + errno.EIO, m18n.n('backup_borg_not_implemented')) def mount(self, mnt_path): raise MoulinetteError( - errno.EIO, m18n.n('backup_borg_not_implemented')) + errno.EIO, m18n.n('backup_borg_not_implemented')) class CustomBackupMethod(BackupMethod): @@ -1883,18 +1851,17 @@ class CustomBackupMethod(BackupMethod): backup/restore operations. A user can add his own hook inside /etc/yunohost/hooks.d/backup_method/ """ - def __init__(self, repo = None, method = None,**kwargs): + + def __init__(self, repo=None, method=None, **kwargs): super(CustomBackupMethod, self).__init__(repo) self.args = kwargs self.method = method self._need_mount = None - @property def method_name(self): return 'borg' - def need_mount(self): """Call the backup_method hook to know if we need to organize files @@ -1910,7 +1877,6 @@ class CustomBackupMethod(BackupMethod): self._need_mount = True if ret['succeed'] else False return self._need_mount - def backup(self): """ Launch a custom script to backup @@ -1939,7 +1905,6 @@ class CustomBackupMethod(BackupMethod): raise MoulinetteError(errno.EIO, m18n.n('backup_custom_mount_error')) - def _get_args(self, action): """Return the arguments to give to the custom script""" return [action, self.work_dir, self.name, self.repo, self.manager.size, @@ -2006,7 +1971,7 @@ def backup_create(name=None, description=None, methods=[], # Check for forbidden folders if output_directory.startswith(ARCHIVES_PATH) or \ - re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', + re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): raise MoulinetteError(errno.EINVAL, m18n.n('backup_output_directory_forbidden')) @@ -2172,7 +2137,6 @@ def backup_restore(auth, name, restore_manager.mount() restore_manager.restore() - # Check if something has been restored if restore_manager.success: logger.success(m18n.n('restore_complete')) @@ -2334,6 +2298,7 @@ def backup_delete(name): # Misc helpers # ############################################################################### + def _create_archive_dir(): """ Create the YunoHost archives directory if doesn't exist """ if not os.path.isdir(ARCHIVES_PATH): @@ -2361,4 +2326,3 @@ def disk_usage(path): du_output = subprocess.check_output(['du', '-sb', path]) return int(du_output.split()[0].decode('utf-8')) - From 8117ffb85fa4c7ab8a6e3be7dc346b228224f35d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 20 Aug 2017 22:52:51 +0200 Subject: [PATCH 0447/1066] [mod] pep8 on app.py --- src/yunohost/app.py | 7 +++---- src/yunohost/backup.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bb81efeeb..025de2e19 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1544,7 +1544,7 @@ def _fetch_app_from_git(app): else: raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) - if not 'git' in app_info: + if 'git' not in app_info: raise MoulinetteError(errno.EINVAL, m18n.n('app_unsupported_remote_type')) url = app_info['git']['url'] @@ -1852,9 +1852,9 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): # is an available url and normalize the path. domain_args = [arg["name"] for arg in action_args - if arg.get("type","string") == "domain"] + if arg.get("type", "string") == "domain"] path_args = [arg["name"] for arg in action_args - if arg.get("type","string") == "path"] + if arg.get("type", "string") == "path"] if len(domain_args) == 1 and len(path_args) == 1: @@ -1871,7 +1871,6 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): # standard path format to deal with no matter what the user inputted) args_dict[path_args[0]] = path - return args_dict diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 548e4aa6b..d794a2fca 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1456,8 +1456,7 @@ class BackupMethod(object): points_to_umount = [line.split(" ")[2] for line in mount_lines - if len(line) >= 3 - and line.split(" ")[2].startswith(directory)] + if len(line) >= 3 and line.split(" ")[2].startswith(directory)] ret = 0 for point in reversed(points_to_umount): ret = subprocess.call(["umount", point]) @@ -1945,7 +1944,7 @@ def backup_create(name=None, description=None, methods=[], ########################################################################### # Historical, deprecated options - if ignore_hooks != False: + if ignore_hooks is not False: logger.warning("--ignore-hooks is deprecated and will be removed in the" "future. Please use --ignore-system instead.") ignore_system = ignore_hooks @@ -2073,7 +2072,7 @@ def backup_restore(auth, name, ########################################################################### # Historical, deprecated options - if ignore_hooks != False: + if ignore_hooks is not False: logger.warning("--ignore-hooks is deprecated and will be removed in the" "future. Please use --ignore-system instead.") ignore_system = ignore_hooks From d5b6072c84cc8c4110181e4975372b9d51a55e2b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Aug 2017 15:27:52 +0200 Subject: [PATCH 0448/1066] [fix] Explicitly require moulinette and ssowat >= 2.7.1 --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 4cd128cbe..dcdd0dd9a 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Homepage: https://yunohost.org/ Package: yunohost Architecture: all Depends: ${python:Depends}, ${misc:Depends} - , moulinette (>= 2.3.5.1) + , moulinette (>= 2.7.1), ssowat (>= 2.7.1) , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc , glances @@ -23,7 +23,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname - , ssowat, metronome + , metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools , haveged Recommends: yunohost-admin From 950359061ef52e84b9aeda7f31e03c471c5b7248 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Aug 2017 02:02:44 +0200 Subject: [PATCH 0449/1066] [fix] Set firewall start as background task (to be done right after postinstall) to avoid lock issues --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index c23bb042e..43407ad85 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -387,7 +387,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): # Enable and start YunoHost firewall at boot time os.system('update-rc.d yunohost-firewall enable') - os.system('service yunohost-firewall start') + os.system('service yunohost-firewall start &') service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured')) From 32d3a9940b1d9ae752026fdbd26cbe4a30c8a387 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Aug 2017 21:26:15 -0400 Subject: [PATCH 0450/1066] Update changelog for 2.7.2 release --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index c2f4a6783..312c626e3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (2.7.2) stable; urgency=low + + * [mod] pep8 + * [fix] Explicitly require moulinette and ssowat >= 2.7.1 + * [fix] Set firewall start as background task (to be done right after postinstall) to avoid lock issues + +Thanks to all contributors <3 ! (Bram, Alex) + + -- Alexandre Aubin Tue, 22 Aug 2017 21:25:17 -0400 + yunohost (2.7.1) testing; urgency=low ## Security: uses sha-512 to store password and auto upgrade old password on login From bb2424e9b878ec3f573b6224d302073ddcff67db Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 23 Aug 2017 17:25:16 +0200 Subject: [PATCH 0451/1066] [enh] Escape some special character in ynh_replace_string --- data/helpers.d/string | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index 772681fb9..3e68b382d 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -18,8 +18,18 @@ ynh_string_random() { # | arg: target_file - File in which the string will be replaced. ynh_replace_string () { delimit=@ - match_string=${1//${delimit}/"\\${delimit}"} # Escape the delimiter if it's in the string. + # Escape the delimiter if it's in the string. + match_string=${1//${delimit}/"\\${delimit}"} replace_string=${2//${delimit}/"\\${delimit}"} + + # Escape any backslash to preserve them as simple backslash. + match_string=${match_string//\\/"\\\\"} + replace_string=${replace_string//\\/"\\\\"} + + # Escape the & character, who has a special function in sed. + match_string=${match_string//&/"\&"} + replace_string=${replace_string//&/"\&"} + workfile=$3 sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" From f541edbb2a3062d6462addcde5b665add4c2d6f9 Mon Sep 17 00:00:00 2001 From: ariasuni Date: Sat, 26 Aug 2017 20:24:38 +0200 Subject: [PATCH 0452/1066] [enh] enable gzip compression for common text mimetypes in Nginx --- data/templates/nginx/plain/global.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/plain/global.conf b/data/templates/nginx/plain/global.conf index b3a5f356a..a3096d009 100644 --- a/data/templates/nginx/plain/global.conf +++ b/data/templates/nginx/plain/global.conf @@ -1 +1,2 @@ server_tokens off; +gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; From 8e45f960a7df570d923a00bd2b9400d55bac5843 Mon Sep 17 00:00:00 2001 From: ariasuni Date: Sun, 27 Aug 2017 03:37:32 +0200 Subject: [PATCH 0453/1066] [fix] Use --value="$3" syntax so that $3 can begin with a dash --- data/helpers.d/setting | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index 89d1f64e5..ad036ba4f 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -14,7 +14,7 @@ ynh_app_setting_get() { # | arg: key - the setting name to set # | arg: value - the setting value to set ynh_app_setting_set() { - sudo yunohost app setting "$1" "$2" -v "$3" --quiet + sudo yunohost app setting "$1" "$2" --value="$3" --quiet } # Delete an application setting From c6b5284be8da39cf2da4e1036a730eb5e0515096 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 27 Aug 2017 05:42:33 +0200 Subject: [PATCH 0454/1066] [fix] use cryptorandom to generate password --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 025de2e19..84092580f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2104,4 +2104,4 @@ def random_password(length=8): import random char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase - return ''.join(random.sample(char_set, length)) + return ''.join([random.SystemRandom().choice(char_set) for x in range(length)]) From e1b5eead3f24535e4825ac080edf65f5157c7623 Mon Sep 17 00:00:00 2001 From: ariasuni Date: Mon, 28 Aug 2017 16:40:50 +0200 Subject: [PATCH 0455/1066] [fix] disable gzip compression for json to avoid BREACH attack --- data/templates/nginx/plain/global.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/global.conf b/data/templates/nginx/plain/global.conf index a3096d009..341f08620 100644 --- a/data/templates/nginx/plain/global.conf +++ b/data/templates/nginx/plain/global.conf @@ -1,2 +1,2 @@ server_tokens off; -gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; +gzip_types text/plain text/css application/javascript text/xml application/xml application/xml+rss text/javascript; From 171258e7faa3f310f2a6024b493c832a04280fc0 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 28 Aug 2017 17:17:19 +0200 Subject: [PATCH 0456/1066] [enh] Return log after app upgrade with api --- src/yunohost/app.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 025de2e19..476723000 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -38,7 +38,7 @@ import pwd import grp from collections import OrderedDict -from moulinette import msignals, m18n +from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -531,6 +531,9 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec + # Retrieve interface + is_api = msettings.get('interface') == 'api' + try: app_list() except MoulinetteError: @@ -632,6 +635,10 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('upgrade_complete')) + # Return API logs if it is an API call + if is_api: + return {"log": service_log('yunohost-api', number="100").values()[0]} + def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ From fea6d3568ffd55f38f23c99cea8f71176bde8b98 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Aug 2017 18:00:23 +0200 Subject: [PATCH 0457/1066] [fix] previous string was expecting an app name but we don't have any --- locales/en.json | 1 + src/yunohost/tools.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index dd2dae240..80ff22655 100644 --- a/locales/en.json +++ b/locales/en.json @@ -35,6 +35,7 @@ "app_unknown": "Unknown app", "app_unsupported_remote_type": "Unsupported remote type used for the app", "app_upgrade_failed": "Unable to upgrade {app:s}", + "app_upgrade_some_app_failed": "Unable to upgrade some applications", "app_upgraded": "{app:s} has been upgraded", "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 43407ad85..e172fa8e5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -511,7 +511,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): except Exception as e: failure = True logger.warning('unable to upgrade apps: %s' % str(e)) - logger.error(m18n.n('app_upgrade_failed')) + logger.error(m18n.n('app_upgrade_some_app_failed')) if not failure: logger.success(m18n.n('system_upgraded')) From 75d80848524c00cf85323ada62fb627ee6c93a31 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 28 Aug 2017 21:08:45 +0200 Subject: [PATCH 0458/1066] [enh] Add hooks on app management operations --- src/yunohost/app.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 476723000..5560fabaf 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -430,7 +430,7 @@ def app_change_url(auth, app, domain, path): path -- New path at which the application will be move """ - from yunohost.hook import hook_exec + from yunohost.hook import hook_exec, hook_callback installed = _is_installed(app) if not installed: @@ -518,6 +518,8 @@ def app_change_url(auth, app, domain, path): logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) + hook_callback('post_app_change_url', args=args_list, env=env_dict) + def app_upgrade(auth, app=[], url=None, file=None): """ @@ -529,7 +531,8 @@ def app_upgrade(auth, app=[], url=None, file=None): url -- Git url to fetch for upgrade """ - from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback + # Retrieve interface is_api = msettings.get('interface') == 'api' @@ -628,6 +631,9 @@ def app_upgrade(auth, app=[], url=None, file=None): upgraded_apps.append(app_instance_name) logger.success(m18n.n('app_upgraded', app=app_instance_name)) + hook_callback('post_app_upgrade', args=args_list, env=env_dict) + + if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -651,7 +657,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): no_remove_on_failure -- Debug option to avoid removing the app on a failed installation """ - from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback # Fetch or extract sources try: @@ -790,6 +796,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): logger.success(m18n.n('installation_complete')) + hook_callback('post_app_install', args=args_list, env=env_dict) + def app_remove(auth, app): """ @@ -799,7 +807,7 @@ def app_remove(auth, app): app -- App(s) to delete """ - from yunohost.hook import hook_exec, hook_remove + from yunohost.hook import hook_exec, hook_remove, hook_callback if not _is_installed(app): raise MoulinetteError(errno.EINVAL, @@ -828,6 +836,8 @@ def app_remove(auth, app): if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) + hook_callback('post_app_remove', args=args_list, env=env_dict) + if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) shutil.rmtree('/tmp/yunohost_remove') From fa40a5cf3b0f3f46c54e61b29094c9ab486b3c9a Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 1 Sep 2017 03:10:31 +0200 Subject: [PATCH 0459/1066] [enh] Bootprompt is back --- bin/yunoprompt | 103 ++++++++++++++++++++++++++++++++++ data/other/yunoprompt.service | 14 +++++ debian/postinst | 3 + 3 files changed, 120 insertions(+) create mode 100644 bin/yunoprompt create mode 100644 data/other/yunoprompt.service diff --git a/bin/yunoprompt b/bin/yunoprompt new file mode 100644 index 000000000..212debdc9 --- /dev/null +++ b/bin/yunoprompt @@ -0,0 +1,103 @@ +#!/bin/bash + +ip=$(hostname --all-ip-address) + +i=0 +for key in /etc/ssh/ssh_host_*_key.pub ; do + output=$(ssh-keygen -l -f $key) + fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)" + i=$(($i + 1)) +done + +cat > /etc/issue.net << EOF + '. ' '' -d. + /M+ h- .shh/ // /NMy- hMdosso + 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ + sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' + .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. + mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: + 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - + hy /yy: :- -. -Nh ' + . + +IP: ${ip} +SSH fingerprints: +${fingerprint[0]} +${fingerprint[1]} +${fingerprint[2]} +${fingerprint[3]} +${fingerprint[4]} +EOF +if [[ ! -f /etc/yunohost/installed ]] +then + if [[ ! -f /etc/yunohost/from_script ]] + then +sleep 5 +chvt 2 +cat << EOF + '. ' '' -d. + /M+ h- .shh/ // /NMy- hMdosso + 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ + sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' + .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. + mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: + 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - + hy /yy: :- -. -Nh ' + . + +IP: ${ip} +SSH fingerprints*: +${fingerprint[0]} +${fingerprint[1]} +${fingerprint[2]} +${fingerprint[3]} +${fingerprint[4]} +EOF + +echo -e "\e[m Post-installation \e[0m" +cat << EOF +Congratulations! YunoHost has been successfully installed. +Two more steps are required to activate the services of your server. +EOF +read -p "Proceed to post-installation? (y/n) " -n 1 + RESULT=1 + while [ $RESULT -gt 0 ]; do + if [[ $REPLY =~ ^[Nn]$ ]]; then + chvt 1 + exit 0 + fi + echo -e "\n" + /usr/bin/yunohost tools postinstall + let RESULT=$? + if [ $RESULT -gt 0 ]; then + echo -e "\n" + read -p "Retry? (y/n) " -n 1 + fi + done + fi +else # YunoHost is already post-installed + + domain=$(cat /etc/yunohost/current_host) +cat > /etc/issue << EOF + '. ' '' -d. + /M+ h- .shh/ // /NMy- hMdosso + 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ + sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' + .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. + mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: + 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - + hy /yy: :- -. -Nh ' + . + +IP: ${ip} +SSH fingerprints: +${fingerprint[0]} +${fingerprint[1]} +${fingerprint[2]} +${fingerprint[3]} +${fingerprint[4]} +EOF + + +fi + diff --git a/data/other/yunoprompt.service b/data/other/yunoprompt.service new file mode 100644 index 000000000..3c4df50f9 --- /dev/null +++ b/data/other/yunoprompt.service @@ -0,0 +1,14 @@ +[Unit] +Description=YunoHost boot prompt +After=getty@tty2.service + +[Service] +Type=simple +ExecStart=/usr/bin/yunoprompt +StandardInput=tty +TTYPath=/dev/tty2 +TTYReset=yes +TTYVHangup=yes + +[Install] +WantedBy=default.target diff --git a/debian/postinst b/debian/postinst index 7e91ffbb3..39063694b 100644 --- a/debian/postinst +++ b/debian/postinst @@ -24,6 +24,9 @@ do_configure() { "consider to start it by doing 'service yunohost-firewall start'." fi + # Yunoprompt + systemctl enable yunoprompt.service + # remove old PAM config and update it [[ ! -f /usr/share/pam-configs/my_mkhomedir ]] \ || rm /usr/share/pam-configs/my_mkhomedir From 6bbcf0ee95e03781f30b89a156f6f8ba9f7dbc2f Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 1 Sep 2017 03:16:52 +0200 Subject: [PATCH 0460/1066] [fix] Missing install path --- debian/install | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/install b/debian/install index 70add7992..e9c79e963 100644 --- a/debian/install +++ b/debian/install @@ -3,6 +3,7 @@ sbin/* /usr/sbin/ data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ +data/other/yunoprompt.service /etc/systemd/system/ data/other/* /usr/share/yunohost/yunohost-config/moulinette/ data/templates/* /usr/share/yunohost/templates/ data/helpers /usr/share/yunohost/ From f4955341bb5ad71969eb765915509708ebb70d0f Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Tue, 5 Sep 2017 21:09:24 +0200 Subject: [PATCH 0461/1066] Complete ynh_replace_string helper comments to mention the possible use of regexp --- data/helpers.d/string | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index 772681fb9..b77b42ef7 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -10,12 +10,16 @@ ynh_string_random() { | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' } -# Substitute/replace a string by another in a file +# Substitute/replace a string (or expression) by another in a file # # usage: ynh_replace_string match_string replace_string target_file # | arg: match_string - String to be searched and replaced in the file # | arg: replace_string - String that will replace matches # | arg: target_file - File in which the string will be replaced. +# +# As this helper is based on sed command, regular expressions and +# references to sub-expressions can be used +# (see sed manual page for more information) ynh_replace_string () { delimit=@ match_string=${1//${delimit}/"\\${delimit}"} # Escape the delimiter if it's in the string. From 0dbec4fa62beebdb173f58aec5dee6a40ffa967d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 6 Sep 2017 22:00:17 +0200 Subject: [PATCH 0462/1066] [enh] add debugging in ldap init (#365) --- src/yunohost/tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e172fa8e5..22ac7894f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -78,14 +78,14 @@ def tools_ldapinit(): for rdn, attr_dict in ldap_map['parents'].items(): try: auth.add(rdn, attr_dict) - except: - pass + except Exception as e: + logger.warn("Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) for rdn, attr_dict in ldap_map['children'].items(): try: auth.add(rdn, attr_dict) - except: - pass + except Exception as e: + logger.warn("Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) admin_dict = { 'cn': 'admin', From 85f0b02f1fe87cc6794d4f299329299a26ed877e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 7 Sep 2017 15:06:12 +0200 Subject: [PATCH 0463/1066] [fix] '/' was resulting in '//' --- src/yunohost/app.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 84092580f..fe71cfd85 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -445,8 +445,9 @@ def app_change_url(auth, app, domain, path): # Normalize path and domain format domain = domain.strip().lower() - old_path = '/' + old_path.strip("/").strip() + '/' - path = '/' + path.strip("/").strip() + '/' + + old_path = normalize_url_path(old_path) + path = normalize_url_path(path) if (domain, path) == (old_domain, old_path): raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path)) @@ -2105,3 +2106,10 @@ def random_password(length=8): char_set = string.ascii_uppercase + string.digits + string.ascii_lowercase return ''.join([random.SystemRandom().choice(char_set) for x in range(length)]) + + +def normalize_url_path(url_path): + if url_path.strip("/").strip(): + return '/' + url_path.strip("/").strip() + '/' + + return "/" From 1086a50eb2d8c9400f3da347862916eeee746291 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Wed, 13 Sep 2017 19:35:29 +0200 Subject: [PATCH 0464/1066] Fix #1005 (add missing 'ask_path' key) (#369) --- locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/en.json b/locales/en.json index 80ff22655..9ef41a3f2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -55,6 +55,7 @@ "ask_main_domain": "Main domain", "ask_new_admin_password": "New administration password", "ask_password": "Password", + "ask_path": "Path", "backup_abstract_method": "This backup method hasn't yet been implemented", "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", From 8a559f2ae1de4fe03d0b4636d84c518b88ce1347 Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Thu, 14 Sep 2017 22:12:01 +0200 Subject: [PATCH 0465/1066] Make MySQL dumps with a single transaction to ensure backup consistency --- data/helpers.d/mysql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 1c0ece114..0774adbce 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -73,7 +73,7 @@ ynh_mysql_drop_db() { # | arg: db - the database name to dump # | ret: the mysqldump output ynh_mysql_dump_db() { - mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" "$1" + mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" --single-transaction "$1" } # Create a user From 0fc80b256bc38fb244a70e7364a2400360424e68 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 18 Sep 2017 21:17:13 +0200 Subject: [PATCH 0466/1066] [mod] constant needs to be upper cases --- src/yunohost/dyndns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 621043c3e..7bbf5e568 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -63,7 +63,7 @@ class IPRouteLine(object): for k, v in self.m.groupdict().items(): setattr(self, k, v) -re_dyndns_private_key = re.compile( +RE_DYNDNS_PRIVATE_KEY = re.compile( r'.*/K(?P[^\s\+]+)\.\+157.+\.private$' ) @@ -176,7 +176,7 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, if domain is None: # Retrieve the first registered domain for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): - match = re_dyndns_private_key.match(path) + match = RE_DYNDNS_PRIVATE_KEY.match(path) if not match: continue _domain = match.group('domain') From 622bed116935fcb9740b3c3551eb7fadfe05776f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 18 Sep 2017 21:29:01 +0200 Subject: [PATCH 0467/1066] [mod] introduce regex for SHA512 --- src/yunohost/dyndns.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 7bbf5e568..842fdb841 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -63,11 +63,17 @@ class IPRouteLine(object): for k, v in self.m.groupdict().items(): setattr(self, k, v) -RE_DYNDNS_PRIVATE_KEY = re.compile( + +RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( r'.*/K(?P[^\s\+]+)\.\+157.+\.private$' ) +RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( + r'.*/K(?P[^\s\+]+)\.\+163.+\.private$' +) + + def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -176,7 +182,7 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, if domain is None: # Retrieve the first registered domain for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): - match = RE_DYNDNS_PRIVATE_KEY.match(path) + match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) if not match: continue _domain = match.group('domain') From b53b05eabda62cefb0a9c56e60f30995fb4cb406 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 19 Sep 2017 00:08:56 +0200 Subject: [PATCH 0468/1066] [enh] register tsig as hmac-sha512 --- src/yunohost/dyndns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 842fdb841..e46598fbd 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -101,7 +101,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None logger.info(m18n.n('dyndns_key_generating')) os.system('cd /etc/yunohost/dyndns && ' - 'dnssec-keygen -a hmac-md5 -b 128 -r /dev/urandom -n USER %s' % domain) + 'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain) os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0] @@ -110,7 +110,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: - r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) + r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: From 2c73dd1a2f7529aa85989175715c2f7cbf2feecd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 19 Sep 2017 00:51:11 +0200 Subject: [PATCH 0469/1066] [fix] 163 was for sha256 --- src/yunohost/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index e46598fbd..516ac59de 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -70,7 +70,7 @@ RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( - r'.*/K(?P[^\s\+]+)\.\+163.+\.private$' + r'.*/K(?P[^\s\+]+)\.\+165.+\.private$' ) From d42cc00a8765b77339ca089fd134684a8a50890b Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 19 Sep 2017 01:34:23 +0200 Subject: [PATCH 0470/1066] [enh] Remove date from sql dump --- data/helpers.d/mysql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 1c0ece114..5bc598cbc 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -73,7 +73,7 @@ ynh_mysql_drop_db() { # | arg: db - the database name to dump # | ret: the mysqldump output ynh_mysql_dump_db() { - mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" "$1" + mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" --skip-dump-date "$1" } # Create a user From f5f813badb326a5fcbfb29ae5aa816ab2a4ff217 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 21 Sep 2017 05:52:59 +0200 Subject: [PATCH 0471/1066] [fix] hmac-sha512 key can have space in them because why the fuck not --- src/yunohost/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 516ac59de..c3e216071 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -106,7 +106,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None key_file = glob.glob('/etc/yunohost/dyndns/*.key')[0] with open(key_file) as f: - key = f.readline().strip().split(' ')[-1] + key = f.readline().strip().split(' ', 6)[-1] # Send subscription try: From 87a5759ca427c5e80ddc58d5d53fdb9eb8ecb91e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 21 Sep 2017 06:56:11 +0200 Subject: [PATCH 0472/1066] [enh] automatically migrate tsig done using md5 to sha512 when doing a dyndns update --- src/yunohost/dyndns.py | 46 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index c3e216071..be37072f9 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -182,9 +182,11 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, if domain is None: # Retrieve the first registered domain for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): - match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) + match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path) if not match: - continue + match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) + if not match: + continue _domain = match.group('domain') try: @@ -213,6 +215,11 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] + # this mean that hmac-md5 is used + if "+157" in key: + print "detecting md5 key" + key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host) + host = domain.split('.')[1:] host = '.'.join(host) @@ -269,6 +276,41 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, f.write(ipv6) +def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): + public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" + public_key_md5 = open(public_key_path).read().strip().split(' ')[-1] + + os.system('cd /etc/yunohost/dyndns && ' + 'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain) + os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') + + # +165 means that this file store a hmac-sha512 key + new_key_path = glob.glob('/etc/yunohost/dyndns/*+165*.key')[0] + public_key_sha512 = open(new_key_path).read().strip().split(' ', 6)[-1] + + try: + r = requests.put('https://%s/migrate_key_to_sha512/' % (dyn_host), + data={ + 'public_key_md5': base64.b64encode(public_key_md5), + 'public_key_sha512': base64.b64encode(public_key_sha512), + }) + except requests.ConnectionError: + raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + + if r.status_code != 201: + print r.text + error = json.loads(r.text)['error'] + print "ERROR:", error + # raise MoulinetteError(errno.EPERM, + # m18n.n('dyndns_registration_failed', error=error)) + # XXX print warning + os.system("mv /etc/yunohost/dyndns/*+165* /tmp") + return public_key_path + + os.system("mv /etc/yunohost/dyndns/*+157* /tmp") + return new_key_path.rsplit(".key", 1)[0] + ".private" + + def dyndns_installcron(): """ Install IP update cron From 8590d6b5c6112581c021f81a21c45bfe7e140610 Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Sat, 23 Sep 2017 18:45:59 +0200 Subject: [PATCH 0473/1066] Update php-fpm helpers to handle stretch/php7 and a smooth migration --- data/helpers.d/backend | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c54e82754..e64b67795 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -154,7 +154,18 @@ ynh_remove_nginx_config () { # # usage: ynh_add_fpm_config ynh_add_fpm_config () { - finalphpconf="/etc/php5/fpm/pool.d/$app.conf" + local debian_release=$(lsb_release --codename --short) + # Configure PHP-FPM 7.0 by default + local fpm_config_dir="/etc/php/7.0/fpm" + local fpm_service="php7.0-fpm" + # Configure PHP-FPM 5 on Debian Jessie + if [ "$debian_release" == "jessie" ]; then + fpm_config_dir="/etc/php5/fpm" + fpm_service="php5-fpm" + fi + ynh_app_setting_set $app fpm_config_dir "$fpm_config_dir" + ynh_app_setting_set $app fpm_service "$fpm_service" + finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_backup_if_checksum_is_different "$finalphpconf" sudo cp ../conf/php-fpm.conf "$finalphpconf" ynh_replace_string "__NAMETOCHANGE__" "$app" "$finalphpconf" @@ -165,21 +176,27 @@ ynh_add_fpm_config () { if [ -e "../conf/php-fpm.ini" ] then - finalphpini="/etc/php5/fpm/conf.d/20-$app.ini" + finalphpini="$fpm_config_dir/conf.d/20-$app.ini" ynh_backup_if_checksum_is_different "$finalphpini" sudo cp ../conf/php-fpm.ini "$finalphpini" sudo chown root: "$finalphpini" ynh_store_file_checksum "$finalphpini" fi - - sudo systemctl reload php5-fpm + sudo systemctl reload $fpm_service } # Remove the dedicated php-fpm config # # usage: ynh_remove_fpm_config ynh_remove_fpm_config () { - ynh_secure_remove "/etc/php5/fpm/pool.d/$app.conf" - ynh_secure_remove "/etc/php5/fpm/conf.d/20-$app.ini" 2>&1 - sudo systemctl reload php5-fpm + local fpm_config_dir=$(ynh_app_setting_get $app fpm_config_dir) + local fpm_service=$(ynh_app_setting_get $app fpm_service) + # Assume php version 5 if not set + if [ -z "$fpm_config_dir" ]; then + fpm_config_dir="/etc/php5/fpm" + fpm_service="php5-fpm" + fi + ynh_secure_remove "$fpm_config_dir/pool.d/$app.conf" + ynh_secure_remove "$fpm_config_dir/conf.d/20-$app.ini" 2>&1 + sudo systemctl reload $fpm_service } From 330a6fb9a72a54f7b5636e1659dfd5d56177ed0d Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Mon, 25 Sep 2017 21:59:55 +0200 Subject: [PATCH 0474/1066] Add and use ynh_get_debian_release --- data/helpers.d/backend | 3 +-- data/helpers.d/system | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index e64b67795..60c9c94de 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -154,12 +154,11 @@ ynh_remove_nginx_config () { # # usage: ynh_add_fpm_config ynh_add_fpm_config () { - local debian_release=$(lsb_release --codename --short) # Configure PHP-FPM 7.0 by default local fpm_config_dir="/etc/php/7.0/fpm" local fpm_service="php7.0-fpm" # Configure PHP-FPM 5 on Debian Jessie - if [ "$debian_release" == "jessie" ]; then + if [ "$(ynh_get_debian_release)" == "jessie" ]; then fpm_config_dir="/etc/php5/fpm" fpm_service="php5-fpm" fi diff --git a/data/helpers.d/system b/data/helpers.d/system index 5f2ad385b..d70129c9a 100644 --- a/data/helpers.d/system +++ b/data/helpers.d/system @@ -41,3 +41,10 @@ ynh_abort_if_errors () { set -eu # Exit if a command fail, and if a variable is used unset. trap ynh_exit_properly EXIT # Capturing exit signals on shell script } + +# Return the Debian release codename (i.e. jessie, stretch, etc.) +# +# usage: ynh_get_debian_release +ynh_get_debian_release () { + echo $(lsb_release --codename --short) +} \ No newline at end of file From f4b95ca5ff5dccbc96e18d34f0b3f8995e8e0a14 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 26 Sep 2017 01:01:00 +0200 Subject: [PATCH 0475/1066] [fix] fix this UnicodeDecodingError on backup-restore --- src/yunohost/backup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index d794a2fca..def7fb27b 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -166,11 +166,11 @@ class BackupRestoreTargetsManager(object): or (exclude and isinstance(exclude, list) and not include) if include: - return [target for target in self.targets[category] + return [target.encode("Utf-8") for target in self.targets[category] if self.results[category][target] in include] if exclude: - return [target for target in self.targets[category] + return [target.encode("Utf-8") for target in self.targets[category] if self.results[category][target] not in exclude] From 220c447074534fc2c283559ab255fd86a54ebb9c Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 30 Sep 2017 13:01:15 +0200 Subject: [PATCH 0476/1066] [enh] Handle root path in nginx conf (#361) Avoid to put a double slash when the path is /. That can induce some errors in a nginx config. --- data/helpers.d/backend | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c54e82754..c0cbc616c 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -123,6 +123,9 @@ ynh_add_nginx_config () { # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. # Substitute in a nginx config file only if the variable is not empty if test -n "${path_url:-}"; then + # path_url_slash_less if path_url or a blank value if path_url is only '/' + path_url_slash_less=${path_url%/} + ynh_replace_string "__PATH__/" "$path_url_slash_less/" "$finalnginxconf" ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf" fi if test -n "${domain:-}"; then From 2a49613e59117811b0da09f2d212ba4046429de0 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Mon, 2 Oct 2017 19:56:31 +0200 Subject: [PATCH 0477/1066] Update from Weblate. (#355) * [i18n] Translated using Weblate (French) Currently translated at 100.0% (351 of 351 strings) * [i18n] Translated using Weblate (French) Currently translated at 100.0% (351 of 351 strings) --- locales/fr.json | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 86df7b10d..f4afdceb3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -14,7 +14,7 @@ "app_install_files_invalid": "Fichiers d'installation incorrects", "app_location_already_used": "Une application est déjà installée à cet emplacement", "app_location_install_failed": "Impossible d'installer l'application à cet emplacement", - "app_manifest_invalid": "Manifeste d'application incorrect", + "app_manifest_invalid": "Manifeste d'application incorrect : {error}", "app_no_upgrade": "Aucune application à mettre à jour", "app_not_correctly_installed": "{app:s} semble être mal installé", "app_not_installed": "{app:s} n'est pas installé", @@ -320,7 +320,7 @@ "backup_archive_system_part_not_available": "La partie « {part:s} » du système n’est pas disponible dans cette sauvegarde", "backup_archive_mount_failed": "Le montage de l’archive de sauvegarde a échoué", "backup_archive_writing_error": "Impossible d’ajouter les fichiers à la sauvegarde dans l’archive compressée", - "backup_ask_for_copying_if_needed": "Votre système ne prend pas complètement en charge la méthode rapide d’organisation des fichiers dans l’archive, voulez-vous les organiser en copiant {size:s} Mio ?", + "backup_ask_for_copying_if_needed": "Certains fichiers n’ont pas pu être préparés pour être sauvegardée en utilisant la méthode qui évite de temporairement gaspiller de l’espace sur le système. Pour mener la sauvegarde, {size:s} Mio doivent être temporairement utilisés. Acceptez-vous ?", "backup_borg_not_implemented": "La méthode de sauvegarde Bord n’est pas encore implémentée", "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l’archive décompressée", "backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser l’archive", @@ -344,5 +344,24 @@ "restore_mounting_archive": "Montage de l’archive dans « {path:s} »", "restore_may_be_not_enough_disk_space": "Votre système semble ne pas avoir suffisamment d’espace disponible (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", "restore_not_enough_disk_space": "Espace disponible insuffisant (libre : {free_space:d} octets, nécessaire : {needed_space:d} octets, marge de sécurité : {margin:d} octets)", - "restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système" + "restore_system_part_failed": "Impossible de restaurer la partie « {part:s} » du système", + "backup_couldnt_bind": "Impossible de lier {src:s} avec {dest:s}.", + "domain_dns_conf_is_just_a_recommendation": "Cette page montre la configuration *recommandée*. Elle ne configure *pas* le DNS pour vous. Il est de votre responsabilité que de configurer votre zone DNS chez votre registrar DNS avec cette recommandation.", + "domain_dyndns_dynette_is_unreachable": "Impossible de contacter la dynette YunoHost, soit YunoHost n’est pas correctement connecté à internet ou alors le serveur de dynette est arrêté. Erreur : {error}", + "migrations_backward": "Migration en arrière.", + "migrations_bad_value_for_target": "Nombre invalide pour le paramètre « target », les numéros de migration sont ou {}", + "migrations_cant_reach_migration_file": "Impossible d’accéder aux fichiers de migrations avec le chemin %s", + "migrations_current_target": "La cible de migration est {}", + "migrations_error_failed_to_load_migration": "ERREUR : échec du chargement de migration {number} {name}", + "migrations_forward": "Migration en avant", + "migrations_loading_migration": "Chargement de la migration {number} {name}…", + "migrations_migration_has_failed": "La migration {number} {name} a échoué avec l’exception {exception}, annulation", + "migrations_no_migrations_to_run": "Aucune migration à lancer", + "migrations_show_currently_running_migration": "Application de la migration {number} {name}…", + "migrations_show_last_migration": "La dernière migration appliquée est {}", + "migrations_skip_migration": "Omission de la migration {number} {name}…", + "server_shutdown": "Le serveur sera éteint", + "server_shutdown_confirm": "Le serveur immédiatement être éteint, le voulez-vous vraiment ? [{answers:s}]", + "server_reboot": "Le serveur va redémarrer", + "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]" } From bd2ea3c12cf81eba6879de6d67c44bad3e66b96e Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 3 Oct 2017 18:46:29 +0200 Subject: [PATCH 0478/1066] [fix] Limit ascii logo to 79 characters --- bin/yunoprompt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 212debdc9..9839564e3 100644 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -11,13 +11,13 @@ done cat > /etc/issue.net << EOF '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso + /M+ h- .shh/ // /NMy- hMdosso 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' + hy /yy: :- -. -Nh ' . IP: ${ip} From 44676438fe642b4843ecb56cae7a95d4e4c16b8b Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 3 Oct 2017 18:47:52 +0200 Subject: [PATCH 0479/1066] [fix] Limit ascii 2nd and 3rd logo to 79 char --- bin/yunoprompt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 9839564e3..200e81933 100644 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -36,13 +36,13 @@ sleep 5 chvt 2 cat << EOF '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso + /M+ h- .shh/ // /NMy- hMdosso 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' + hy /yy: :- -. -Nh ' . IP: ${ip} @@ -80,13 +80,13 @@ else # YunoHost is already post-installed domain=$(cat /etc/yunohost/current_host) cat > /etc/issue << EOF '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso + /M+ h- .shh/ // /NMy- hMdosso 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' + hy /yy: :- -. -Nh ' . IP: ${ip} From f25ef833e935a2b8d70acde8863a6bd8b80eabfa Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 3 Oct 2017 18:50:46 +0200 Subject: [PATCH 0480/1066] [fix] Remove dead code --- bin/yunoprompt | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 200e81933..83b28296f 100644 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -77,7 +77,6 @@ read -p "Proceed to post-installation? (y/n) " -n 1 fi else # YunoHost is already post-installed - domain=$(cat /etc/yunohost/current_host) cat > /etc/issue << EOF '. ' '' -d. /M+ h- .shh/ // /NMy- hMdosso From e3a4b307f7c307820c83afce9489f8a128ffb966 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 3 Oct 2017 22:11:24 +0200 Subject: [PATCH 0481/1066] Fix double backslash in case of delimiter used --- data/helpers.d/string | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index 3e68b382d..677350e56 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -18,9 +18,6 @@ ynh_string_random() { # | arg: target_file - File in which the string will be replaced. ynh_replace_string () { delimit=@ - # Escape the delimiter if it's in the string. - match_string=${1//${delimit}/"\\${delimit}"} - replace_string=${2//${delimit}/"\\${delimit}"} # Escape any backslash to preserve them as simple backslash. match_string=${match_string//\\/"\\\\"} @@ -30,6 +27,10 @@ ynh_replace_string () { match_string=${match_string//&/"\&"} replace_string=${replace_string//&/"\&"} + # Escape the delimiter if it's in the string. + match_string=${1//${delimit}/"\\${delimit}"} + replace_string=${2//${delimit}/"\\${delimit}"} + workfile=$3 sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" From 43dbf03ac8fd1c5ac599ca7b0089d5b17cbbd85b Mon Sep 17 00:00:00 2001 From: Weblate Admin Date: Wed, 4 Oct 2017 10:26:25 +0000 Subject: [PATCH 0482/1066] Translated using Weblate (French) Currently translated at 100.0% (353 of 353 strings) Translation: YunoHost/YunoHost Translate-URL: https://holcroft.fr/weblate/projects/yunohost/yunohost/fr/ --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f4afdceb3..2685a231a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -363,5 +363,7 @@ "server_shutdown": "Le serveur sera éteint", "server_shutdown_confirm": "Le serveur immédiatement être éteint, le voulez-vous vraiment ? [{answers:s}]", "server_reboot": "Le serveur va redémarrer", - "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]" + "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]", + "app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications", + "ask_path": "Chemin" } From 0086c8c16a93c1ff350e41e7a53d37cf57e93535 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 7 Oct 2017 15:06:00 +0200 Subject: [PATCH 0483/1066] [fix] match_string: unbound variable (#379) * [fix] match_string: unbound variable * Define local variables at beginning of function --- data/helpers.d/string | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index adfdf89fe..fbf598738 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -21,7 +21,10 @@ ynh_string_random() { # references to sub-expressions can be used # (see sed manual page for more information) ynh_replace_string () { - delimit=@ + local delimit=@ + local match_string=$1 + local replace_string=$2 + local workfile=$3 # Escape any backslash to preserve them as simple backslash. match_string=${match_string//\\/"\\\\"} @@ -32,10 +35,8 @@ ynh_replace_string () { replace_string=${replace_string//&/"\&"} # Escape the delimiter if it's in the string. - match_string=${1//${delimit}/"\\${delimit}"} - replace_string=${2//${delimit}/"\\${delimit}"} - - workfile=$3 + match_string=${match_string//${delimit}/"\\${delimit}"} + replace_string=${replace_string//${delimit}/"\\${delimit}"} sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" } From 5ae558edc915ec3b4284848db8585d3c9c4ca3a0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 8 Oct 2017 23:44:07 +0200 Subject: [PATCH 0484/1066] [fix] Clean madness related to DynDNS (#353) * Add a function to check if a dyndns provider provides a domain * Add a function to check a domain availability from a dyndns provider * Use new functions in dyndns_subscribe * Replace complete madness by _dyndns_available in dyndns_update * This regex is only used in dyndns_update, no need to define it at the whole file scope level * Try to clarify management of old ipv4/ipv6 ... * Add a nice helper to get both ipv4 and ipv6... * Clarify the dyndns_update madness to get current ipv4/v6 * Remove now useless IPRouteLine * Change default values of old ipv4/v6 to None, otherwise shit gets update just because IPv6 = None * Rearrange thing a bit, move path to global variable * Copypasta typo * Dyndns zone file as a global variable * Use helper to write zone content to file * Adding some debugs/info messages * Move the domain guessing to a dedicated function... * Adding comments.. * Using subprocess check_call instead of os.system for nsupdate * Removing dump of the zone update because it's kinda duplicated from what nsupdate already does * Ignore error if old_ipvx file is non existent * Add docstring for _dyndns_available * Remove parenthesis otherwise this gonna displease Bram-sama :P * Start working on postinstall .. use _dyndns_provides to check if domain is a .nohost.me or .nohost.st * Use _dyndns_available to cleanly check for domain availability * Forget about the weird 'domain split' check... * Clean dyndns stuff in domain.py * Missing argument for string --- locales/en.json | 4 +- src/yunohost/domain.py | 40 ++++--- src/yunohost/dyndns.py | 232 ++++++++++++++++++++++++----------------- src/yunohost/tools.py | 46 +++++--- 4 files changed, 198 insertions(+), 124 deletions(-) diff --git a/locales/en.json b/locales/en.json index 9ef41a3f2..8dac6e799 100644 --- a/locales/en.json +++ b/locales/en.json @@ -158,6 +158,7 @@ "domains_available": "Available domains:", "done": "Done", "downloading": "Downloading...", + "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.", "dyndns_cron_installed": "The DynDNS cron job has been installed", "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", "dyndns_cron_removed": "The DynDNS cron job has been removed", @@ -168,7 +169,8 @@ "dyndns_no_domain_registered": "No domain has been registered with DynDNS", "dyndns_registered": "The DynDNS domain has been registered", "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}", - "dyndns_unavailable": "Unavailable DynDNS subdomain", + "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.", + "dyndns_unavailable": "Domain {domain:s} is not available.", "executing_command": "Executing command '{command:s}'...", "executing_script": "Executing script '{script:s}'...", "extracting": "Extracting...", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index f17cc75f6..f828b0973 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -82,28 +82,23 @@ def domain_add(auth, domain, dyndns=False): # DynDNS domain if dyndns: - if len(domain.split('.')) < 3: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_invalid')) + # Do not allow to subscribe to multiple dyndns domains... if os.path.exists('/etc/cron.d/yunohost-dyndns'): raise MoulinetteError(errno.EPERM, m18n.n('domain_dyndns_already_subscribed')) - try: - r = requests.get('https://dyndns.yunohost.org/domains', timeout=30) - except requests.ConnectionError as e: - raise MoulinetteError(errno.EHOSTUNREACH, - m18n.n('domain_dyndns_dynette_is_unreachable', error=str(e))) - from yunohost.dyndns import dyndns_subscribe + from yunohost.dyndns import dyndns_subscribe, _dyndns_provides - dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) - if dyndomain in dyndomains: - dyndns_subscribe(domain=domain) - else: + # Check that this domain can effectively be provided by + # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) + if not _dyndns_provides("dyndns.yunohost.org", domain): raise MoulinetteError(errno.EINVAL, m18n.n('domain_dyndns_root_unknown')) + # Actually subscribe + dyndns_subscribe(domain=domain) + try: yunohost.certificate._certificate_install_selfsigned([domain], False) @@ -281,6 +276,25 @@ def get_public_ip(protocol=4): raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) +def get_public_ips(): + """ + Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org + + Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not + found. + """ + + try: + ipv4 = get_public_ip() + except: + ipv4 = None + try: + ipv6 = get_public_ip(6) + except: + ipv6 = None + + return (ipv4, ipv6) + def _get_maindomain(): with open('/etc/yunohost/current_host', 'r') as f: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 621043c3e..55a2be692 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -35,37 +35,72 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file, write_to_file, rm +from moulinette.utils.network import download_json -from yunohost.domain import get_public_ip, _get_maindomain, _build_dns_conf +from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf logger = getActionLogger('yunohost.dyndns') +OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip' +OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6' +DYNDNS_ZONE = '/etc/yunohost/dyndns/zone' -class IPRouteLine(object): - """ Utility class to parse an ip route output line - The output of ip ro is variable and hard to parse completly, it would - require a real parser, not just a regexp, so do minimal parsing here... - - >>> a = IPRouteLine('2001:: from :: via fe80::c23f:fe:1e:cafe dev eth0 src 2000:de:beef:ca:0:fe:1e:cafe metric 0') - >>> a.src_addr - "2000:de:beef:ca:0:fe:1e:cafe" +def _dyndns_provides(provider, domain): """ - regexp = re.compile( - r'(?Punreachable)?.*src\s+(?P[0-9a-f:]+).*') + Checks if a provider provide/manage a given domain. - def __init__(self, line): - self.m = self.regexp.match(line) - if not self.m: - raise ValueError("Not a valid ip route get line") + Keyword arguments: + provider -- The url of the provider, e.g. "dyndns.yunohost.org" + domain -- The full domain that you'd like.. e.g. "foo.nohost.me" - # make regexp group available as object attributes - for k, v in self.m.groupdict().items(): - setattr(self, k, v) + Returns: + True if the provider provide/manages the domain. False otherwise. + """ -re_dyndns_private_key = re.compile( - r'.*/K(?P[^\s\+]+)\.\+157.+\.private$' -) + logger.debug("Checking if %s is managed by %s ..." % (domain, provider)) + + try: + # Dyndomains will be a list of domains supported by the provider + # e.g. [ "nohost.me", "noho.st" ] + dyndomains = download_json('https://%s/domains' % provider, timeout=30) + except MoulinetteError as e: + logger.error(str(e)) + raise MoulinetteError(errno.EIO, + m18n.n('dyndns_could_not_check_provide', + domain=domain, provider=provider)) + + # Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me' + dyndomain = '.'.join(domain.split('.')[1:]) + + return dyndomain in dyndomains + + +def _dyndns_available(provider, domain): + """ + Checks if a domain is available from a given provider. + + Keyword arguments: + provider -- The url of the provider, e.g. "dyndns.yunohost.org" + domain -- The full domain that you'd like.. e.g. "foo.nohost.me" + + Returns: + True if the domain is avaible, False otherwise. + """ + logger.debug("Checking if domain %s is available on %s ..." + % (domain, provider)) + + try: + r = download_json('https://%s/test/%s' % (provider, domain), + expected_status_code=None) + except MoulinetteError as e: + logger.error(str(e)) + raise MoulinetteError(errno.EIO, + m18n.n('dyndns_could_not_check_available', + domain=domain, provider=provider)) + + return r == u"Domain %s is available" % domain def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None): @@ -81,12 +116,16 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None if domain is None: domain = _get_maindomain() + # Verify if domain is provided by subscribe_host + if not _dyndns_provides(subscribe_host, domain): + raise MoulinetteError(errno.ENOENT, + m18n.n('dyndns_domain_not_provided', + domain=domain, provider=subscribe_host)) + # Verify if domain is available - try: - if requests.get('https://%s/test/%s' % (subscribe_host, domain)).status_code != 200: - raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) - except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + if not _dyndns_available(subscribe_host, domain): + raise MoulinetteError(errno.ENOENT, + m18n.n('dyndns_unavailable', domain=domain)) if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: @@ -133,73 +172,40 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv6 -- IPv6 address to send """ - # IPv4 + # Get old ipv4/v6 + + old_ipv4, old_ipv6 = (None, None) # (default values) + + if os.path.isfile(OLD_IPV4_FILE): + old_ipv4 = read_file(OLD_IPV4_FILE).rstrip() + + if os.path.isfile(OLD_IPV6_FILE): + old_ipv6 = read_file(OLD_IPV6_FILE).rstrip() + + # Get current IPv4 and IPv6 + (ipv4_, ipv6_) = get_public_ips() + if ipv4 is None: - ipv4 = get_public_ip() + ipv4 = ipv4_ - try: - with open('/etc/yunohost/dyndns/old_ip', 'r') as f: - old_ip = f.readline().rstrip() - except IOError: - old_ip = '0.0.0.0' - - # IPv6 if ipv6 is None: - try: - ip_route_out = subprocess.check_output( - ['ip', 'route', 'get', '2000::']).split('\n') + ipv6 = ipv6_ - if len(ip_route_out) > 0: - route = IPRouteLine(ip_route_out[0]) - if not route.unreachable: - ipv6 = route.src_addr - - except (OSError, ValueError) as e: - # Unlikely case "ip route" does not return status 0 - # or produces unexpected output - raise MoulinetteError(errno.EBADMSG, - "ip route cmd error : {}".format(e)) - - if ipv6 is None: - logger.info(m18n.n('no_ipv6_connectivity')) - - try: - with open('/etc/yunohost/dyndns/old_ipv6', 'r') as f: - old_ipv6 = f.readline().rstrip() - except IOError: - old_ipv6 = '0000:0000:0000:0000:0000:0000:0000:0000' + logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) + logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) # no need to update - if old_ip == ipv4 and old_ipv6 == ipv6: + if old_ipv4 == ipv4 and old_ipv6 == ipv6: + logger.info("No updated needed.") return + else: + logger.info("Updated needed, going on...") + # If domain is not given, try to guess it from keys available... if domain is None: - # Retrieve the first registered domain - for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): - match = re_dyndns_private_key.match(path) - if not match: - continue - _domain = match.group('domain') - - try: - # Check if domain is registered - request_url = 'https://{0}/test/{1}'.format(dyn_host, _domain) - if requests.get(request_url, timeout=30).status_code == 200: - continue - except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, - m18n.n('no_internet_connection')) - except requests.exceptions.Timeout: - logger.warning("Correction timed out on {}, skip it".format( - request_url)) - domain = _domain - key = path - break - if not domain: - raise MoulinetteError(errno.EINVAL, - m18n.n('dyndns_no_domain_registered')) - - if key is None: + (domain, key) = _guess_current_dyndns_domain(dyn_host) + # If key is not given, pick the first file we find with the domain given + elif key is None: keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) if not keys: @@ -207,9 +213,12 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] + # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' host = domain.split('.')[1:] host = '.'.join(host) + logger.debug("Building zone update file ...") + lines = [ 'server %s' % dyn_host, 'zone %s' % host, @@ -246,21 +255,27 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, 'send' ] - with open('/etc/yunohost/dyndns/zone', 'w') as zone: - zone.write('\n'.join(lines)) + # Write the actions to do to update to a file, to be able to pass it + # to nsupdate as argument + write_to_file(DYNDNS_ZONE, '\n'.join(lines)) - if os.system('/usr/bin/nsupdate -k %s /etc/yunohost/dyndns/zone' % key) != 0: - os.system('rm -f /etc/yunohost/dyndns/old_ip') - os.system('rm -f /etc/yunohost/dyndns/old_ipv6') + logger.info("Now pushing new conf to DynDNS host...") + + try: + command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] + subprocess.check_call(command) + except subprocess.CalledProcessError: + rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent) + rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent) raise MoulinetteError(errno.EPERM, m18n.n('dyndns_ip_update_failed')) logger.success(m18n.n('dyndns_ip_updated')) - with open('/etc/yunohost/dyndns/old_ip', 'w') as f: - f.write(ipv4) + + if ipv4 is not None: + write_to_file(OLD_IPV4_FILE, ipv4) if ipv6 is not None: - with open('/etc/yunohost/dyndns/old_ipv6', 'w') as f: - f.write(ipv6) + write_to_file(OLD_IPV6_FILE, ipv6) def dyndns_installcron(): @@ -287,3 +302,34 @@ def dyndns_removecron(): raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed')) logger.success(m18n.n('dyndns_cron_removed')) + + +def _guess_current_dyndns_domain(dyn_host): + """ + This function tries to guess which domain should be updated by + "dyndns_update()" because there's not proper management of the current + dyndns domain :/ (and at the moment the code doesn't support having several + dyndns domain, which is sort of a feature so that people don't abuse the + dynette...) + """ + + re_dyndns_private_key = re.compile( + r'.*/K(?P[^\s\+]+)\.\+157.+\.private$' + ) + + # Retrieve the first registered domain + for path in glob.iglob('/etc/yunohost/dyndns/K*.private'): + match = re_dyndns_private_key.match(path) + if not match: + continue + _domain = match.group('domain') + + # Verify if domain is registered (i.e., if it's available, skip + # current domain beause that's not the one we want to update..) + if _dyndns_available(dyn_host, _domain): + continue + else: + return (_domain, path) + + raise MoulinetteError(errno.EINVAL, + m18n.n('dyndns_no_domain_registered')) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 22ac7894f..042671125 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -45,7 +45,7 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_json, write_to_json from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain -from yunohost.dyndns import dyndns_subscribe +from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.firewall import firewall_upnp from yunohost.service import service_status, service_regen_conf, service_log from yunohost.monitor import monitor_disk, monitor_system @@ -253,29 +253,41 @@ def tools_postinstall(domain, password, ignore_dyndns=False): password -- YunoHost admin password """ - dyndns = not ignore_dyndns + dyndns_provider = "dyndns.yunohost.org" # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) - if len(domain.split('.')) >= 3 and not ignore_dyndns: - try: - r = requests.get('https://dyndns.yunohost.org/domains') - except requests.ConnectionError: - pass - else: - dyndomains = json.loads(r.text) - dyndomain = '.'.join(domain.split('.')[1:]) - if dyndomain in dyndomains: - if requests.get('https://dyndns.yunohost.org/test/%s' % domain).status_code == 200: - dyndns = True - else: - raise MoulinetteError(errno.EEXIST, - m18n.n('dyndns_unavailable')) - else: + if not ignore_dyndns: + # Check if yunohost dyndns can handle the given domain + # (i.e. is it a .nohost.me ? a .noho.st ?) + try: + is_nohostme_or_nohost = _dyndns_provides(dyndns_provider, domain) + # If an exception is thrown, most likely we don't have internet + # connectivity or something. Assume that this domain isn't manageable + # and inform the user that we could not contact the dyndns host server. + except: + logger.warning(m18n.n('dyndns_provider_unreachable', + provider=dyndns_provider)) + is_nohostme_or_nohost = False + + # If this is a nohost.me/noho.st, actually check for availability + if is_nohostme_or_nohost: + # (Except if the user explicitly said he/she doesn't care about dyndns) + if ignore_dyndns: dyndns = False + # Check if the domain is available... + elif _dyndns_available(dyndns_provider, domain): + dyndns = True + # If not, abort the postinstall + else: + raise MoulinetteError(errno.EEXIST, + m18n.n('dyndns_unavailable', + domain=domain)) + else: + dyndns = False else: dyndns = False From 9903c48f3a44fb5105866643ffe9d935e13fe3d9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 8 Oct 2017 23:46:10 +0200 Subject: [PATCH 0485/1066] Be able to give lock to son processes detached by systemctl (#367) * Give lock to systemctl sons * Get the 'need_lock' flag from services.yml * Don't need the lock for enable/disable and other stuff --- data/templates/yunohost/services.yml | 1 + src/yunohost/service.py | 57 ++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 514cf5258..fb8c076f9 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -47,6 +47,7 @@ yunohost-api: log: /var/log/yunohost/yunohost-api.log yunohost-firewall: status: service + need_lock: true nslcd: status: service log: /var/log/syslog diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 2ea953873..5401a1fab 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -39,10 +39,10 @@ from moulinette.utils import log, filesystem from yunohost.hook import hook_callback - BASE_CONF_PATH = '/home/yunohost.conf' BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup') PENDING_CONF_DIR = os.path.join(BASE_CONF_PATH, 'pending') +MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" logger = log.getActionLogger('yunohost.service') @@ -493,7 +493,8 @@ def _run_service_command(action, service): service -- Service name """ - if service not in _get_services().keys(): + services = _get_services() + if service not in services.keys(): raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service)) cmd = None @@ -505,8 +506,23 @@ def _run_service_command(action, service): else: raise ValueError("Unknown action '%s'" % action) + need_lock = (services[service].get('need_lock') or False) \ + and action in ['start', 'stop', 'restart', 'reload'] + try: - ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) + # Launch the command + logger.debug("Running '%s'" % cmd) + p = subprocess.Popen(cmd.split(), stderr=subprocess.STDOUT) + # If this command needs a lock (because the service uses yunohost + # commands inside), find the PID and add a lock for it + if need_lock: + PID = _give_lock(action, service, p) + # Wait for the command to complete + p.communicate() + # Remove the lock if one was given + if need_lock and PID != 0: + _remove_lock(PID) + except subprocess.CalledProcessError as e: # TODO: Log output? logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd))) @@ -514,6 +530,41 @@ def _run_service_command(action, service): return True +def _give_lock(action, service, p): + + # Depending of the action, systemctl calls the PID differently :/ + if action == "start" or action == "restart": + systemctl_PID_name = "MainPID" + else: + systemctl_PID_name = "ControlPID" + + cmd_get_son_PID ="systemctl show %s -p %s" % (service, systemctl_PID_name) + son_PID = 0 + # As long as we did not found the PID and that the command is still running + while son_PID == 0 and p.poll() == None: + # Call systemctl to get the PID + # Output of the command is e.g. ControlPID=1234 + son_PID = subprocess.check_output(cmd_get_son_PID.split()) \ + .strip().split("=")[1] + son_PID = int(son_PID) + time.sleep(1) + + # If we found a PID + if son_PID != 0: + # Append the PID to the lock file + logger.debug("Giving a lock to PID %s for service %s !" + % (str(son_PID), service)) + filesystem.append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID)) + + return son_PID + +def _remove_lock(PID_to_remove): + + PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n") + PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ] + filesystem.write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep)) + + def _get_services(): """ Get a dict of managed services with their parameters From fbdf6842542432be2012b81530136271a178b39d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Oct 2017 12:12:08 +0200 Subject: [PATCH 0486/1066] [mod] add failure handling mechanism for json decoding --- src/yunohost/dyndns.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index be37072f9..d596e2525 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -299,8 +299,15 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): if r.status_code != 201: print r.text - error = json.loads(r.text)['error'] - print "ERROR:", error + try: + error = json.loads(r.text)['error'] + print "ERROR:", error + except Exception as e: + import traceback + traceback.print_exc() + print e + error = r.text + # raise MoulinetteError(errno.EPERM, # m18n.n('dyndns_registration_failed', error=error)) # XXX print warning From a32359c76a6005d3a46b8a73aea2d5c9766116dd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Oct 2017 12:12:41 +0200 Subject: [PATCH 0487/1066] [mod] wait locally for the dynette cron to have runned during tsig migration --- src/yunohost/dyndns.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d596e2525..ff6a5d42c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -27,6 +27,7 @@ import os import re import json import glob +import time import base64 import errno import requests @@ -315,6 +316,10 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): return public_key_path os.system("mv /etc/yunohost/dyndns/*+157* /tmp") + + # sleep to wait for dyndns cache invalidation + time.sleep(180) + return new_key_path.rsplit(".key", 1)[0] + ".private" From 50867079836553c98c6faac05b524dafd8fdbd89 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Oct 2017 12:14:14 +0200 Subject: [PATCH 0488/1066] [mod] add some documentation comment --- src/yunohost/dyndns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ff6a5d42c..ccbfdaffb 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -315,6 +315,7 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): os.system("mv /etc/yunohost/dyndns/*+165* /tmp") return public_key_path + # remove old certificates os.system("mv /etc/yunohost/dyndns/*+157* /tmp") # sleep to wait for dyndns cache invalidation From 968ff5196d51cee44f0c5a181957d02ccdc55c1a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 12 Oct 2017 16:44:26 -0400 Subject: [PATCH 0489/1066] Update changelog for 2.7.3 release --- debian/changelog | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/debian/changelog b/debian/changelog index 312c626e3..e8f85388b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,31 @@ +yunohost (2.7.3) testing; urgency=low + + Major changes : + + * [fix] Refactor/clean madness related to DynDNS (#353) + * [i18n] Improve french translation (#355) + * [fix] Use cryptorandom to generate password (#358) + * [enh] Support for single app upgrade from the webadmin (#359) + * [enh] Be able to give lock to son processes detached by systemctl (#367) + * [enh] Make MySQL dumps with a single transaction to ensure backup consistency (#370) + + Misc fixes/improvements : + + * [enh] Escape some special character in ynh_replace_string (#354) + * [fix] Allow dash at the beginning of app settings value (#357) + * [enh] Handle root path in nginx conf (#361) + * [enh] Add debugging in ldap init (#365) + * [fix] Fix app_upgrade_string with missing key + * [fix] Fix for change_url path normalizing with root url (#368) + * [fix] Missing 'ask_path' string (#369) + * [enh] Remove date from sql dump (#371) + * [fix] Fix unicode error in backup/restore (#375) + * [fix] Fix an error in ynh_replace_string (#379) + +Thanks to all contributors <3 ! (Bram, Maniack C, ljf, JimboJoe, ariasuni, Jibec, Aleks) + + -- Alexandre Aubin Thu, 12 Oct 2017 16:18:51 -0400 + yunohost (2.7.2) stable; urgency=low * [mod] pep8 From 4b99df7d07877cdaced5f8704a19f8f4cd713264 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Fri, 13 Oct 2017 08:44:42 +0000 Subject: [PATCH 0490/1066] Translated using Weblate (French) Currently translated at 100.0% (355 of 355 strings) Translation: YunoHost/YunoHost Translate-URL: https://holcroft.fr/weblate/projects/yunohost/yunohost/fr/ --- locales/fr.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2685a231a..8bda0ddc3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -98,7 +98,7 @@ "dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS", "dyndns_registered": "Le domaine DynDNS a été enregistré", "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}", - "dyndns_unavailable": "Sous-domaine DynDNS indisponible", + "dyndns_unavailable": "Le domaine {domain:s} est indisponible.", "executing_command": "Exécution de la commande « {command:s} »...", "executing_script": "Exécution du script « {script:s} »...", "extracting": "Extraction...", @@ -365,5 +365,7 @@ "server_reboot": "Le serveur va redémarrer", "server_reboot_confirm": "Le serveur va redémarrer immédiatement, le voulez-vous vraiment ? [{answers:s}]", "app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications", - "ask_path": "Chemin" + "ask_path": "Chemin", + "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.", + "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}." } From dfa7ca28bf2e8f5180fcfc4519d8d88bcfc6fec7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 16 Oct 2017 21:31:59 +0200 Subject: [PATCH 0491/1066] [microdecision] Typo --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c0cbc616c..b715776e2 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -123,7 +123,7 @@ ynh_add_nginx_config () { # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. # Substitute in a nginx config file only if the variable is not empty if test -n "${path_url:-}"; then - # path_url_slash_less if path_url or a blank value if path_url is only '/' + # path_url_slash_less is path_url, or a blank value if path_url is only '/' path_url_slash_less=${path_url%/} ynh_replace_string "__PATH__/" "$path_url_slash_less/" "$finalnginxconf" ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf" From b8d8f72e36b014fd21cd63a39c3aff7357fc1e18 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 29 Oct 2017 23:19:52 +0100 Subject: [PATCH 0492/1066] [enh] Add an optional stdinfo output for stuff ran with hook_exec --- src/yunohost/hook.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 1f971edb6..489cebd5d 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -26,6 +26,7 @@ import os import re import errno +import tempfile from glob import iglob from moulinette import m18n @@ -297,7 +298,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="admin"): + chdir=None, env=None, user="admin", enable_stdinfo=False): """ Execute hook from a file with arguments @@ -336,6 +337,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, env = {} env['YNH_CWD'] = chdir + if enable_stdinfo: + stdinfo = os.path.join(tempfile.mkdtemp(), "stdinfo") + env['YNH_STDINFO'] = stdinfo + else: + stdinfo = None + # Construct command to execute if user == "root": command = ['sh', '-c'] @@ -361,11 +368,17 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, # Define output callbacks and call command callbacks = ( - lambda l: logger.info(l.rstrip()), - lambda l: logger.warning(l.rstrip()), + lambda l: logger.debug(l.rstrip()), # Stdout + lambda l: logger.warning(l.rstrip()), # Stderr ) + + if stdinfo: + callbacks = ( callbacks[0], callbacks[1], + lambda l: logger.info(l.rstrip())) + returncode = call_async_output( - command, callbacks, shell=False, cwd=chdir + command, callbacks, shell=False, cwd=chdir, + stdinfo=stdinfo ) # Check and return process' return code From 5cd1563014ade84ffa96cac66212697b0002b6fd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 29 Oct 2017 23:20:10 +0100 Subject: [PATCH 0493/1066] [enh] Enable stdinfo when running app script --- src/yunohost/app.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..24229571d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -489,7 +489,9 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) # XXX journal - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), + args=args_list, env=env_dict, user="root", + enable_stdinfo=True) != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -603,7 +605,9 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', + args=args_list, env=env_dict, user="root", + enable_stdinfo=True) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) @@ -738,7 +742,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root") + args=args_list, env=env_dict, user="root", enable_stdinfo=True) except (KeyboardInterrupt, EOFError): install_retcode = -1 except: @@ -755,7 +759,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Execute remove script remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root") + args=[app_instance_name], env=env_dict_remove, user="root", + enable_stdinfo=True) if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) @@ -826,7 +831,8 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, + env=env_dict, user="root", enable_stdinfo=True) == 0: logger.success(m18n.n('app_removed', app=app)) if os.path.exists(app_setting_path): From 7c28ec1c1b51dee9c39c6beab8f5df16b62ee632 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 29 Oct 2017 23:37:51 +0100 Subject: [PATCH 0494/1066] [enh] Adding an app helper to display infos --- data/helpers.d/print | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/helpers.d/print b/data/helpers.d/print index 36f4a120e..740933acb 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -5,6 +5,14 @@ ynh_die() { exit "${2:-1}" } +# Display a message in the 'INFO' logging category +# +# usage: ynh_info "Some message" +ynh_info() +{ + echo "$1" >>"$YNH_STDINFO" +} + # Ignore the yunohost-cli log to prevent errors with conditionals commands # usage: ynh_no_log COMMAND # Simply duplicate the log, execute the yunohost command and replace the log without the result of this command From 46469b8893db8bc0e491bfa341261e702cf29789 Mon Sep 17 00:00:00 2001 From: M5oul Date: Tue, 21 Nov 2017 16:46:45 +0100 Subject: [PATCH 0495/1066] [fix] Update acme-tiny as LE updated its ToS (#386) - fix https://dev.yunohost.org/issues/1007 - Renewing a certificate is not problematic, but creating a certificate for a new domain name may failed according to recent feedbacks. --- src/yunohost/vendor/acme_tiny/acme_tiny.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py index d0ba33d1e..6fd8558d5 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -39,7 +39,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): raise IOError("OpenSSL Error: {0}".format(err)) pub_hex, pub_exp = re.search( r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", - out.decode('utf8'), re.MULTILINE | re.DOTALL).groups() + out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() pub_exp = "{0:x}".format(int(pub_exp)) pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp header = { @@ -82,10 +82,10 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): if proc.returncode != 0: raise IOError("Error loading {0}: {1}".format(csr, err)) domains = set([]) - common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) + common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8')) if common_name is not None: domains.add(common_name.group(1)) - subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE | re.DOTALL) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) if subject_alt_names is not None: for san in subject_alt_names.group(1).split(", "): if san.startswith("DNS:"): @@ -95,7 +95,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): log.info("Registering account...") code, result = _send_signed_request(CA + "/acme/new-reg", { "resource": "new-reg", - "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", + "agreement": json.loads(urlopen(CA + "/directory").read().decode('utf8'))['meta']['terms-of-service'], }) if code == 201: log.info("Registered!") From b560207423194411cf5d3289a4a5c4e0960ca3a7 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Sun, 26 Nov 2017 17:29:14 +0100 Subject: [PATCH 0496/1066] [i18n] Translated using Weblate (French) (#387) Currently translated at 100.0% (355 of 355 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 8bda0ddc3..8baccbd70 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,7 +1,7 @@ { "action_invalid": "Action « {action:s} » incorrecte", "admin_password": "Mot de passe d'administration", - "admin_password_change_failed": "Impossible de modifier le mot de passe d'administration", + "admin_password_change_failed": "Impossible de changer le mot de passe", "admin_password_changed": "Le mot de passe d'administration a été modifié", "app_already_installed": "{app:s} est déjà installé", "app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}", From fd2321654441e95dd3fb2d22c7c3a60e76422a08 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Nov 2017 22:17:09 +0100 Subject: [PATCH 0497/1066] [mod] Use proper functions to enable/start firewall during postinstall --- src/yunohost/service.py | 2 +- src/yunohost/tools.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 5401a1fab..0861d05cd 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -506,7 +506,7 @@ def _run_service_command(action, service): else: raise ValueError("Unknown action '%s'" % action) - need_lock = (services[service].get('need_lock') or False) \ + need_lock = services[service].get('need_lock', False) \ and action in ['start', 'stop', 'restart', 'reload'] try: diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 042671125..51db12cfb 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -47,7 +47,7 @@ from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, a from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.firewall import firewall_upnp -from yunohost.service import service_status, service_regen_conf, service_log +from yunohost.service import service_status, service_regen_conf, service_log, service_start, service_enable from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version @@ -398,8 +398,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): os.system('touch /etc/yunohost/installed') # Enable and start YunoHost firewall at boot time - os.system('update-rc.d yunohost-firewall enable') - os.system('service yunohost-firewall start &') + service_enable("yunohost-firewall") + service_start("yunohost-firewall") service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured')) From 164377d5999aafc6523192539d4336c085a64477 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 27 Nov 2017 23:12:13 +0100 Subject: [PATCH 0498/1066] [mod] Use systemctl for all service operations --- src/yunohost/service.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 0861d05cd..f0948c961 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -498,11 +498,8 @@ def _run_service_command(action, service): raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service)) cmd = None - if action in ['start', 'stop', 'restart', 'reload']: - cmd = 'service %s %s' % (service, action) - elif action in ['enable', 'disable']: - arg = 'defaults' if action == 'enable' else 'remove' - cmd = 'update-rc.d %s %s' % (service, arg) + if action in ['start', 'stop', 'restart', 'reload', 'enable', 'disable']: + cmd = 'systemctl %s %s' % (action, service) else: raise ValueError("Unknown action '%s'" % action) From 7df6874fc999e7615e0bf8082a4f00e161c47656 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 28 Nov 2017 00:23:43 +0100 Subject: [PATCH 0499/1066] [fix] Open NTP port to update date --- data/templates/yunohost/firewall.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/yunohost/firewall.yml b/data/templates/yunohost/firewall.yml index df5b0fe88..3ee284d97 100644 --- a/data/templates/yunohost/firewall.yml +++ b/data/templates/yunohost/firewall.yml @@ -4,7 +4,7 @@ uPnP: UDP: [53] ipv4: TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] - UDP: [53, 5353] + UDP: [53, 5353, 123] ipv6: TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] - UDP: [53, 5353] + UDP: [53, 5353, 123] From b0849cc43d944dd9b68ee5e15c3d6cb02730c8c5 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 28 Nov 2017 00:25:06 +0100 Subject: [PATCH 0500/1066] Bad branch --- data/templates/yunohost/firewall.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/yunohost/firewall.yml b/data/templates/yunohost/firewall.yml index 3ee284d97..df5b0fe88 100644 --- a/data/templates/yunohost/firewall.yml +++ b/data/templates/yunohost/firewall.yml @@ -4,7 +4,7 @@ uPnP: UDP: [53] ipv4: TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] - UDP: [53, 5353, 123] + UDP: [53, 5353] ipv6: TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] - UDP: [53, 5353, 123] + UDP: [53, 5353] From 17ba10ad924bebc94914cb7209164a356a39b499 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 28 Nov 2017 19:58:51 +0100 Subject: [PATCH 0501/1066] [fix] Fix helper for old apps without backup script (#388) --- data/helpers.d/utils | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 44c679471..2cb18c5c0 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -59,14 +59,19 @@ ynh_restore_upgradebackup () { # ynh_abort_if_errors # ynh_backup_before_upgrade () { + if [ ! -e "/etc/yunohost/apps/$app/scripts/backup" ] + then + echo "This app doesn't have any backup script." >&2 + return + fi backup_number=1 old_backup_number=2 app_bck=${app//_/-} # Replace all '_' by '-' - - # Check if a backup already exists with the prefix 1 + + # Check if a backup already exists with the prefix 1 if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1 - then - # Prefix becomes 2 to preserve the previous backup + then + # Prefix becomes 2 to preserve the previous backup backup_number=2 old_backup_number=1 fi @@ -74,7 +79,7 @@ ynh_backup_before_upgrade () { # Create backup sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number if [ "$?" -eq 0 ] - then + then # If the backup succeeded, remove the previous backup if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number then From cb1728f32c701219c6a4692c1940883eb1d084ae Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 28 Nov 2017 21:06:16 +0100 Subject: [PATCH 0502/1066] Remove port 53 from UPnP (but keep it open on local network) (#362) --- data/templates/yunohost/firewall.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/yunohost/firewall.yml b/data/templates/yunohost/firewall.yml index df5b0fe88..201a39092 100644 --- a/data/templates/yunohost/firewall.yml +++ b/data/templates/yunohost/firewall.yml @@ -1,7 +1,7 @@ uPnP: enabled: false - TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] - UDP: [53] + TCP: [22, 25, 80, 443, 465, 587, 993, 5222, 5269] + UDP: [] ipv4: TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] UDP: [53, 5353] From a0d7279ad07c12117aa8db608365eb334b72fdb9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 28 Nov 2017 19:03:29 -0500 Subject: [PATCH 0503/1066] Update changelog for 2.7.4 release --- debian/changelog | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/changelog b/debian/changelog index e8f85388b..c684283c3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +yunohost (2.7.4) testing; urgency=low + + * [fix] Update acme-tiny as LE updated its ToS (#386) + * [fix] Fix helper for old apps without backup script (#388) + * [mod] Remove port 53 from UPnP (but keep it open on local network) (#362) + * [i18n] Improve French translation + +Thanks to all contributors <3 ! (jibec, Moul, Maniack, Aleks) + + -- Alexandre Aubin Tue, 28 Nov 2017 19:01:41 -0500 + yunohost (2.7.3) testing; urgency=low Major changes : From 4f62eb5320323d4f4de83b2af306ae53e89bc5ba Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 2 Dec 2017 16:44:47 +0100 Subject: [PATCH 0504/1066] Fix upgrade fake package --- data/helpers.d/package | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 36777aa52..f28691579 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -126,10 +126,7 @@ ynh_install_app_dependencies () { version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. dep_app=${app//_/-} # Replace all '_' by '-' - if ynh_package_is_installed "${dep_app}-ynh-deps"; then - echo "A package named ${dep_app}-ynh-deps is already installed" >&2 - else - cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build + cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc Priority: optional Package: ${dep_app}-ynh-deps @@ -139,11 +136,10 @@ Architecture: all Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. EOF - ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ - || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies - rm /tmp/${dep_app}-ynh-deps.control - ynh_app_setting_set $app apt_dependencies $dependencies - fi + ynh_package_install_from_equivs /tmp/${dep_app}-ynh-deps.control \ + || ynh_die "Unable to install dependencies" # Install the fake package and its dependencies + rm /tmp/${dep_app}-ynh-deps.control + ynh_app_setting_set $app apt_dependencies $dependencies } # Remove fake package and its dependencies From c4ae0fcb169af713d34e944e0a7e7f89cbfaaa28 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Dec 2017 12:38:16 -0500 Subject: [PATCH 0505/1066] Update changelog for 2.7.5 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index c684283c3..bd7430cb7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.7.5) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Sat, 02 Dec 2017 12:38:00 -0500 + yunohost (2.7.4) testing; urgency=low * [fix] Update acme-tiny as LE updated its ToS (#386) From c13c75f3e76d33363f314b4634b937c33bb837f4 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Fri, 8 Dec 2017 23:04:45 +0100 Subject: [PATCH 0506/1066] Solve ynh_use_logrotate Actually when we use the command `ynh_use_logrotate /var/log/abcd/` the result in the logrotate conf file is `/opt/yunohost/abcd/logs//.log` witch is not what we want. --- data/helpers.d/backend | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index b715776e2..c32ef02ac 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -6,7 +6,7 @@ # # If no argument provided, a standard directory will be use. /var/log/${app} # You can provide a path with the directory only or with the logfile. -# /parentdir/logdir/ +# /parentdir/logdir # /parentdir/logdir/logfile.log # # It's possible to use this helper several times, each config will be added to the same logrotate config file. @@ -24,7 +24,7 @@ ynh_use_logrotate () { if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile logfile=$1 # In this case, focus logrotate on the logfile else - logfile=$1/.log # Else, uses the directory and all logfile into it. + logfile=$1/*.log # Else, uses the directory and all logfile into it. fi else logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log From 4276aebfa379fcbc8f6fc0bca911aaa1a01742b4 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 10 Dec 2017 23:38:09 +0100 Subject: [PATCH 0507/1066] [enh] Default version number for ynh_install_app_dependencies --- data/helpers.d/package | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/helpers.d/package b/data/helpers.d/package index f28691579..f6c9c07db 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -124,6 +124,9 @@ ynh_install_app_dependencies () { manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place fi version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. + if [ " ${#version}" -eq 0 ]; then + version="1.0" + if dep_app=${app//_/-} # Replace all '_' by '-' cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build From 2685c42ac605b9a79cb389791f415e97b7214ef4 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 11 Dec 2017 17:23:26 +0100 Subject: [PATCH 0508/1066] [fix] Only for JimboJoe ;) --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index f6c9c07db..a337bdd09 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -126,7 +126,7 @@ ynh_install_app_dependencies () { version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. if [ " ${#version}" -eq 0 ]; then version="1.0" - if + fi dep_app=${app//_/-} # Replace all '_' by '-' cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build From 42f9c8fc185a04a999360cb0005bf27abba8d851 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 11 Dec 2017 17:25:28 +0100 Subject: [PATCH 0509/1066] [fix] Who said I didn't check my code !? --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index a337bdd09..2e60d723e 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -124,7 +124,7 @@ ynh_install_app_dependencies () { manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place fi version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. - if [ " ${#version}" -eq 0 ]; then + if [ ${#version} -eq 0 ]; then version="1.0" fi dep_app=${app//_/-} # Replace all '_' by '-' From 3a3ec7d9b5329967f64a0fda4dc1c8f2b289d85b Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 17 Dec 2017 20:26:52 +0100 Subject: [PATCH 0510/1066] [fix/enh] Use local variables --- data/helpers.d/backend | 10 +++++----- data/helpers.d/filesystem | 10 +++++----- data/helpers.d/ip | 10 +++++----- data/helpers.d/mysql | 6 +++--- data/helpers.d/network | 4 ++-- data/helpers.d/package | 18 +++++++++--------- data/helpers.d/print | 4 ++-- data/helpers.d/string | 12 ++++++------ data/helpers.d/system | 2 +- data/helpers.d/user | 4 ++-- data/helpers.d/utils | 29 +++++++++++++++-------------- 11 files changed, 55 insertions(+), 54 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c32ef02ac..8fef412cf 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -22,12 +22,12 @@ ynh_use_logrotate () { fi if [ $# -gt 0 ]; then if [ "$(echo ${1##*.})" == "log" ]; then # Keep only the extension to check if it's a logfile - logfile=$1 # In this case, focus logrotate on the logfile + local logfile=$1 # In this case, focus logrotate on the logfile else - logfile=$1/*.log # Else, uses the directory and all logfile into it. + local logfile=$1/*.log # Else, uses the directory and all logfile into it. fi else - logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log + local logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log fi cat > ./${app}-logrotate << EOF # Build a config file for logrotate $logfile { @@ -97,7 +97,7 @@ ynh_add_systemd_config () { # # usage: ynh_remove_systemd_config ynh_remove_systemd_config () { - finalsystemdconf="/etc/systemd/system/$app.service" + local finalsystemdconf="/etc/systemd/system/$app.service" if [ -e "$finalsystemdconf" ]; then sudo systemctl stop $app sudo systemctl disable $app @@ -124,7 +124,7 @@ ynh_add_nginx_config () { # Substitute in a nginx config file only if the variable is not empty if test -n "${path_url:-}"; then # path_url_slash_less is path_url, or a blank value if path_url is only '/' - path_url_slash_less=${path_url%/} + local path_url_slash_less=${path_url%/} ynh_replace_string "__PATH__/" "$path_url_slash_less/" "$finalnginxconf" ynh_replace_string "__PATH__" "$path_url" "$finalnginxconf" fi diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 6fb073e06..6361d278e 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -180,12 +180,12 @@ ynh_restore_file () { local ARCHIVE_PATH="$YNH_CWD${ORIGIN_PATH}" # Default value for DEST_PATH = /$ORIGIN_PATH local DEST_PATH="${2:-$ORIGIN_PATH}" - + # If ARCHIVE_PATH doesn't exist, search for a corresponding path in CSV if [ ! -d "$ARCHIVE_PATH" ] && [ ! -f "$ARCHIVE_PATH" ] && [ ! -L "$ARCHIVE_PATH" ]; then ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" fi - + # Restore ORIGIN_PATH into DEST_PATH mkdir -p $(dirname "$DEST_PATH") @@ -258,7 +258,7 @@ ynh_backup_if_checksum_is_different () { then # Proceed only if a value was stored into the app settings if ! echo "$checksum_value $file" | sudo md5sum -c --status then # If the checksum is now different - backup_file="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')" + local backup_file="/home/yunohost.conf/backup/$file.backup.$(date '+%Y%m%d.%H%M%S')" sudo mkdir -p "$(dirname "$backup_file")" sudo cp -a "$file" "$backup_file" # Backup the current file echo "File $file has been manually modified since the installation or last upgrade. So it has been duplicated in $backup_file" >&2 @@ -272,8 +272,8 @@ ynh_backup_if_checksum_is_different () { # usage: ynh_secure_remove path_to_remove # | arg: path_to_remove - File or directory to remove ynh_secure_remove () { - path_to_remove=$1 - forbidden_path=" \ + local path_to_remove=$1 + local forbidden_path=" \ /var/www \ /home/yunohost.app" diff --git a/data/helpers.d/ip b/data/helpers.d/ip index cb507b35a..874675c9d 100644 --- a/data/helpers.d/ip +++ b/data/helpers.d/ip @@ -8,12 +8,12 @@ ynh_validate_ip() { # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298 - - IP_ADDRESS_FAMILY=$1 - IP_ADDRESS=$2 - + + local IP_ADDRESS_FAMILY=$1 + local IP_ADDRESS=$2 + [ "$IP_ADDRESS_FAMILY" == "4" ] || [ "$IP_ADDRESS_FAMILY" == "6" ] || return 1 - + python /dev/stdin << EOF import socket import sys diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 42c204f95..56741ec0e 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -40,9 +40,9 @@ ynh_mysql_execute_file_as_root() { # | arg: user - the user to grant privilegies # | arg: pwd - the password to identify user by ynh_mysql_create_db() { - db=$1 + local db=$1 - sql="CREATE DATABASE ${db};" + local sql="CREATE DATABASE ${db};" # grant all privilegies to user if [[ $# -gt 1 ]]; then @@ -159,6 +159,6 @@ ynh_mysql_remove_db () { # | arg: name - name to correct/sanitize # | ret: the corrected name ynh_sanitize_dbid () { - dbid=${1//[-.]/_} # We should avoid having - and . in the name of databases. They are replaced by _ + local dbid=${1//[-.]/_} # We should avoid having - and . in the name of databases. They are replaced by _ echo $dbid } diff --git a/data/helpers.d/network b/data/helpers.d/network index c6764c1f5..f9e37e6cc 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -11,7 +11,7 @@ # usage: ynh_normalize_url_path path_to_normalize # | arg: url_path_to_normalize - URL path to normalize before using it ynh_normalize_url_path () { - path_url=$1 + local path_url=$1 test -n "$path_url" || ynh_die "ynh_normalize_url_path expect a URL path as first argument and received nothing." if [ "${path_url:0:1}" != "/" ]; then # If the first character is not a / path_url="/$path_url" # Add / at begin of path variable @@ -29,7 +29,7 @@ ynh_normalize_url_path () { # usage: ynh_find_port begin_port # | arg: begin_port - port to start to search ynh_find_port () { - port=$1 + local port=$1 test -n "$port" || ynh_die "The argument of ynh_find_port must be a valid port." while netcat -z 127.0.0.1 $port # Check if the port is free do diff --git a/data/helpers.d/package b/data/helpers.d/package index f28691579..ccb0c44d0 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -80,15 +80,15 @@ ynh_package_autopurge() { # usage: ynh_package_install_from_equivs controlfile # | arg: controlfile - path of the equivs control file ynh_package_install_from_equivs () { - controlfile=$1 + local controlfile=$1 # Check if the equivs package is installed. Or install it. ynh_package_is_installed 'equivs' \ || ynh_package_install equivs # retrieve package information - pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) # Retrieve the name of the debian package - pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) # And its version number + local pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) # Retrieve the name of the debian package + local pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) # And its version number [[ -z "$pkgname" || -z "$pkgversion" ]] \ && echo "Invalid control file" && exit 1 # Check if this 2 variables aren't empty. @@ -96,7 +96,7 @@ ynh_package_install_from_equivs () { ynh_package_update # Build and install the package - TMPDIR=$(mktemp -d) + local TMPDIR=$(mktemp -d) # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg @@ -118,13 +118,13 @@ ynh_package_install_from_equivs () { # usage: ynh_install_app_dependencies dep [dep [...]] # | arg: dep - the package name to install in dependence ynh_install_app_dependencies () { - dependencies=$@ - manifest_path="../manifest.json" + local dependencies=$@ + local manifest_path="../manifest.json" if [ ! -e "$manifest_path" ]; then manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place fi - version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. - dep_app=${app//_/-} # Replace all '_' by '-' + local version=$(grep '\"version\": ' "$manifest_path" | cut -d '"' -f 4) # Retrieve the version number in the manifest file. + local dep_app=${app//_/-} # Replace all '_' by '-' cat > /tmp/${dep_app}-ynh-deps.control << EOF # Make a control file for equivs-build Section: misc @@ -148,6 +148,6 @@ EOF # # usage: ynh_remove_app_dependencies ynh_remove_app_dependencies () { - dep_app=${app//_/-} # Replace all '_' by '-' + local dep_app=${app//_/-} # Replace all '_' by '-' ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. } diff --git a/data/helpers.d/print b/data/helpers.d/print index 36f4a120e..d9c8f1ec4 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -10,10 +10,10 @@ ynh_die() { # Simply duplicate the log, execute the yunohost command and replace the log without the result of this command # It's a very badly hack... ynh_no_log() { - ynh_cli_log=/var/log/yunohost/yunohost-cli.log + local ynh_cli_log=/var/log/yunohost/yunohost-cli.log sudo cp -a ${ynh_cli_log} ${ynh_cli_log}-move eval $@ - exit_code=$? + local exit_code=$? sudo mv ${ynh_cli_log}-move ${ynh_cli_log} return $? } diff --git a/data/helpers.d/string b/data/helpers.d/string index fbf598738..80fae8cf7 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -26,13 +26,13 @@ ynh_replace_string () { local replace_string=$2 local workfile=$3 - # Escape any backslash to preserve them as simple backslash. - match_string=${match_string//\\/"\\\\"} - replace_string=${replace_string//\\/"\\\\"} + # Escape any backslash to preserve them as simple backslash. + match_string=${match_string//\\/"\\\\"} + replace_string=${replace_string//\\/"\\\\"} - # Escape the & character, who has a special function in sed. - match_string=${match_string//&/"\&"} - replace_string=${replace_string//&/"\&"} + # Escape the & character, who has a special function in sed. + match_string=${match_string//&/"\&"} + replace_string=${replace_string//&/"\&"} # Escape the delimiter if it's in the string. match_string=${match_string//${delimit}/"\\${delimit}"} diff --git a/data/helpers.d/system b/data/helpers.d/system index 5f2ad385b..4bb941b7d 100644 --- a/data/helpers.d/system +++ b/data/helpers.d/system @@ -14,7 +14,7 @@ # Usage: ynh_exit_properly is used only by the helper ynh_abort_if_errors. # You must not use it directly. ynh_exit_properly () { - exit_code=$? + local exit_code=$? if [ "$exit_code" -eq 0 ]; then exit 0 # Exit without error if the script ended correctly fi diff --git a/data/helpers.d/user b/data/helpers.d/user index 0bb0736af..8e214691c 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -48,9 +48,9 @@ ynh_system_user_create () { if ! ynh_system_user_exists "$1" # Check if the user exists on the system then # If the user doesn't exist if [ $# -ge 2 ]; then # If a home dir is mentioned - user_home_dir="-d $2" + local user_home_dir="-d $2" else - user_home_dir="--no-create-home" + local user_home_dir="--no-create-home" fi sudo useradd $user_home_dir --system --user-group $1 --shell /usr/sbin/nologin || ynh_die "Unable to create $1 system account" fi diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 2cb18c5c0..030fddcde 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -5,9 +5,9 @@ # usage: ynh_get_plain_key key [subkey [subsubkey ...]] # | ret: string - the key's value ynh_get_plain_key() { - prefix="#" - founded=0 - key=$1 + local prefix="#" + local founded=0 + local key=$1 shift while read line; do if [[ "$founded" == "1" ]] ; then @@ -36,7 +36,7 @@ ynh_get_plain_key() { # ynh_restore_upgradebackup () { echo "Upgrade failed." >&2 - app_bck=${app//_/-} # Replace all '_' by '-' + local app_bck=${app//_/-} # Replace all '_' by '-' # Check if an existing backup can be found before removing and restoring the application. if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number @@ -64,9 +64,9 @@ ynh_backup_before_upgrade () { echo "This app doesn't have any backup script." >&2 return fi - backup_number=1 - old_backup_number=2 - app_bck=${app//_/-} # Replace all '_' by '-' + local backup_number=1 + local old_backup_number=2 + local app_bck=${app//_/-} # Replace all '_' by '-' # Check if a backup already exists with the prefix 1 if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1 @@ -94,7 +94,7 @@ ynh_backup_before_upgrade () { # Download, check integrity, uncompress and patch the source from app.src # # The file conf/app.src need to contains: -# +# # SOURCE_URL=Address to download the app archive # SOURCE_SUM=Control sum # # (Optional) Program to check the integrity (sha256sum, md5sum...) @@ -113,9 +113,9 @@ ynh_backup_before_upgrade () { # Details: # This helper downloads sources from SOURCE_URL if there is no local source # archive in /opt/yunohost-apps-src/APP_ID/SOURCE_FILENAME -# +# # Next, it checks the integrity with "SOURCE_SUM_PRG -c --status" command. -# +# # If it's ok, the source archive will be uncompressed in $dest_dir. If the # SOURCE_IN_SUBDIR is true, the first level directory of the archive will be # removed. @@ -130,7 +130,7 @@ ynh_backup_before_upgrade () { ynh_setup_source () { local dest_dir=$1 local src_id=${2:-app} # If the argument is not given, source_id equals "app" - + # Load value from configuration file (see above for a small doc about this file # format) local src_url=$(grep 'SOURCE_URL=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) @@ -139,7 +139,7 @@ ynh_setup_source () { local src_format=$(grep 'SOURCE_FORMAT=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_filename=$(grep 'SOURCE_FILENAME=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) - + # Default value src_sumprg=${src_sumprg:-sha256sum} src_in_subdir=${src_in_subdir:-true} @@ -216,10 +216,11 @@ ynh_setup_source () { # | arg: ... - (Optionnal) More POST keys and values ynh_local_curl () { # Define url of page to curl - full_page_url=https://localhost$path_url$1 + local full_page_url=https://localhost$path_url$1 # Concatenate all other arguments with '&' to prepare POST data - POST_data="" + local POST_data="" + local arg="" for arg in "${@:2}" do POST_data="${POST_data}${arg}&" From 542528ab05716c5335cdc6079e9bb0029292e24d Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 21 Dec 2017 19:19:33 +0100 Subject: [PATCH 0511/1066] Fix broken ynh_replace_string (#394) * Fix broken ynh_replace_string * Replace name for ynh_replace_special_string --- data/helpers.d/string | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index fbf598738..13399ffe0 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -26,6 +26,27 @@ ynh_replace_string () { local replace_string=$2 local workfile=$3 + # Escape the delimiter if it's in the string. + match_string=${match_string//${delimit}/"\\${delimit}"} + replace_string=${replace_string//${delimit}/"\\${delimit}"} + + sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" +} + +# Substitute/replace a special string by another in a file +# +# usage: ynh_replace_special_string match_string replace_string target_file +# | arg: match_string - String to be searched and replaced in the file +# | arg: replace_string - String that will replace matches +# | arg: target_file - File in which the string will be replaced. +# +# This helper will use ynh_replace_string, but as you can use special +# characters, you can't use some regular expressions and sub-expressions. +ynh_replace_special_string () { + local match_string=$1 + local replace_string=$2 + local workfile=$3 + # Escape any backslash to preserve them as simple backslash. match_string=${match_string//\\/"\\\\"} replace_string=${replace_string//\\/"\\\\"} @@ -34,9 +55,5 @@ ynh_replace_string () { match_string=${match_string//&/"\&"} replace_string=${replace_string//&/"\&"} - # Escape the delimiter if it's in the string. - match_string=${match_string//${delimit}/"\\${delimit}"} - replace_string=${replace_string//${delimit}/"\\${delimit}"} - - sudo sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$workfile" + ynh_replace_string "$match_string" "$replace_string" "$workfile" } From 421994a3efe93d93eb20b2dcad0185aeb6449ded Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 22 Dec 2017 19:01:09 +0100 Subject: [PATCH 0512/1066] [enh] Tranfert backup_core_only to BACKUP_CORE_ONLY --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 2cb18c5c0..4679121e4 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -77,7 +77,7 @@ ynh_backup_before_upgrade () { fi # Create backup - sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number + sudo BACKUP_CORE_ONLY=1 yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number if [ "$?" -eq 0 ] then # If the backup succeeded, remove the previous backup From 810eaec8c995495f2b86ef17d73b7f12ffbb342f Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Fri, 22 Dec 2017 19:27:02 +0100 Subject: [PATCH 0513/1066] Don't backup big data items when BACKUP_CORE_ONLY is set --- data/helpers.d/filesystem | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 6fb073e06..6f24a7777 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -47,6 +47,12 @@ ynh_backup() { local DEST_PATH="${2:-}" local IS_BIG="${3:-0}" + # If backing up core only (used by ynh_backup_before_upgrade), + # don't backup big data items + if [ "$IS_BIG" == "1" ] && [ -n "$BACKUP_CORE_ONLY" ] ; then + return 0 + fi + # ============================================================================== # Format correctly source and destination paths # ============================================================================== From 3dd6317ae6d93997c9f4d512c4ded0c626108b08 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 22 Dec 2017 19:33:47 +0100 Subject: [PATCH 0514/1066] Add info --- data/helpers.d/filesystem | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 6f24a7777..f8da72ad5 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -50,6 +50,7 @@ ynh_backup() { # If backing up core only (used by ynh_backup_before_upgrade), # don't backup big data items if [ "$IS_BIG" == "1" ] && [ -n "$BACKUP_CORE_ONLY" ] ; then + echo "$SRC_PATH will not be saved, because backup_core_only is set." >&2 return 0 fi From e696caa31fc2c6f3613ab11568fc0d2392a047af Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 29 Dec 2017 16:00:29 +0100 Subject: [PATCH 0515/1066] [Fix] Nginx headers --- data/templates/nginx/server.tpl.conf | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 685ae01b8..90a258433 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -42,7 +42,12 @@ server { # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 #ssl_dhparam /etc/ssl/private/dh2048.pem; - add_header Strict-Transport-Security "max-age=31536000;"; + add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"; + add_header 'Referrer-Policy' 'no-referrer'; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options "SAMEORIGIN"; access_by_lua_file /usr/share/ssowat/access.lua; From 9e19e5316ccbeeb1c6d3dd16dc70278eee18c648 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 29 Dec 2017 16:07:15 +0100 Subject: [PATCH 0516/1066] [Fix] Nginx headers --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 90a258433..dee667939 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -42,7 +42,7 @@ server { # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 #ssl_dhparam /etc/ssl/private/dh2048.pem; - add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; add_header 'Referrer-Policy' 'no-referrer'; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; From 5cd30e584d4225a3c82ca54709189aa4f9bc9ba9 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 29 Dec 2017 16:16:37 +0100 Subject: [PATCH 0517/1066] [Fix] Nginx headers in Admin conf --- data/templates/nginx/plain/yunohost_admin.conf | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index a9d26d151..349be9177 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -36,8 +36,14 @@ server { # Uncomment the following directive after DH generation # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 #ssl_dhparam /etc/ssl/private/dh2048.pem; - - add_header Strict-Transport-Security "max-age=31536000;"; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; + add_header 'Referrer-Policy' 'no-referrer'; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Download-Options noopen; + add_header X-Permitted-Cross-Domain-Policies none; + add_header X-Frame-Options "SAMEORIGIN"; location / { return 302 https://$http_host/yunohost/admin; From 95835118bd8e0c308dec2c3659719760cd17570f Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 29 Dec 2017 17:59:12 +0100 Subject: [PATCH 0518/1066] [Fix] CSP Standart. --- data/templates/nginx/server.tpl.conf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index dee667939..6f49d68c3 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -43,9 +43,11 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header 'Referrer-Policy' 'no-referrer'; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; + add_header X-Download-Options noopen; add_header X-Permitted-Cross-Domain-Policies none; add_header X-Frame-Options "SAMEORIGIN"; From 804d0b29c35aa450608487ae12ad4deefb1e8537 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 29 Dec 2017 18:25:17 +0100 Subject: [PATCH 0519/1066] [Fix] Add CSP in Admin conf --- data/templates/nginx/plain/yunohost_admin.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 349be9177..9bdbb0f26 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -38,7 +38,8 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header 'Referrer-Policy' 'no-referrer'; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; From b655229cbd8c00cf57838f848b1988ff8d02d1d2 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 30 Dec 2017 11:18:17 +0100 Subject: [PATCH 0520/1066] [Fix] Referrer --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 6f49d68c3..11f503c98 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -43,7 +43,7 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header 'Referrer-Policy' 'same-origin'; add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; From bd2f459e8663c401506faa2ac5503e74aa8e50b8 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 30 Dec 2017 11:18:58 +0100 Subject: [PATCH 0521/1066] [Fix] Referrer --- data/templates/nginx/plain/yunohost_admin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 9bdbb0f26..eedbd61b3 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -38,7 +38,7 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header 'Referrer-Policy' 'same-origin'; add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; From 044b2406d3c1a0f11e246dc1f2827a91e8a77212 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 18:45:18 +0100 Subject: [PATCH 0522/1066] [enh] better logging during key migration --- locales/en.json | 4 ++++ src/yunohost/dyndns.py | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 80ff22655..421a04f56 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,6 +207,10 @@ "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", + "migrate_tsig_end": "Migration to hmac-sha512 finished", + "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}", + "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512", + "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ccbfdaffb..459a1e04e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -278,6 +278,7 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): + logger.warning(m18n.n('migrate_tsig_start', domain=domain)) public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" public_key_md5 = open(public_key_path).read().strip().split(' ')[-1] @@ -299,19 +300,17 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: - print r.text try: error = json.loads(r.text)['error'] - print "ERROR:", error except Exception as e: import traceback traceback.print_exc() print e error = r.text - # raise MoulinetteError(errno.EPERM, - # m18n.n('dyndns_registration_failed', error=error)) - # XXX print warning + logger.warning(m18n.n('migrate_tsig_failed', domain=domain, + error_code=str(r.status_code), error=error)) + os.system("mv /etc/yunohost/dyndns/*+165* /tmp") return public_key_path @@ -319,8 +318,10 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): os.system("mv /etc/yunohost/dyndns/*+157* /tmp") # sleep to wait for dyndns cache invalidation + logger.warning(m18n.n('migrate_tsig_wait')) time.sleep(180) + logger.warning(m18n.n('migrate_tsig_end')) return new_key_path.rsplit(".key", 1)[0] + ".private" From 299cba623b8ac9d4cfc5edbb360826e052681ec7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 18:52:01 +0100 Subject: [PATCH 0523/1066] [fix] avoid stupid mistake --- src/yunohost/dyndns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 459a1e04e..36f30bc03 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -97,7 +97,8 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: - os.makedirs('/etc/yunohost/dyndns') + if not os.path.exists('/etc/yunohost/dyndns'): + os.makedirs('/etc/yunohost/dyndns') logger.info(m18n.n('dyndns_key_generating')) From 544ffb273e4977a421660a8b4b0d282da48087c4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 19:00:16 +0100 Subject: [PATCH 0524/1066] [mod] improve waiting time UX --- locales/en.json | 3 +++ src/yunohost/dyndns.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 421a04f56..3a8c2b13c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -211,6 +211,9 @@ "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}", "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512", "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", + "migrate_tsig_wait_2": "2min...", + "migrate_tsig_wait_3": "1min...", + "migrate_tsig_wait_3": "30 secondes...", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 36f30bc03..d4313dc8b 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -320,7 +320,13 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): # sleep to wait for dyndns cache invalidation logger.warning(m18n.n('migrate_tsig_wait')) - time.sleep(180) + time.sleep(60) + logger.warning(m18n.n('migrate_tsig_wait_2')) + time.sleep(60) + logger.warning(m18n.n('migrate_tsig_wait_3')) + time.sleep(30) + logger.warning(m18n.n('migrate_tsig_wait_4')) + time.sleep(30) logger.warning(m18n.n('migrate_tsig_end')) return new_key_path.rsplit(".key", 1)[0] + ".private" From 89358173387ddfd88e458f8e803c90e0f16cbbc1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 19:21:53 +0100 Subject: [PATCH 0525/1066] [fix] add timeout on requests --- src/yunohost/dyndns.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d4313dc8b..aabc191a1 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -90,7 +90,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Verify if domain is available try: - if requests.get('https://%s/test/%s' % (subscribe_host, domain)).status_code != 200: + if requests.get('https://%s/test/%s' % (subscribe_host, domain), timeout=30).status_code != 200: raise MoulinetteError(errno.EEXIST, m18n.n('dyndns_unavailable')) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) @@ -112,7 +112,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: - r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}) + r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: @@ -296,7 +296,7 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): data={ 'public_key_md5': base64.b64encode(public_key_md5), 'public_key_sha512': base64.b64encode(public_key_sha512), - }) + }, timeout=30) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) From 28b44401f5f97425df42c5d1a7ab7daf0aaccdb8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 19:22:10 +0100 Subject: [PATCH 0526/1066] [mod] remove debug print --- src/yunohost/dyndns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index aabc191a1..d2137f91a 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -219,7 +219,6 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, # this mean that hmac-md5 is used if "+157" in key: - print "detecting md5 key" key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host) host = domain.split('.')[1:] From 57665b6f11968dc60a65fa74a891004e135d3eb9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 19:23:47 +0100 Subject: [PATCH 0527/1066] [mod] uses builtin function to show traceback --- src/yunohost/dyndns.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d2137f91a..d65f5f7ed 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -302,14 +302,15 @@ def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): if r.status_code != 201: try: error = json.loads(r.text)['error'] - except Exception as e: - import traceback - traceback.print_exc() - print e + show_traceback = 0 + except Exception: + # failed to decode json error = r.text + show_traceback = 1 logger.warning(m18n.n('migrate_tsig_failed', domain=domain, - error_code=str(r.status_code), error=error)) + error_code=str(r.status_code), error=error), + exc_info=show_traceback) os.system("mv /etc/yunohost/dyndns/*+165* /tmp") return public_key_path From a9eea61a26e2ed78a9e2496b20150f1d8c5bcb27 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 20:07:32 +0100 Subject: [PATCH 0528/1066] [fix] overwritting key --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index b2d7c847a..a2df29a54 100644 --- a/locales/en.json +++ b/locales/en.json @@ -216,7 +216,7 @@ "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", "migrate_tsig_wait_2": "2min...", "migrate_tsig_wait_3": "1min...", - "migrate_tsig_wait_3": "30 secondes...", + "migrate_tsig_wait_4": "30 secondes...", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", From 7c63c0a34de6013b9e528437d7c705902e6ce322 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 3 Jan 2018 20:22:46 +0100 Subject: [PATCH 0529/1066] [enh] subscribe has hmac-sha512 --- src/yunohost/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index c11d0a067..6ff73d1e2 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -153,7 +153,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: - r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) + r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: From a079e3f5eef856845e3485312e989bf13eabac38 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 18:02:58 +0100 Subject: [PATCH 0530/1066] [enh] display which app is being upgraded --- locales/en.json | 1 + src/yunohost/app.py | 1 + 2 files changed, 2 insertions(+) diff --git a/locales/en.json b/locales/en.json index 8dac6e799..e3bc763bb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -34,6 +34,7 @@ "app_sources_fetch_failed": "Unable to fetch sources files", "app_unknown": "Unknown app", "app_unsupported_remote_type": "Unsupported remote type used for the app", + "app_upgrade_app_name": "Upgrading app {app}...", "app_upgrade_failed": "Unable to upgrade {app:s}", "app_upgrade_some_app_failed": "Unable to upgrade some applications", "app_upgraded": "{app:s} has been upgraded", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..e9278855e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -555,6 +555,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.info("Upgrading apps %s", ", ".join(app)) for app_instance_name in apps: + logger.warning(m18n.n('app_upgrade_app_name', app=app_instance_name)) installed = _is_installed(app_instance_name) if not installed: raise MoulinetteError(errno.ENOPKG, From b97675516ee1f9dcc54327a82e87f083f1cc94e5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 18:15:10 +0100 Subject: [PATCH 0531/1066] [enh] display app name everytime possible on requirements testing to improve UX --- locales/en.json | 10 +++++----- src/yunohost/app.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/locales/en.json b/locales/en.json index e3bc763bb..11598a473 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,7 +16,7 @@ "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", - "app_incompatible": "The app is incompatible with your YunoHost version", + "app_incompatible": "The app {app} is incompatible with your YunoHost version", "app_install_files_invalid": "Invalid installation files", "app_location_already_used": "An app is already installed in this location", "app_location_install_failed": "Unable to install the app in this location", @@ -26,11 +26,11 @@ "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", "app_not_installed": "{app:s} is not installed", "app_not_properly_removed": "{app:s} has not been properly removed", - "app_package_need_update": "The app package needs to be updated to follow YunoHost changes", + "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", "app_removed": "{app:s} has been removed", - "app_requirements_checking": "Checking required packages...", - "app_requirements_failed": "Unable to meet requirements: {error}", - "app_requirements_unmeet": "Requirements are not met, the package {pkgname} ({version}) must be {spec}", + "app_requirements_checking": "Checking required packages for {app}...", + "app_requirements_failed": "Unable to meet requirements for {app}: {error}", + "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", "app_sources_fetch_failed": "Unable to fetch sources files", "app_unknown": "Unknown app", "app_unsupported_remote_type": "Unsupported remote type used for the app", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e9278855e..3802874b6 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -581,7 +581,7 @@ def app_upgrade(auth, app=[], url=None, file=None): continue # Check requirements - _check_manifest_requirements(manifest) + _check_manifest_requirements(manifest, app_instance_name=app_instance_name) app_setting_path = APPS_SETTING_PATH + '/' + app_instance_name @@ -684,7 +684,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): app_id = manifest['id'] # Check requirements - _check_manifest_requirements(manifest) + _check_manifest_requirements(manifest, app_id) # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 @@ -1690,7 +1690,7 @@ def _encode_string(value): return value -def _check_manifest_requirements(manifest): +def _check_manifest_requirements(manifest, app_instance_name): """Check if required packages are met from the manifest""" requirements = manifest.get('requirements', dict()) @@ -1708,12 +1708,12 @@ def _check_manifest_requirements(manifest): if (not yunohost_req or not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'): raise MoulinetteError(errno.EINVAL, '{0}{1}'.format( - m18n.g('colon', m18n.n('app_incompatible')), - m18n.n('app_package_need_update'))) + m18n.g('colon', m18n.n('app_incompatible'), app=app_instance_name), + m18n.n('app_package_need_update', app=app_instance_name))) elif not requirements: return - logger.info(m18n.n('app_requirements_checking')) + logger.info(m18n.n('app_requirements_checking', app=app_instance_name)) # Retrieve versions of each required package try: @@ -1722,7 +1722,7 @@ def _check_manifest_requirements(manifest): except packages.PackageException as e: raise MoulinetteError(errno.EINVAL, m18n.n('app_requirements_failed', - error=str(e))) + error=str(e), app=app_instance_name)) # Iterate over requirements for pkgname, spec in requirements.items(): @@ -1731,7 +1731,7 @@ def _check_manifest_requirements(manifest): raise MoulinetteError( errno.EINVAL, m18n.n('app_requirements_unmeet', pkgname=pkgname, version=version, - spec=spec)) + spec=spec, app=app_instance_name)) def _parse_args_from_manifest(manifest, action, args={}, auth=None): From a194e1fa1cc655e9076dc99b6caee6d6d9c4b68d Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 4 Jan 2018 18:44:13 +0100 Subject: [PATCH 0532/1066] [enh] Maintain ssh client connexion --- data/templates/ssh/sshd_config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 695ea0d36..8c5a7fb95 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -66,6 +66,9 @@ PrintLastLog yes TCPKeepAlive yes #UseLogin no +# keep ssh sessions fresh +ClientAliveInterval 60 + #MaxStartups 10:30:60 Banner /etc/issue.net From 13aca112fa74b79f9c8097521f3f19abe24a72e2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 20:31:58 +0100 Subject: [PATCH 0533/1066] [enh] make change_url possibility available on the API for the admin --- src/yunohost/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..7ceb795d7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -330,6 +330,9 @@ def app_info(app, show_status=False, raw=False): if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) + + app_setting_path = APPS_SETTING_PATH + app + if raw: ret = app_list(filter=app, raw=True)[app] ret['settings'] = _get_app_settings(app) @@ -345,11 +348,10 @@ def app_info(app, show_status=False, raw=False): upgradable = "no" ret['upgradable'] = upgradable + ret['change_url'] = os.path.exists(os.path.join(app_setting_path, "scripts", "change_url")) return ret - app_setting_path = APPS_SETTING_PATH + app - # Retrieve manifest and status with open(app_setting_path + '/manifest.json') as f: manifest = json.loads(str(f.read())) From ab282e88a75d80bafd4eaa1c3b0d9b566177aaa0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 22:04:15 +0100 Subject: [PATCH 0534/1066] [enh] add commands to allow user to have access in ssh --- data/actionsmap/yunohost.yml | 24 ++++++++++++++++ src/yunohost/user.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 966de21df..c4e95e748 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -203,6 +203,30 @@ user: extra: pattern: *pattern_mailbox_quota + ### ssh_user_enable_ssh() + allow-ssh: + action_help: Allow the user to uses ssh + api: POST /ssh/user/enable-ssh + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + + ### ssh_user_disable_ssh() + disallow-ssh: + action_help: Disallow the user to uses ssh + api: POST /ssh/user/disable-ssh + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + ### user_info() info: action_help: Get user information diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 11f61d807..123438da3 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -25,6 +25,7 @@ """ import os import re +import pwd import json import errno import crypt @@ -435,6 +436,36 @@ def user_info(auth, username): raise MoulinetteError(167, m18n.n('user_info_failed')) +def user_allow_ssh(auth, username): + """ + Allow YunoHost user connect as ssh. + + Keyword argument: + username -- User username + """ + # TODO it would be good to support different kind of shells + + if not _get_user_for_ssh(auth, username): + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + + auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'}) + + +def user_disallow_ssh(auth, username): + """ + Disallow YunoHost user connect as ssh. + + Keyword argument: + username -- User username + """ + # TODO it would be good to support different kind of shells + + if not _get_user_for_ssh(auth, username) : + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + + auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'}) + + def _convertSize(num, suffix=''): for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: @@ -470,3 +501,28 @@ def _hash_user_password(password): salt = '$6$' + salt + '$' return '{CRYPT}' + crypt.crypt(str(password), salt) + + +def _get_user_for_ssh(auth, username, attrs=None): + if username == "admin": + admin_unix = pwd.getpwnam("admin") + return { + 'username': 'admin', + 'fullname': '', + 'mail': '', + 'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false", + 'shell': admin_unix.pw_shell, + 'home_path': admin_unix.pw_dir, + } + + # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html + user = auth.search('ou=users,dc=yunohost,dc=org', + '(&(objectclass=person)(uid=%s))' % username, + attrs) + + assert len(user) in (0, 1) + + if not user: + return None + + return user[0] From 3deb11cf8a57cec5cbf5131de603c135698d354e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 22:10:36 +0100 Subject: [PATCH 0535/1066] [enh] add ssh information in userlist for admin UI --- src/yunohost/user.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 123438da3..3cb848582 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -57,6 +57,7 @@ def user_list(auth, fields=None): 'cn': 'fullname', 'mail': 'mail', 'maildrop': 'mail-forward', + 'loginShell': 'shell', 'mailuserquota': 'mailbox-quota' } @@ -72,7 +73,7 @@ def user_list(auth, fields=None): raise MoulinetteError(errno.EINVAL, m18n.n('field_invalid', attr)) else: - attrs = ['uid', 'cn', 'mail', 'mailuserquota'] + attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] result = auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', @@ -82,6 +83,12 @@ def user_list(auth, fields=None): entry = {} for attr, values in user.items(): if values: + if attr == "loginShell": + if values[0].strip() == "/bin/false": + entry["ssh_allowed"] = False + else: + entry["ssh_allowed"] = True + entry[user_attrs[attr]] = values[0] uid = entry[user_attrs['uid']] From c55b8cec16ded23162c1cf34bbddaa7fb5b70942 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 4 Jan 2018 22:31:13 +0100 Subject: [PATCH 0536/1066] [enh] add commands to manage authorized-keys of users --- data/actionsmap/yunohost.yml | 68 +++++++++++++++++++++++ src/yunohost/ssh.py | 102 +++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/yunohost/ssh.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index c4e95e748..e47d0b04a 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1336,6 +1336,74 @@ dyndns: api: DELETE /dyndns/cron +############################# +# SSH # +############################# +ssh: + category_help: Manage ssh keys and access + actions: {} + subcategories: + authorized-keys: + subcategory_help: Manage user's authorized ssh keys + + actions: + ### ssh_authorized_keys_list() + list: + action_help: Show user's authorized ssh keys + api: GET /ssh/authorized-keys + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + + ### ssh_authorized_keys_add() + add: + action_help: Add a new authorized ssh key for this user + api: POST /ssh/authorized-keys + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + -u: + full: --public + help: Public key + extra: + required: True + -i: + full: --private + help: Private key + extra: + required: True + -n: + full: --name + help: Key name + extra: + required: True + + ### ssh_authorized_keys_remove() + remove: + action_help: Remove an authorized ssh key for this user + api: DELETE /ssh/authorized-keys + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + -k: + full: --key + help: Key as a string + extra: + required: True + + ############################# # Tools # ############################# diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py new file mode 100644 index 000000000..5f1f33b55 --- /dev/null +++ b/src/yunohost/ssh.py @@ -0,0 +1,102 @@ +# encoding: utf-8 + +import os + +from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir + +from yunohost.user import _get_user_for_ssh + + +def ssh_authorized_keys_list(auth, username): + user = _get_user_for_ssh(auth, username, ["homeDirectory"]) + if not user: + raise Exception("User with username '%s' doesn't exists" % username) + + authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys") + + if not os.path.exists(authorized_keys_file): + return [] + + keys = [] + last_comment = "" + for line in read_file(authorized_keys_file).split("\n"): + # empty line + if not line.strip(): + continue + + if line.lstrip().startswith("#"): + last_comment = line.lstrip().lstrip("#").strip() + continue + + # assuming a key per non empty line + key = line.strip() + keys.append({ + "key": key, + "name": last_comment, + }) + + last_comment = "" + + return {"keys": keys} + + +def ssh_authorized_keys_add(auth, username, key, comment): + user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"]) + if not user: + raise Exception("User with username '%s' doesn't exists" % username) + + authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys") + + if not os.path.exists(authorized_keys_file): + # ensure ".ssh" exists + mkdir(os.path.join(user["homeDirectory"][0], ".ssh"), + force=True, parents=True, uid=user["uid"][0]) + + # create empty file to set good permissions + write_to_file(authorized_keys_file, "") + chown(authorized_keys_file, uid=user["uid"][0]) + chmod(authorized_keys_file, 0600) + + authorized_keys_content = read_file(authorized_keys_file) + + authorized_keys_content += "\n" + authorized_keys_content += "\n" + + if comment and comment.strip(): + if not comment.lstrip().startswith("#"): + comment = "# " + comment + authorized_keys_content += comment.replace("\n", " ").strip() + authorized_keys_content += "\n" + + authorized_keys_content += key.strip() + authorized_keys_content += "\n" + + write_to_file(authorized_keys_file, authorized_keys_content) + + +def ssh_authorized_keys_remove(auth, username, key): + user = _get_user(auth, username, ["homeDirectory", "uid"]) + if not user: + raise Exception("User with username '%s' doesn't exists" % username) + + authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys") + + if not os.path.exists(authorized_keys_file): + raise Exception("this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file)) + + authorized_keys_content = read_file(authorized_keys_file) + + if key not in authorized_keys_content: + raise Exception("Key '{}' is not present in authorized_keys".format(key)) + + # don't delete the previous comment because we can't verify if it's legit + + # this regex approach failed for some reasons and I don't know why :( + # authorized_keys_content = re.sub("{} *\n?".format(key), + # "", + # authorized_keys_content, + # flags=re.MULTILINE) + + authorized_keys_content = authorized_keys_content.replace(key, "") + + write_to_file(authorized_keys_file, authorized_keys_content) From 1e5323eb08c6e268feffc4a107ff4a86e69b96a4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 5 Jan 2018 00:13:26 +0100 Subject: [PATCH 0537/1066] [enh] handle root user for being allowed to work on his authorized keys --- src/yunohost/user.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 3cb848582..793ccaf7a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -36,10 +36,13 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file from yunohost.service import service_status logger = getActionLogger('yunohost.user') +SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" + def user_list(auth, fields=None): """ @@ -58,6 +61,7 @@ def user_list(auth, fields=None): 'mail': 'mail', 'maildrop': 'mail-forward', 'loginShell': 'shell', + 'homeDirectory': 'home_path', 'mailuserquota': 'mailbox-quota' } @@ -511,6 +515,34 @@ def _hash_user_password(password): def _get_user_for_ssh(auth, username, attrs=None): + def ssh_root_login_status(auth): + # XXX temporary placed here for when the ssh_root commands are integrated + # extracted from https://github.com/YunoHost/yunohost/pull/345 + # XXX should we support all the options? + # this is the content of "man sshd_config" + # PermitRootLogin + # Specifies whether root can log in using ssh(1). The argument must be + # “yes”, “without-password”, “forced-commands-only”, or “no”. The + # default is “yes”. + sshd_config_content = read_file(SSHD_CONFIG_PATH) + + if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$", + sshd_config_content, re.MULTILINE): + return {"PermitRootLogin": False} + + return {"PermitRootLogin": True} + + if username == "root": + root_unix = pwd.getpwnam("root") + return { + 'username': 'root', + 'fullname': '', + 'mail': '', + 'ssh_allowed': ssh_root_login_status(auth)["PermitRootLogin"], + 'shell': root_unix.pw_shell, + 'home_path': root_unix.pw_dir, + } + if username == "admin": admin_unix = pwd.getpwnam("admin") return { From eef247d34b856397806c4fd83444b5d4259200c0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 02:07:28 +0200 Subject: [PATCH 0538/1066] [enh] start to work on journals --- data/actionsmap/yunohost.yml | 12 ++++++++++ src/yunohost/journals.py | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/yunohost/journals.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 966de21df..ea7f5e14f 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1588,3 +1588,15 @@ hook: -d: full: --chdir help: The directory from where the script will be executed + +############################# +# Journals # +############################# +journals: + category_help: Manage debug journals + actions: + + ### domain_list() + list: + action_help: List journals + api: GET /journals diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py new file mode 100644 index 000000000..086af2113 --- /dev/null +++ b/src/yunohost/journals.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_journals.py + + Manage debug journals +""" + +import os + +from moulinette.utils.log import getActionLogger + +JOURNALS_PATH = '/var/log/journals/' + +logger = getActionLogger('yunohost.journals') + + +def journals_list(): + """ + List domains + + Keyword argument: + filter -- LDAP filter used to search + offset -- Starting number for domain fetching + limit -- Maximum number of domain fetched + + """ + return {} From db83eb5a2b17f4941f16dce5d4a284a806d29c19 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 02:08:59 +0200 Subject: [PATCH 0539/1066] [enh] if not journals available, return nothing --- src/yunohost/journals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 086af2113..1e4481ed0 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -43,4 +43,8 @@ def journals_list(): limit -- Maximum number of domain fetched """ + + if not os.path.exists(JOURNALS_PATH): + return {} + return {} From 08b7c4f6d7fcdd3cd696979324334a6af3113d2c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 05:32:58 +0200 Subject: [PATCH 0540/1066] [enh] start Journal class --- src/yunohost/journals.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 1e4481ed0..20037bbc0 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -48,3 +48,41 @@ def journals_list(): return {} return {} + + +class Journal(object): + def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): + self.name = name + self.category = category + self.first_write = False + self.started_at = None + + self.on_stdout = [] if on_stdout is None else on_stdout + self.on_stderr = [] if on_stderr is None else on_stderr + self.on_write = [] if on_write is None else on_write + + self.additional_information = kwargs + + def write(self, line): + print "[journal]", line.rstrip() + + def stdout(self, line): + for i in self.on_stdout: + i(line) + + self.write(line) + + def stderr(self, line): + for i in self.on_stderr: + i(line) + + self.write(line) + + def as_callbacks_tuple(self, stdout=None, stderr=None): + if stdout: + self.on_stdout.append(stdout) + + if stderr: + self.on_stderr.append(stderr) + + return (self.stdout, self.stderr) From 1355b2b754c51ca4e7c7491e1fb4e999e045243f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 05:33:10 +0200 Subject: [PATCH 0541/1066] [enh] allow to use a journal for hook exec --- src/yunohost/hook.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 1f971edb6..95ed2aec4 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -297,7 +297,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="admin"): + chdir=None, env=None, journal=None, user="admin"): """ Execute hook from a file with arguments @@ -359,11 +359,18 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) - # Define output callbacks and call command - callbacks = ( - lambda l: logger.info(l.rstrip()), - lambda l: logger.warning(l.rstrip()), - ) + if journal is None: + # Define output callbacks and call command + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + else: + callbacks = journal.as_callbacks_tuple( + stdout=lambda l: logger.info(l.rstrip()), + stderr=lambda l: logger.warning(l.rstrip()), + ) + returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From 0668c7e350d6648f119d301713474e42e481126b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 06:26:02 +0200 Subject: [PATCH 0542/1066] [enh] journal class works as expected \o/ --- src/yunohost/journals.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 20037bbc0..ab245a78f 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -25,6 +25,9 @@ """ import os +import yaml + +from datetime import datetime from moulinette.utils.log import getActionLogger @@ -54,17 +57,47 @@ class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): self.name = name self.category = category - self.first_write = False + self.first_write = True self.started_at = None + self.path = os.path.join(JOURNALS_PATH, category) + + self.fd = None + self.on_stdout = [] if on_stdout is None else on_stdout self.on_stderr = [] if on_stderr is None else on_stderr self.on_write = [] if on_write is None else on_write self.additional_information = kwargs + def __del__(self): + if self.fd: + self.fd.close() + def write(self, line): - print "[journal]", line.rstrip() + if self.first_write: + self._do_first_write() + self.first_write = False + + self.fd.write("%s: " % datetime.now().strftime("%F_%X").replace(":", "-")) + self.fd.write(line.rstrip()) + self.fd.write("\n") + self.fd.flush() + + def _do_first_write(self): + self.started_at = datetime.now() + + if not os.path.exists(self.path): + os.makedirs(self.path) + + file_name = "%s_%s.journal" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + + serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) + + self.fd = open(os.path.join(self.path, file_name), "w") + + self.fd.write(serialized_additional_information) + self.fd.write("\n---\n") def stdout(self, line): for i in self.on_stdout: From a6d89f8ea1f007a49c0d38c1556b67bf9f2b7541 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 07:42:50 +0200 Subject: [PATCH 0543/1066] [enh] journal list correctly list available journals --- src/yunohost/journals.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index ab245a78f..0be01853f 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -50,7 +50,23 @@ def journals_list(): if not os.path.exists(JOURNALS_PATH): return {} - return {} + result = {} + + for category in os.listdir(JOURNALS_PATH): + result[category] = [] + for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + + journal = journal[:-len(".journal")] + journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + result[category].append({ + "started_at": journal_datetime, + "name": " ".join(journal[:-2]), + }) + + result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + + return result class Journal(object): From 5fba28426037fb58e82fee7bed059c3cba51fb66 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 07:49:52 +0200 Subject: [PATCH 0544/1066] [enh] generate a journal for app installation --- src/yunohost/app.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..03c8b9d93 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -653,6 +653,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.journals import Journal # Fetch or extract sources try: @@ -738,7 +739,12 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root") + args=args_list, env=env_dict, user="root" + journal = Journal( + ["install", app_instance_name], + "app", args=args_list, env=env_dict + ), + ) except (KeyboardInterrupt, EOFError): install_retcode = -1 except: From 5eda0fe20e8b9d1f8be7734c521ce240df3401e7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:06:52 +0200 Subject: [PATCH 0545/1066] [fix] avoid a possible thread problem --- src/yunohost/journals.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 0be01853f..b8807fa94 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -74,7 +74,9 @@ class Journal(object): self.name = name self.category = category self.first_write = True - self.started_at = None + + # this help uniformise file name and avoir threads concurrency errors + self.started_at = datetime.now() self.path = os.path.join(JOURNALS_PATH, category) @@ -101,8 +103,6 @@ class Journal(object): self.fd.flush() def _do_first_write(self): - self.started_at = datetime.now() - if not os.path.exists(self.path): os.makedirs(self.path) From d159a437f67d7784517de51871803af6f0ec77ee Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:25:11 +0200 Subject: [PATCH 0546/1066] [enh] journal on app remove when installation failed --- src/yunohost/app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 03c8b9d93..bad492fae 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -739,7 +739,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root" + args=args_list, env=env_dict, user="root", journal = Journal( ["install", app_instance_name], "app", args=args_list, env=env_dict @@ -761,7 +761,12 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Execute remove script remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root") + args=[app_instance_name], env=env_dict_remove, user="root", + journal = Journal( + ["remove", app_instance_name, "failed install"], + "app", args=[app_instance_name], env=env_dict_remove, + ), + ) if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) From de7d26521b623d153871c45466d6bafb41e71cb6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:25:20 +0200 Subject: [PATCH 0547/1066] [enh] journal on app remove --- src/yunohost/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bad492fae..c7e1cfec9 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -812,6 +812,7 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove + from yunohost.journals import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, @@ -837,7 +838,8 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: + journal = Journal(["remove", app], "app", args=args_list, env=env_dict) + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root", journal=journal) == 0: logger.success(m18n.n('app_removed', app=app)) if os.path.exists(app_setting_path): From 278f692a2fc7773bcdc2c39cbc4b31e4ebf8f23c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:42:49 +0200 Subject: [PATCH 0548/1066] [enh] journal on app upgrade --- src/yunohost/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c7e1cfec9..0662fa98c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -531,6 +531,7 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec + from yunohost.journals import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' @@ -603,7 +604,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: + journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journa=journal) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) From 5dcf0b782f62691dcaf01c462faaf4c725c6ed75 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 08:46:12 +0200 Subject: [PATCH 0549/1066] [enh] allow to limit to number of journals per categories --- data/actionsmap/yunohost.yml | 5 +++++ src/yunohost/journals.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ea7f5e14f..05528bb1a 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1600,3 +1600,8 @@ journals: list: action_help: List journals api: GET /journals + arguments: + -l: + full: --limit + help: Maximum number of journals per categories + type: int diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index b8807fa94..e07a96fa2 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -36,7 +36,7 @@ JOURNALS_PATH = '/var/log/journals/' logger = getActionLogger('yunohost.journals') -def journals_list(): +def journals_list(limit=None): """ List domains @@ -66,6 +66,9 @@ def journals_list(): result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + if limit is not None: + result[category] = result[category][:limit] + return result From b18092e329a494f9aa57aa84a71be3dd9b395cde Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 09:48:41 +0200 Subject: [PATCH 0550/1066] [enh] display more informations on journals list --- src/yunohost/journals.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index e07a96fa2..90aa80398 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -56,12 +56,16 @@ def journals_list(limit=None): result[category] = [] for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + file_name = journal + journal = journal[:-len(".journal")] journal = journal.split("_") journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") result[category].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), + "file_name": file_name, + "path": os.path.join(JOURNALS_PATH, category, file_name), }) result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) From 6cb179703e8a940719448dc5ef083914637a04d2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 09:51:18 +0200 Subject: [PATCH 0551/1066] [fix] bad docstring --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 05528bb1a..b4e160e59 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1596,7 +1596,7 @@ journals: category_help: Manage debug journals actions: - ### domain_list() + ### journals_list() list: action_help: List journals api: GET /journals From 19a29d9b3471f85370ac16610100cea3470df155 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:00:14 +0200 Subject: [PATCH 0552/1066] [mod] use more classical datetime format for logs --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 90aa80398..18a0d5672 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -104,7 +104,7 @@ class Journal(object): self._do_first_write() self.first_write = False - self.fd.write("%s: " % datetime.now().strftime("%F_%X").replace(":", "-")) + self.fd.write("%s: " % datetime.now().strftime("%F %X")) self.fd.write(line.rstrip()) self.fd.write("\n") self.fd.flush() From 3eed2a28fee4dcef4685b7be515cb9ea069ac905 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:05:43 +0200 Subject: [PATCH 0553/1066] [enh] add journals display command --- data/actionsmap/yunohost.yml | 8 ++++++++ src/yunohost/journals.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b4e160e59..91b6c2c76 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1605,3 +1605,11 @@ journals: full: --limit help: Maximum number of journals per categories type: int + + ### journals_display() + display: + action_help: List journals + api: GET /journals/ + arguments: + file_name: + help: Journal file name diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 18a0d5672..b9de4bcda 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -76,6 +76,40 @@ def journals_list(limit=None): return result +def journals_display(file_name): + if not os.path.exists(JOURNALS_PATH): + # TODO raise exception + return {} + + for category in os.listdir(JOURNALS_PATH): + for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + if journal != file_name: + continue + + with open(os.path.join(JOURNALS_PATH, category, file_name), "r") as content: + content = content.read() + + journal = journal[:-len(".journal")] + journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + + infos, logs = content.split("\n---\n", 1) + infos = yaml.safe_load(infos) + logs = [x.split(": ", 1) for x in logs.split("\n") if x] + + return { + "started_at": journal_datetime, + "name": " ".join(journal[:-2]), + "file_name": file_name, + "path": os.path.join(JOURNALS_PATH, category, file_name), + "metadata": infos, + "logs": logs, + } + + # TODO raise exception + return {} + + class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): self.name = name From 1fc7065eafa4fdcb5a47007aba53ae674808b7fe Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 24 Jun 2016 10:06:13 +0200 Subject: [PATCH 0554/1066] [doc] add TODO comment --- src/yunohost/journals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index b9de4bcda..425ec15c8 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -112,6 +112,7 @@ def journals_display(file_name): class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): + # TODO add a way to not save password on app installation self.name = name self.category = category self.first_write = True From 9a08b8ad48cc3f634b0b063306bd5fcd3e2e479b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 04:02:14 +0200 Subject: [PATCH 0555/1066] [mod] modify data format because mustache is lame --- src/yunohost/journals.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 425ec15c8..f59e5c2c7 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -47,13 +47,13 @@ def journals_list(limit=None): """ + result = {"categories": []} + if not os.path.exists(JOURNALS_PATH): - return {} + return result - result = {} - - for category in os.listdir(JOURNALS_PATH): - result[category] = [] + for category in sorted(os.listdir(JOURNALS_PATH)): + result["categories"].append({"name": category, "journals": []}) for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): file_name = journal @@ -61,17 +61,17 @@ def journals_list(limit=None): journal = journal[:-len(".journal")] journal = journal.split("_") journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") - result[category].append({ + result["categories"][-1]["journals"].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), "file_name": file_name, "path": os.path.join(JOURNALS_PATH, category, file_name), }) - result[category] = list(reversed(sorted(result[category], key=lambda x: x["started_at"]))) + result["categories"][-1]["journals"] = list(reversed(sorted(result["categories"][-1]["journals"], key=lambda x: x["started_at"]))) if limit is not None: - result[category] = result[category][:limit] + result["categories"][-1]["journals"] = result["categories"][-1]["journals"][:limit] return result From 804b6ee96236cef10fda9b38fe80259f933b6a14 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 04:05:33 +0200 Subject: [PATCH 0556/1066] [mod] store journals in yunohost varlog dir --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index f59e5c2c7..7ee0c2694 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -31,7 +31,7 @@ from datetime import datetime from moulinette.utils.log import getActionLogger -JOURNALS_PATH = '/var/log/journals/' +JOURNALS_PATH = '/var/log/yunohost/journals/' logger = getActionLogger('yunohost.journals') From 41aef0f2d645e6916d4a4e23bb1e99b34cda08ea Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 28 Jun 2016 06:07:21 +0200 Subject: [PATCH 0557/1066] [mod] adapt logs format to stupid mustache --- src/yunohost/journals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 7ee0c2694..640eac419 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -95,7 +95,7 @@ def journals_display(file_name): infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) - logs = [x.split(": ", 1) for x in logs.split("\n") if x] + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] return { "started_at": journal_datetime, From 4543a23a92044f56440a8dc060b7aa12c926ed2d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 01:09:32 +0100 Subject: [PATCH 0558/1066] [enh] add journal to change_url --- src/yunohost/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0662fa98c..bac85eaec 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -431,6 +431,7 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec + from yunohost.journals import Journal installed = _is_installed(app) if not installed: @@ -488,8 +489,8 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) - # XXX journal - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: + journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journa=journal) != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl From 021a989c03b251e74f5dae8a9781aaf949c22f5f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 01:43:07 +0100 Subject: [PATCH 0559/1066] [fix] documentation was lying --- src/yunohost/journals.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 640eac419..e01f364a5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -38,13 +38,10 @@ logger = getActionLogger('yunohost.journals') def journals_list(limit=None): """ - List domains + List available journals Keyword argument: - filter -- LDAP filter used to search - offset -- Starting number for domain fetching - limit -- Maximum number of domain fetched - + limit -- Maximum number of journals per categories """ result = {"categories": []} From d92b8562d0478d69d5349b664c8ac24857dfa5ae Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:18:28 +0100 Subject: [PATCH 0560/1066] [doc] document journal display --- data/actionsmap/yunohost.yml | 2 +- src/yunohost/journals.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 91b6c2c76..1de6c729d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1608,7 +1608,7 @@ journals: ### journals_display() display: - action_help: List journals + action_help: Display a journal content api: GET /journals/ arguments: file_name: diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index e01f364a5..5677d77e5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -74,6 +74,13 @@ def journals_list(limit=None): def journals_display(file_name): + """ + Display a journal content + + Argument: + file_name + """ + if not os.path.exists(JOURNALS_PATH): # TODO raise exception return {} From 223e41743b88bcffcce7390895bd974455deb9bb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:18:42 +0100 Subject: [PATCH 0561/1066] [mod] spacing for lisiblity --- src/yunohost/journals.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index 5677d77e5..d6a41bed5 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -57,7 +57,9 @@ def journals_list(limit=None): journal = journal[:-len(".journal")] journal = journal.split("_") + journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + result["categories"][-1]["journals"].append({ "started_at": journal_datetime, "name": " ".join(journal[:-2]), From 388e611c459701dcabecbbd7726c40353f71144e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 11:20:41 +0100 Subject: [PATCH 0562/1066] [enh] raise exception when journal doesn't exist --- locales/en.json | 1 + src/yunohost/journals.py | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 8dac6e799..92864f348 100644 --- a/locales/en.json +++ b/locales/en.json @@ -201,6 +201,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "journal_does_exists": "There is not journal with the name '{journal}', use 'yunohost journal list to see all available journals'", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/journals.py b/src/yunohost/journals.py index d6a41bed5..0f1785697 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/journals.py @@ -26,9 +26,12 @@ import os import yaml +import errno from datetime import datetime +from moulinette import m18n +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger JOURNALS_PATH = '/var/log/yunohost/journals/' @@ -84,8 +87,8 @@ def journals_display(file_name): """ if not os.path.exists(JOURNALS_PATH): - # TODO raise exception - return {} + raise MoulinetteError(errno.EINVAL, + m18n.n('journal_does_exists', journal=file_name)) for category in os.listdir(JOURNALS_PATH): for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): @@ -112,8 +115,8 @@ def journals_display(file_name): "logs": logs, } - # TODO raise exception - return {} + raise MoulinetteError(errno.EINVAL, + m18n.n('journal_does_exists', journal=file_name)) class Journal(object): From beb432bc6fca34ad11141083a3bef115197b6c9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 18:05:01 +0100 Subject: [PATCH 0563/1066] Add a draft migration for tsig_sha256 based on existing stuff in dyndns.py --- .../0002_migrate_to_tsig_sha256.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py new file mode 100644 index 000000000..df27a1f9f --- /dev/null +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -0,0 +1,79 @@ +import glob + +from yunohost.tools import Migration +from moulinette.utils.log import getActionLogger +logger = getActionLogger('yunohost.migration') + + +class MigrateToTsigSha512(Migration): + "Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG": + + + def backward(self): + # Not possible because that's a non-reversible operation ? + pass + + + def forward(self): + + dyn_host="dyndns.yunohost.org" + + try: + (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) + except MoulinetteError: + logger.warning("migrate_tsig_not_needed") + + logger.warning(m18n.n('migrate_tsig_start', domain=domain)) + public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" + public_key_md5 = open(public_key_path).read().strip().split(' ')[-1] + + os.system('cd /etc/yunohost/dyndns && ' + 'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain) + os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') + + # +165 means that this file store a hmac-sha512 key + new_key_path = glob.glob('/etc/yunohost/dyndns/*+165*.key')[0] + public_key_sha512 = open(new_key_path).read().strip().split(' ', 6)[-1] + + try: + r = requests.put('https://%s/migrate_key_to_sha512/' % (dyn_host), + data={ + 'public_key_md5': base64.b64encode(public_key_md5), + 'public_key_sha512': base64.b64encode(public_key_sha512), + }, timeout=30) + except requests.ConnectionError: + raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + + if r.status_code != 201: + try: + error = json.loads(r.text)['error'] + show_traceback = 0 + except Exception: + # failed to decode json + error = r.text + show_traceback = 1 + + logger.warning(m18n.n('migrate_tsig_failed', domain=domain, + error_code=str(r.status_code), error=error), + exc_info=show_traceback) + + os.system("mv /etc/yunohost/dyndns/*+165* /tmp") + return public_key_path + + # remove old certificates + os.system("mv /etc/yunohost/dyndns/*+157* /tmp") + + # sleep to wait for dyndns cache invalidation + logger.warning(m18n.n('migrate_tsig_wait')) + time.sleep(60) + logger.warning(m18n.n('migrate_tsig_wait_2')) + time.sleep(60) + logger.warning(m18n.n('migrate_tsig_wait_3')) + time.sleep(30) + logger.warning(m18n.n('migrate_tsig_wait_4')) + time.sleep(30) + + logger.warning(m18n.n('migrate_tsig_end')) + return new_key_path.rsplit(".key", 1)[0] + ".private" + + From aea13bc3e8816375b37ec33624d2fdcc6c5d1807 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 18:36:29 +0100 Subject: [PATCH 0564/1066] [enh] save the conf/ directory of app during installation and upgrade --- src/yunohost/app.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..87178464c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -621,10 +621,13 @@ def app_upgrade(auth, app=[], url=None, file=None): with open(app_setting_path + '/status.json', 'w+') as f: json.dump(status, f) - # Replace scripts and manifest - os.system('rm -rf "%s/scripts" "%s/manifest.json"' % (app_setting_path, app_setting_path)) + # Replace scripts and manifest and conf (if exists) + os.system('rm -rf "%s/scripts" "%s/manifest.json %/conf"' % (app_setting_path, app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "conf")): + os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) + # So much win upgraded_apps.append(app_instance_name) logger.success(m18n.n('app_upgraded', app=app_instance_name)) @@ -733,6 +736,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "conf")): + os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) + # Execute the app install script install_retcode = 1 try: From 7d9307c4c60edd2e7001100c86db95882b22ccca Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 6 Jan 2018 18:44:31 +0100 Subject: [PATCH 0565/1066] [doc$ add PULL_REQUEST_TEMPLATE.md --- src/yunohost/.github/PULL_REQUEST_TEMPLATE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/yunohost/.github/PULL_REQUEST_TEMPLATE.md diff --git a/src/yunohost/.github/PULL_REQUEST_TEMPLATE.md b/src/yunohost/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..065c2dd90 --- /dev/null +++ b/src/yunohost/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## The problem + +... + +## Solution + +... + +## PR Status + +... + + +## Validation + +- [ ] Principle agreement 0/2 : +- [ ] Quick review 0/1 : +- [ ] Simple test 0/1 : +- [ ] Deep review 0/1 : From 6f8912f0d431df79ccfe1ed0811e1d80911815e5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 18:50:30 +0100 Subject: [PATCH 0566/1066] Fix a few things following tests --- .../0002_migrate_to_tsig_sha256.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index df27a1f9f..26ea3a8b8 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -1,13 +1,21 @@ import glob +import os +import requests +import base64 +import time + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from moulinette.utils.log import getActionLogger +from yunohost.dyndns import _guess_current_dyndns_domain + logger = getActionLogger('yunohost.migration') -class MigrateToTsigSha512(Migration): - "Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG": - +class MyMigration(Migration): + "Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG" def backward(self): # Not possible because that's a non-reversible operation ? @@ -22,6 +30,7 @@ class MigrateToTsigSha512(Migration): (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) except MoulinetteError: logger.warning("migrate_tsig_not_needed") + return logger.warning(m18n.n('migrate_tsig_start', domain=domain)) public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" From 465aff4581cac68bd8776ab6a59c9a7fd868bf39 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 20:51:20 +0100 Subject: [PATCH 0567/1066] Be able to fetch a single migration by its name --- src/yunohost/tools.py | 63 ++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 042671125..024ae0da4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -717,30 +717,10 @@ def tools_migrations_migrate(target=None, skip=False): # loading all migrations for migration in tools_migrations_list()["migrations"]: - logger.debug(m18n.n('migrations_loading_migration', - number=migration["number"], - name=migration["name"], - )) - - try: - # this is python builtin method to import a module using a name, we - # use that to import the migration as a python object so we'll be - # able to run it in the next loop - module = import_module("yunohost.data_migrations.{file_name}".format(**migration)) - except Exception: - import traceback - traceback.print_exc() - - raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', - number=migration["number"], - name=migration["name"], - )) - break - migrations.append({ "number": migration["number"], "name": migration["name"], - "module": module, + "module": _get_migration_module(migration), }) migrations = sorted(migrations, key=lambda x: x["number"]) @@ -877,6 +857,47 @@ def _get_migrations_list(): return sorted(migrations) +def _get_migration_by_name(migration_name, with_module=True): + """ + Low-level / "private" function to find a migration by its name + """ + + migrations = tools_migrations_list()["migrations"] + + matches = [ m for m in migrations if m["name"] == migration_name ] + + assert len(matches) == 1, "Unable to find migration with name %s" % migration_name + + migration = matches[0] + + if with_module: + migration["module"] = _get_migration_module(migration) + + return migration + + +def _get_migration_module(migration): + + logger.debug(m18n.n('migrations_loading_migration', + number=migration["number"], + name=migration["name"], + )) + + try: + # this is python builtin method to import a module using a name, we + # use that to import the migration as a python object so we'll be + # able to run it in the next loop + return import_module("yunohost.data_migrations.{file_name}".format(**migration)) + except Exception: + import traceback + traceback.print_exc() + + raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', + number=migration["number"], + name=migration["name"], + )) + + class Migration(object): def migrate(self): From 7e02e355d51dc819b183d697d9acc0e8c731248d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 21:01:10 +0100 Subject: [PATCH 0568/1066] Call the new migration from dyndns.py when MD5 is detected --- .../0002_migrate_to_tsig_sha256.py | 16 ++--- src/yunohost/dyndns.py | 68 +++---------------- 2 files changed, 19 insertions(+), 65 deletions(-) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 26ea3a8b8..257f3525a 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -22,15 +22,15 @@ class MyMigration(Migration): pass - def forward(self): + def forward(self, dyn_host="dyndns.yunohost.org", domain=None, private_key_path=None): - dyn_host="dyndns.yunohost.org" - - try: - (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) - except MoulinetteError: - logger.warning("migrate_tsig_not_needed") - return + if domain in None or private_key_path is None: + try: + (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) + assert "+157" in private_key_path + except MoulinetteError: + logger.warning("migrate_tsig_not_needed") + return logger.warning(m18n.n('migrate_tsig_start', domain=domain)) public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 6ff73d1e2..851d04f45 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -223,9 +223,18 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] - # this mean that hmac-md5 is used + # This mean that hmac-md5 is used + # (Re?)Trigger the migration to sha256 and return immediately. + # The actual update will be done in next run. if "+157" in key: - key = _migrate_from_md5_tsig_to_sha512_tsig(key, domain, dyn_host) + from yunohost.tools import _get_migration_by_name + migration = _get_migration_by_name("migrate_to_tsig_sha256") + try: + migration["module"].MyMigration().migrate(dyn_host, domain, key) + except Exception as e: + logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) + + return # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' host = domain.split('.')[1:] @@ -292,61 +301,6 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, write_to_file(OLD_IPV6_FILE, ipv6) -def _migrate_from_md5_tsig_to_sha512_tsig(private_key_path, domain, dyn_host): - logger.warning(m18n.n('migrate_tsig_start', domain=domain)) - public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" - public_key_md5 = open(public_key_path).read().strip().split(' ')[-1] - - os.system('cd /etc/yunohost/dyndns && ' - 'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain) - os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') - - # +165 means that this file store a hmac-sha512 key - new_key_path = glob.glob('/etc/yunohost/dyndns/*+165*.key')[0] - public_key_sha512 = open(new_key_path).read().strip().split(' ', 6)[-1] - - try: - r = requests.put('https://%s/migrate_key_to_sha512/' % (dyn_host), - data={ - 'public_key_md5': base64.b64encode(public_key_md5), - 'public_key_sha512': base64.b64encode(public_key_sha512), - }, timeout=30) - except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) - - if r.status_code != 201: - try: - error = json.loads(r.text)['error'] - show_traceback = 0 - except Exception: - # failed to decode json - error = r.text - show_traceback = 1 - - logger.warning(m18n.n('migrate_tsig_failed', domain=domain, - error_code=str(r.status_code), error=error), - exc_info=show_traceback) - - os.system("mv /etc/yunohost/dyndns/*+165* /tmp") - return public_key_path - - # remove old certificates - os.system("mv /etc/yunohost/dyndns/*+157* /tmp") - - # sleep to wait for dyndns cache invalidation - logger.warning(m18n.n('migrate_tsig_wait')) - time.sleep(60) - logger.warning(m18n.n('migrate_tsig_wait_2')) - time.sleep(60) - logger.warning(m18n.n('migrate_tsig_wait_3')) - time.sleep(30) - logger.warning(m18n.n('migrate_tsig_wait_4')) - time.sleep(30) - - logger.warning(m18n.n('migrate_tsig_end')) - return new_key_path.rsplit(".key", 1)[0] + ".private" - - def dyndns_installcron(): """ Install IP update cron From 6bf80877af7ae0b0d3ad2530ea66e617ae8e2c72 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 21:42:47 +0100 Subject: [PATCH 0569/1066] Fixing a few stuff after tests.. --- .../0002_migrate_to_tsig_sha256.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 257f3525a..98c591716 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -3,6 +3,7 @@ import os import requests import base64 import time +import json from moulinette import m18n from moulinette.core import MoulinetteError @@ -21,14 +22,13 @@ class MyMigration(Migration): # Not possible because that's a non-reversible operation ? pass + def migrate(self, dyn_host="dyndns.yunohost.org", domain=None, private_key_path=None): - def forward(self, dyn_host="dyndns.yunohost.org", domain=None, private_key_path=None): - - if domain in None or private_key_path is None: + if domain is None or private_key_path is None: try: (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) - assert "+157" in private_key_path - except MoulinetteError: + #assert "+157" in private_key_path + except (MoulinetteError, AssertionError): logger.warning("migrate_tsig_not_needed") return @@ -56,18 +56,21 @@ class MyMigration(Migration): if r.status_code != 201: try: error = json.loads(r.text)['error'] - show_traceback = 0 except Exception: # failed to decode json error = r.text - show_traceback = 1 - logger.warning(m18n.n('migrate_tsig_failed', domain=domain, - error_code=str(r.status_code), error=error), - exc_info=show_traceback) + import traceback + from StringIO import StringIO + stack = StringIO() + traceback.print_exc(file=stack) + logger.error(stack.getvalue()) + # Migration didn't succeed, so we rollback and raise an exception os.system("mv /etc/yunohost/dyndns/*+165* /tmp") - return public_key_path + + raise MoulinetteError(m18n.n('migrate_tsig_failed', domain=domain, + error_code=str(r.status_code), error=error)) # remove old certificates os.system("mv /etc/yunohost/dyndns/*+157* /tmp") @@ -83,6 +86,5 @@ class MyMigration(Migration): time.sleep(30) logger.warning(m18n.n('migrate_tsig_end')) - return new_key_path.rsplit(".key", 1)[0] + ".private" - + return From 08caf2e07ff380f0613668c1e93e85d5025116b9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 6 Jan 2018 22:42:37 +0100 Subject: [PATCH 0570/1066] Fixing a few stuff after tests.. --- src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 98c591716..15f092b75 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -27,7 +27,7 @@ class MyMigration(Migration): if domain is None or private_key_path is None: try: (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) - #assert "+157" in private_key_path + assert "+157" in private_key_path except (MoulinetteError, AssertionError): logger.warning("migrate_tsig_not_needed") return @@ -63,7 +63,7 @@ class MyMigration(Migration): import traceback from StringIO import StringIO stack = StringIO() - traceback.print_exc(file=stack) + traceback.print_stack(file=stack) logger.error(stack.getvalue()) # Migration didn't succeed, so we rollback and raise an exception From f20ef340dc9aa648b5d9d94629076717a7a7b36e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 02:04:18 +0100 Subject: [PATCH 0571/1066] [fix] .github was not in the right folder --- {src/yunohost/.github => .github}/PULL_REQUEST_TEMPLATE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/yunohost/.github => .github}/PULL_REQUEST_TEMPLATE.md (100%) diff --git a/src/yunohost/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from src/yunohost/.github/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md From 297026e6548ef1763f6c5d51bef1c940f02e532d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 16:43:10 +0100 Subject: [PATCH 0572/1066] [fix] improve UX, previous message was unclear for users qsd --- locales/en.json | 2 +- src/yunohost/app.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 8dac6e799..6e6ed6daa 100644 --- a/locales/en.json +++ b/locales/en.json @@ -19,7 +19,7 @@ "app_incompatible": "The app is incompatible with your YunoHost version", "app_install_files_invalid": "Invalid installation files", "app_location_already_used": "An app is already installed in this location", - "app_location_install_failed": "Unable to install the app in this location", + "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", "app_location_unavailable": "This url is not available or conflicts with an already installed app", "app_manifest_invalid": "Invalid app manifest: {error}", "app_no_upgrade": "No app to upgrade", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 403e76cc4..5599c0d53 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1169,10 +1169,12 @@ def app_checkurl(auth, url, app=None): continue if path == p: raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_already_used')) + m18n.n('app_location_already_used', + app=a["id"], path=path)) elif path.startswith(p) or p.startswith(path): raise MoulinetteError(errno.EPERM, - m18n.n('app_location_install_failed')) + m18n.n('app_location_install_failed', + other_path=p, other_app=a['id'])) if app is not None and not installed: app_setting(app, 'domain', value=domain) From 9cd760defd74e815408ece4a7960631692681a0d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 17:04:48 +0100 Subject: [PATCH 0573/1066] [enh] make exceptions messages more obivous --- locales/en.json | 3 ++- src/yunohost/app.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6e6ed6daa..1edf2609c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -18,7 +18,8 @@ "app_id_invalid": "Invalid app id", "app_incompatible": "The app is incompatible with your YunoHost version", "app_install_files_invalid": "Invalid installation files", - "app_location_already_used": "An app is already installed in this location", + "app_location_already_used": "The app '{app}' is already installed on that location ({path})", + "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", "app_location_unavailable": "This url is not available or conflicts with an already installed app", "app_manifest_invalid": "Invalid app manifest: {error}", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5599c0d53..851fcdaa6 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1016,7 +1016,9 @@ def app_makedefault(auth, app, domain=None): if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, - m18n.n('app_location_already_used')) + m18n.n('app_make_default_location_already_used', + app=app, domain=app_domain, + other_app=app_map(raw=True)[domain]["/"]["id"])) try: with open('/etc/ssowat/conf.json.persistent') as json_conf: From e8196cbc9855ab460ec9cbcb6c3505e70eb46be1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 17:23:02 +0100 Subject: [PATCH 0574/1066] [doc] add comment to speak intent --- src/yunohost/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 851fcdaa6..d9ca5977f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1173,6 +1173,7 @@ def app_checkurl(auth, url, app=None): raise MoulinetteError(errno.EINVAL, m18n.n('app_location_already_used', app=a["id"], path=path)) + # can't install "/a/b/" if "/a/" exists elif path.startswith(p) or p.startswith(path): raise MoulinetteError(errno.EPERM, m18n.n('app_location_install_failed', From 0922568d64686f996aedb5d87dfdb41522757c2d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 17:34:27 +0100 Subject: [PATCH 0575/1066] [fix] was replacing the dictionnary with a string and thus breaking everything --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 042671125..318c08ce6 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -570,8 +570,8 @@ def tools_diagnosis(auth, private=False): else: diagnosis['system']['disks'] = {} for disk in disks: - if isinstance(disk, str): - diagnosis['system']['disks'] = disk + if isinstance(disks[disk], str): + diagnosis['system']['disks'][disk] = disks[disk] else: diagnosis['system']['disks'][disk] = 'Mounted on %s, %s (%s free)' % ( disks[disk]['mnt_point'], From 97fea34124ffe2fb222bf2a401ea76a72facc525 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 20:23:00 +0100 Subject: [PATCH 0576/1066] [enh] display regen-conf in private diagnosis --- src/yunohost/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 042671125..97664ba45 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -624,6 +624,8 @@ def tools_diagnosis(auth, private=False): # Domains diagnosis['private']['domains'] = domain_list(auth)['domains'] + diagnosis['private']['regen_conf'] = service_regen_conf(with_diff=True, dry_run=True) + return diagnosis From 70e08ed40f32875fc1389a39c2ee0628ffecdf1e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 8 Jan 2018 02:47:36 +0100 Subject: [PATCH 0577/1066] [fix] pkg is None, can't continue loop --- src/yunohost/utils/packages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 9242d22d1..bc822da1f 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -414,6 +414,8 @@ def get_installed_version(*pkgnames, **kwargs): if strict: raise UnknownPackage(pkgname) logger.warning(m18n.n('package_unknown', pkgname=pkgname)) + continue + try: version = pkg.installed.version except AttributeError: From e0edbeca357f7aecae159a1e7c71aef006d9462d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 8 Jan 2018 03:10:16 +0100 Subject: [PATCH 0578/1066] [enh] --version now display stable/testing/unstable information --- src/yunohost/utils/packages.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index bc822da1f..18c4f2a3d 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -422,7 +422,21 @@ def get_installed_version(*pkgnames, **kwargs): if strict: raise UninstalledPackage(pkgname) version = None - versions[pkgname] = version + + try: + # stable, testing, unstable + repo = pkg.installed.origins[0].component + except AttributeError: + if strict: + raise UninstalledPackage(pkgname) + repo = "" + + versions[pkgname] = { + "version": version, + # when we don't have component it's because it's from a local + # install or from an image (like in vagrant) + "repo": repo if repo else "local", + } if len(pkgnames) == 1 and not as_dict: return versions[pkgnames[0]] @@ -438,6 +452,9 @@ def meets_version_specifier(pkgname, specifier): # YunoHost related methods --------------------------------------------------- def ynh_packages_version(*args, **kwargs): + # from cli the received arguments are: + # (Namespace(_callbacks=deque([]), _tid='_global', _to_return={}), []) {} + # they don't seems to serve any purpose """Return the version of each YunoHost package""" return get_installed_version( 'yunohost', 'yunohost-admin', 'moulinette', 'ssowat', From d7967f6d5fde81d4537bff8d2de1f723d1fceec4 Mon Sep 17 00:00:00 2001 From: JocelynDelalande Date: Mon, 8 Jan 2018 17:59:53 +0100 Subject: [PATCH 0579/1066] Fix comment lines in DNS zone example (using ";") If copy-pasted into a registrar zone file, the provided DNS zone sample for a given domain will fail, because comments lines start with "#" However, comments character in DNS zone files is ";" not "#" https://en.wikipedia.org/wiki/Zone_file --- src/yunohost/domain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index f828b0973..727a63df3 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -188,17 +188,17 @@ def domain_dns_conf(domain, ttl=None): result = "" - result += "# Basic ipv4/ipv6 records" + result += "; Basic ipv4/ipv6 records" for record in dns_conf["basic"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) result += "\n\n" - result += "# XMPP" + result += "; XMPP" for record in dns_conf["xmpp"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) result += "\n\n" - result += "# Mail" + result += "; Mail" for record in dns_conf["mail"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) From 436c6d74bb8e960513e8589f3a2e00341519b13b Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 9 Jan 2018 12:07:47 +0100 Subject: [PATCH 0580/1066] [fix] Add ability to symlink the archives dir --- src/yunohost/backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index def7fb27b..4700c63d3 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2300,7 +2300,7 @@ def backup_delete(name): def _create_archive_dir(): """ Create the YunoHost archives directory if doesn't exist """ - if not os.path.isdir(ARCHIVES_PATH): + if not os.path.isdir(ARCHIVES_PATH) and not os.path.islink(ARCHIVES_PATH): os.mkdir(ARCHIVES_PATH, 0750) From 8d2848a4264412600731fce371532d6f7007392b Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 9 Jan 2018 13:00:40 +0100 Subject: [PATCH 0581/1066] [enh] Warn users their symlink archives directory is broken --- locales/en.json | 1 + src/yunohost/backup.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 0aa8798fd..e59b71621 100644 --- a/locales/en.json +++ b/locales/en.json @@ -101,6 +101,7 @@ "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", + "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}', may be you have a specific setup, to backup your data on an other filesystem, in this case you probably miss to remount or plug your hard dirve or usb key.", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", "backup_system_part_failed": "Unable to backup the '{part:s}' system part", diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 4700c63d3..0c957db7e 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2300,7 +2300,12 @@ def backup_delete(name): def _create_archive_dir(): """ Create the YunoHost archives directory if doesn't exist """ - if not os.path.isdir(ARCHIVES_PATH) and not os.path.islink(ARCHIVES_PATH): + if not os.path.isdir(ARCHIVES_PATH): + if os.path.lexists(ARCHIVES_PATH): + raise MoulinetteError(errno.EINVAL, + m18n.n('backup_output_symlink_dir_broken', + path=ARCHIVES_PATH)) + os.mkdir(ARCHIVES_PATH, 0750) From efb841683946270e56e85f2f1bbf1fecbd410db5 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 9 Jan 2018 13:12:46 +0100 Subject: [PATCH 0582/1066] [fix] English sentences --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index e59b71621..5f5eb957f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -101,7 +101,7 @@ "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", - "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}', may be you have a specific setup, to backup your data on an other filesystem, in this case you probably miss to remount or plug your hard dirve or usb key.", + "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably miss to remount or plug your hard dirve or usb key.", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", "backup_system_part_failed": "Unable to backup the '{part:s}' system part", From 4f679367eb8905aee52a76dcefac4a2190a9a382 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Tue, 9 Jan 2018 20:30:43 +0100 Subject: [PATCH 0583/1066] Typo --- src/yunohost/utils/packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 18c4f2a3d..7df311406 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -454,7 +454,7 @@ def meets_version_specifier(pkgname, specifier): def ynh_packages_version(*args, **kwargs): # from cli the received arguments are: # (Namespace(_callbacks=deque([]), _tid='_global', _to_return={}), []) {} - # they don't seems to serve any purpose + # they don't seem to serve any purpose """Return the version of each YunoHost package""" return get_installed_version( 'yunohost', 'yunohost-admin', 'moulinette', 'ssowat', From 20f0e9a40c67fc9b373cd4ae33a982628c816552 Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Tue, 9 Jan 2018 23:01:19 +0100 Subject: [PATCH 0584/1066] Fix conf dir clean-up --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 87178464c..7b6195e60 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -622,7 +622,7 @@ def app_upgrade(auth, app=[], url=None, file=None): json.dump(status, f) # Replace scripts and manifest and conf (if exists) - os.system('rm -rf "%s/scripts" "%s/manifest.json %/conf"' % (app_setting_path, app_setting_path, app_setting_path)) + os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) if os.path.exists(os.path.join(extracted_app_folder, "conf")): From 9c44878d9203bf174bfc7c9ae164e57d781a5b5e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 9 Jan 2018 23:21:33 +0100 Subject: [PATCH 0585/1066] [mod] add section in PR template to inform on how to test a PR --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 065c2dd90..9642e92f6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,6 +10,9 @@ ... +## How to test + +... ## Validation From 52898891742dfb868581a8af1d322e129ab6751f Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 10 Jan 2018 00:44:25 +0100 Subject: [PATCH 0586/1066] Be nice with set -eu --- data/helpers.d/filesystem | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index f8da72ad5..8f6665891 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -46,10 +46,11 @@ ynh_backup() { local SRC_PATH="$1" local DEST_PATH="${2:-}" local IS_BIG="${3:-0}" + BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0} # If backing up core only (used by ynh_backup_before_upgrade), # don't backup big data items - if [ "$IS_BIG" == "1" ] && [ -n "$BACKUP_CORE_ONLY" ] ; then + if [ "$IS_BIG" == "1" ] && [ "$BACKUP_CORE_ONLY" == "1" ] ; then echo "$SRC_PATH will not be saved, because backup_core_only is set." >&2 return 0 fi From 28450ce8c30249b28132257a5396875104ac5a1d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 10 Jan 2018 16:30:20 +0100 Subject: [PATCH 0587/1066] [enh] new command 'app change-label' --- data/actionsmap/yunohost.yml | 13 +++++++++++++ src/yunohost/app.py | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 966de21df..101ac45e1 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -647,6 +647,19 @@ app: authenticate: all authenticator: ldap-anonymous + ### app_change_label() + change-label: + action_help: Change an application label + api: PUT /apps//label + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + app: + help: the application id + new_label: + help: the new application label + ### app_addaccess() TODO: Write help addaccess: action_help: Grant access right to users (everyone by default) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 15855753c..9ccc0886d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1320,6 +1320,17 @@ def app_ssowatconf(auth): logger.success(m18n.n('ssowat_conf_generated')) +def app_change_label(auth, app, new_label): + installed = _is_installed(app) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app)) + + app_setting(app, "label", value=new_label) + + app_ssowatconf(auth) + + def _get_app_settings(app_id): """ Get settings of an installed app From 2b2676a9c1d6f487cbbda00a17a6e3e8ff4a40cc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 23:03:22 +0100 Subject: [PATCH 0588/1066] [enh] add nginx -t output to diagnosis --- src/yunohost/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 042671125..8262e6682 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -42,6 +42,7 @@ import apt.progress from moulinette import msettings, msignals, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger +from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain @@ -589,6 +590,9 @@ def tools_diagnosis(auth, private=False): 'swap': '%s (%s free)' % (system['memory']['swap']['total'], system['memory']['swap']['free']), } + # nginx -t + diagnosis['nginx'] = check_output("nginx -t").strip().split("\n") + # Services status services = service_status() diagnosis['services'] = {} From 07849175736bbe0372f6e0867da731184ea4e7af Mon Sep 17 00:00:00 2001 From: taziden Date: Mon, 2 Oct 2017 13:44:41 +0200 Subject: [PATCH 0589/1066] Create milter_headers.conf This file define a header : X-Spam which will be added to mail considered as spam by rspamd. This configuration is defined here : https://rspamd.com/doc/modules/milter_headers.html and as of the time I'm creating this PR, this documentation page is not up-to-date because X-Spam was supposed to be added when extended_spam_headers is set to True and it's not the case anymore since 1.6.2. This header is being used by rspamd.sieve which we push into dovecot in order to put spammy e-mails in Junk folder automatically. --- data/templates/rspamd/milter_headers.conf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 data/templates/rspamd/milter_headers.conf diff --git a/data/templates/rspamd/milter_headers.conf b/data/templates/rspamd/milter_headers.conf new file mode 100644 index 000000000..d57aa6958 --- /dev/null +++ b/data/templates/rspamd/milter_headers.conf @@ -0,0 +1,9 @@ +use = ["spam-header"]; + +routines { + spam-header { + header = "X-Spam"; + value = "Yes"; + remove = 1; + } +} From 3ed6a2a7eff79c813ffbecec961e7938905767f5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 11 Jan 2018 15:15:22 +0100 Subject: [PATCH 0590/1066] Revert "Create milter_headers.conf" This reverts commit 0122c26492460f618beed86d34c5beee1e8cf25c. --- data/templates/rspamd/milter_headers.conf | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 data/templates/rspamd/milter_headers.conf diff --git a/data/templates/rspamd/milter_headers.conf b/data/templates/rspamd/milter_headers.conf deleted file mode 100644 index d57aa6958..000000000 --- a/data/templates/rspamd/milter_headers.conf +++ /dev/null @@ -1,9 +0,0 @@ -use = ["spam-header"]; - -routines { - spam-header { - header = "X-Spam"; - value = "Yes"; - remove = 1; - } -} From 935f6091368aecbfcf04eb05aae07254604ed691 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 7 Jan 2018 21:46:38 +0100 Subject: [PATCH 0591/1066] [enh] display backports .deb in diagnosis --- src/yunohost/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 70a757d03..1c7b8b87a 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -562,6 +562,8 @@ def tools_diagnosis(auth, private=False): # Packages version diagnosis['packages'] = ynh_packages_version() + diagnosis["backports"] = check_output("dpkg -l |awk '/^ii/ && $3 ~ /bpo[6-8]/ {print $2}'").split() + # Server basic monitoring diagnosis['system'] = OrderedDict() try: From e59e2da0b31bb2af8727ab1e87021650bf2818d4 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Fri, 12 Jan 2018 09:18:31 +0100 Subject: [PATCH 0592/1066] Harmonize help strings --- data/actionsmap/yunohost.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 101ac45e1..076e5599c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -649,16 +649,16 @@ app: ### app_change_label() change-label: - action_help: Change an application label + action_help: Change app label api: PUT /apps//label configuration: authenticate: all authenticator: ldap-anonymous arguments: app: - help: the application id + help: App ID new_label: - help: the new application label + help: New app label ### app_addaccess() TODO: Write help addaccess: From b60d8ca822d08c8e3fdf8a17505ff3e285b28164 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 5 Jan 2018 12:54:11 +0100 Subject: [PATCH 0593/1066] [enh] add new api entry point to check for meltdown vulnerability --- data/actionsmap/yunohost.yml | 5 +++++ src/yunohost/tools.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 966de21df..9e8022964 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1460,6 +1460,11 @@ tools: full: --force action: store_true + ### tools_reboot() + meltdown-spectre-check: + action_help: Check if the server is vulnerable to meltdown/spectre + api: GET /meltdown-spectre-check + subcategories: migrations: diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index cf52ad38f..13f3ea5fd 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -836,6 +836,14 @@ def tools_migrations_state(): return read_json(MIGRATIONS_STATE_PATH) +def tools_meltdown_spectre_check(): + """ + Check if the installation is vulnerable to meltdown/spectre. + """ + # source https://askubuntu.com/questions/992137/how-to-check-that-kpti-is-enabled-on-my-ubuntu + return {"safe": "cpu_insecure" in open("/proc/cpuinfo")} + + def tools_shell(auth, command=None): """ Launch an (i)python shell in the YunoHost context. From a934b3fd19403859cf6a46713b82945524f9ac28 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 5 Jan 2018 16:30:33 +0100 Subject: [PATCH 0594/1066] [mod] move spectre-meltdown check to diagnosis function --- data/actionsmap/yunohost.yml | 5 ----- src/yunohost/tools.py | 17 +++++++++-------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 9e8022964..966de21df 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1460,11 +1460,6 @@ tools: full: --force action: store_true - ### tools_reboot() - meltdown-spectre-check: - action_help: Check if the server is vulnerable to meltdown/spectre - api: GET /meltdown-spectre-check - subcategories: migrations: diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 13f3ea5fd..46bcd06d5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -42,8 +42,12 @@ import apt.progress from moulinette import msettings, msignals, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger +<<<<<<< b60d8ca822d08c8e3fdf8a17505ff3e285b28164 from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json +======= +from moulinette.utils.filesystem import read_json, write_to_json, read_file +>>>>>>> [mod] move spectre-meltdown check to diagnosis function from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import _dyndns_available, _dyndns_provides @@ -632,6 +636,11 @@ def tools_diagnosis(auth, private=False): diagnosis['private']['regen_conf'] = service_regen_conf(with_diff=True, dry_run=True) + diagnosis['security'] = { + # source https://askubuntu.com/questions/992137/how-to-check-that-kpti-is-enabled-on-my-ubuntu + "spectre-meltdown": "cpu_insecure" not in read_file("/proc/cpuinfo") + } + return diagnosis @@ -836,14 +845,6 @@ def tools_migrations_state(): return read_json(MIGRATIONS_STATE_PATH) -def tools_meltdown_spectre_check(): - """ - Check if the installation is vulnerable to meltdown/spectre. - """ - # source https://askubuntu.com/questions/992137/how-to-check-that-kpti-is-enabled-on-my-ubuntu - return {"safe": "cpu_insecure" in open("/proc/cpuinfo")} - - def tools_shell(auth, command=None): """ Launch an (i)python shell in the YunoHost context. From f46351c7c50297ce4e5b39cbc61a1c5cc466ddb2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 13 Jan 2018 05:19:39 +0100 Subject: [PATCH 0595/1066] [enh] uses speed47 script to check for meltdown vulnerability --- data/other/spectre-meltdown-checker.sh | 980 +++++++++++++++++++++++++ src/yunohost/tools.py | 25 +- 2 files changed, 999 insertions(+), 6 deletions(-) create mode 100644 data/other/spectre-meltdown-checker.sh diff --git a/data/other/spectre-meltdown-checker.sh b/data/other/spectre-meltdown-checker.sh new file mode 100644 index 000000000..5658cf0a8 --- /dev/null +++ b/data/other/spectre-meltdown-checker.sh @@ -0,0 +1,980 @@ +#! /bin/sh + +# Message from YunoHost team: +# this is this version https://github.com/speed47/spectre-meltdown-checker/blob/dce917bfbb4fa5e135b7ed4c84881086766be802/spectre-meltdown-checker.sh +# that is included here +# You can find the licence (GPLv2))and author name in this script + +# Spectre & Meltdown checker +# +# Check for the latest version at: +# https://github.com/speed47/spectre-meltdown-checker +# git clone https://github.com/speed47/spectre-meltdown-checker.git +# or wget https://raw.githubusercontent.com/speed47/spectre-meltdown-checker/master/spectre-meltdown-checker.sh +# +# Stephane Lesimple + +VERSION=0.28 + +# Script configuration +show_usage() +{ + cat <] [--config ] [--map ] + + Modes: + Two modes are available. + + First mode is the "live" mode (default), it does its best to find information about the currently running kernel. + To run under this mode, just start the script without any option (you can also use --live explicitly) + + Second mode is the "offline" mode, where you can inspect a non-running kernel. + You'll need to specify the location of the vmlinux file, and if possible, the corresponding config and System.map files: + + --kernel vmlinux_file Specify a (possibly compressed) vmlinux file + --config kernel_config Specify a kernel config file + --map kernel_map_file Specify a kernel System.map file + + Options: + --no-color Don't use color codes + --verbose, -v Increase verbosity level + --no-sysfs Don't use the /sys interface even if present + --batch text Produce machine readable output, this is the default if --batch is specified alone + --batch json Produce JSON output formatted for Puppet, Ansible, Chef... + --batch nrpe Produce machine readable output formatted for NRPE + --variant [1,2,3] Specify which variant you'd like to check, by default all variants are checked + Can be specified multiple times (e.g. --variant 2 --variant 3) + + + IMPORTANT: + A false sense of security is worse than no security at all. + Please use the --disclaimer option to understand exactly what this script does. + +EOF +} + +show_disclaimer() +{ + cat <&2 +} + +_info() +{ + _echo 1 "$@" +} + +_info_nol() +{ + _echo_nol 1 "$@" +} + +_verbose() +{ + _echo 2 "$@" +} + +_debug() +{ + _echo 3 "\033[34m(debug) $@\033[0m" +} + +is_cpu_vulnerable() +{ + # param: 1, 2 or 3 (variant) + # returns 1 if vulnerable, 0 if not vulnerable, 255 on error + # by default, everything is vulnerable, we work in a "whitelist" logic here. + # usage: is_cpu_vulnerable 2 && do something if vulnerable + variant1=0 + variant2=0 + variant3=0 + if grep -q AMD /proc/cpuinfo; then + variant1=0 + variant2=1 + variant3=1 + elif grep -qi 'CPU implementer\s*:\s*0x41' /proc/cpuinfo; then + # ARM + # reference: https://developer.arm.com/support/security-update + cpupart=$(awk '/CPU part/ {print $4;exit}' /proc/cpuinfo) + cpuarch=$(awk '/CPU architecture/ {print $3;exit}' /proc/cpuinfo) + if [ -n "$cpupart" -a -n "$cpuarch" ]; then + # Cortex-R7 and Cortex-R8 are real-time and only used in medical devices or such + # I can't find their CPU part number, but it's probably not that useful anyway + # model R7 R8 A9 A15 A17 A57 A72 A73 A75 + # part ? ? 0xc09 0xc0f 0xc0e 0xd07 0xd08 0xd09 0xd0a + # arch 7? 7? 7 7 7 8 8 8 8 + if [ "$cpuarch" = 7 ] && echo "$cpupart" | grep -Eq '^0x(c09|c0f|c0e)$'; then + # armv7 vulnerable chips + variant1=0 + variant2=0 + elif [ "$cpuarch" = 8 ] && echo "$cpupart" | grep -Eq '^0x(d07|d08|d09|d0a)$'; then + # armv8 vulnerable chips + variant1=0 + variant2=0 + else + variant1=1 + variant2=1 + fi + # for variant3, only A75 is vulnerable + if [ "$cpuarch" = 8 -a "$cpupart" = 0xd0a ]; then + variant3=0 + else + variant3=1 + fi + fi + fi + [ "$1" = 1 ] && return $variant1 + [ "$1" = 2 ] && return $variant2 + [ "$1" = 3 ] && return $variant3 + return 255 +} + +show_header() +{ + _info "\033[1;34mSpectre and Meltdown mitigation detection tool v$VERSION\033[0m" + _info +} + +parse_opt_file() +{ + # parse_opt_file option_name option_value + option_name="$1" + option_value="$2" + if [ -z "$option_value" ]; then + show_header + show_usage + echo "$0: error: --$option_name expects one parameter (a file)" >&2 + exit 1 + elif [ ! -e "$option_value" ]; then + show_header + echo "$0: error: couldn't find file $option_value" >&2 + exit 1 + elif [ ! -f "$option_value" ]; then + show_header + echo "$0: error: $option_value is not a file" >&2 + exit 1 + elif [ ! -r "$option_value" ]; then + show_header + echo "$0: error: couldn't read $option_value (are you root?)" >&2 + exit 1 + fi + echo "$option_value" + exit 0 +} + +while [ -n "$1" ]; do + if [ "$1" = "--kernel" ]; then + opt_kernel=$(parse_opt_file kernel "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--config" ]; then + opt_config=$(parse_opt_file config "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--map" ]; then + opt_map=$(parse_opt_file map "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--live" ]; then + opt_live_explicit=1 + shift + elif [ "$1" = "--no-color" ]; then + opt_no_color=1 + shift + elif [ "$1" = "--no-sysfs" ]; then + opt_no_sysfs=1 + shift + elif [ "$1" = "--batch" ]; then + opt_batch=1 + opt_verbose=0 + shift + case "$1" in + text|nrpe|json) opt_batch_format="$1"; shift;; + --*) ;; # allow subsequent flags + '') ;; # allow nothing at all + *) + echo "$0: error: unknown batch format '$1'" + echo "$0: error: --batch expects a format from: text, nrpe, json" + exit 1 >&2 + ;; + esac + elif [ "$1" = "-v" -o "$1" = "--verbose" ]; then + opt_verbose=$(expr $opt_verbose + 1) + shift + elif [ "$1" = "--variant" ]; then + if [ -z "$2" ]; then + echo "$0: error: option --variant expects a parameter (1, 2 or 3)" >&2 + exit 1 + fi + case "$2" in + 1) opt_variant1=1; opt_allvariants=0;; + 2) opt_variant2=1; opt_allvariants=0;; + 3) opt_variant3=1; opt_allvariants=0;; + *) + echo "$0: error: invalid parameter '$2' for --variant, expected either 1, 2 or 3" >&2; + exit 1;; + esac + shift 2 + elif [ "$1" = "-h" -o "$1" = "--help" ]; then + show_header + show_usage + exit 0 + elif [ "$1" = "--version" ]; then + opt_no_color=1 + show_header + exit 1 + elif [ "$1" = "--disclaimer" ]; then + show_header + show_disclaimer + exit 0 + else + show_header + show_usage + echo "$0: error: unknown option '$1'" + exit 1 + fi +done + +show_header + +# print status function +pstatus() +{ + if [ "$opt_no_color" = 1 ]; then + _info_nol "$2" + else + case "$1" in + red) col="\033[101m\033[30m";; + green) col="\033[102m\033[30m";; + yellow) col="\033[103m\033[30m";; + blue) col="\033[104m\033[30m";; + *) col="";; + esac + _info_nol "$col $2 \033[0m" + fi + [ -n "$3" ] && _info_nol " ($3)" + _info +} + +# Print the final status of a vulnerability (incl. batch mode) +# Arguments are: CVE UNK/OK/VULN description +pvulnstatus() +{ + if [ "$opt_batch" = 1 ]; then + case "$opt_batch_format" in + text) _echo 0 "$1: $2 ($3)";; + nrpe) + case "$2" in + UKN) nrpe_unknown="1";; + VULN) nrpe_critical="1"; nrpe_vuln="$nrpe_vuln $1";; + esac + ;; + json) + case "$1" in + CVE-2017-5753) aka="SPECTRE VARIANT 1";; + CVE-2017-5715) aka="SPECTRE VARIANT 2";; + CVE-2017-5754) aka="MELTDOWN";; + esac + case "$2" in + UKN) is_vuln="unknown";; + VULN) is_vuln="true";; + OK) is_vuln="false";; + esac + json_output="${json_output:-[}{\"NAME\":\""$aka"\",\"CVE\":\""$1"\",\"VULNERABLE\":$is_vuln,\"INFOS\":\""$3"\"}," + ;; + esac + fi + + _info_nol "> \033[46m\033[30mSTATUS:\033[0m " + vulnstatus="$2" + shift 2 + case "$vulnstatus" in + UNK) pstatus yellow UNKNOWN "$@";; + VULN) pstatus red 'VULNERABLE' "$@";; + OK) pstatus green 'NOT VULNERABLE' "$@";; + esac +} + + +# The 3 below functions are taken from the extract-linux script, available here: +# https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux +# The functions have been modified for better integration to this script +# The original header of the file has been retained below + +# ---------------------------------------------------------------------- +# extract-vmlinux - Extract uncompressed vmlinux from a kernel image +# +# Inspired from extract-ikconfig +# (c) 2009,2010 Dick Streefland +# +# (c) 2011 Corentin Chary +# +# Licensed under the GNU General Public License, version 2 (GPLv2). +# ---------------------------------------------------------------------- + +vmlinux='' +vmlinux_err='' +check_vmlinux() +{ + readelf -h "$1" > /dev/null 2>&1 || return 1 + return 0 +} + +try_decompress() +{ + # The obscure use of the "tr" filter is to work around older versions of + # "grep" that report the byte offset of the line instead of the pattern. + + # Try to find the header ($1) and decompress from here + for pos in `tr "$1\n$2" "\n$2=" < "$6" | grep -abo "^$2"` + do + _debug "try_decompress: magic for $3 found at offset $pos" + if ! which "$3" >/dev/null 2>&1; then + vmlinux_err="missing '$3' tool, please install it, usually it's in the '$5' package" + return 0 + fi + pos=${pos%%:*} + tail -c+$pos "$6" 2>/dev/null | $3 $4 > $vmlinuxtmp 2>/dev/null + if check_vmlinux "$vmlinuxtmp"; then + vmlinux="$vmlinuxtmp" + _debug "try_decompress: decompressed with $3 successfully!" + return 0 + else + _debug "try_decompress: decompression with $3 did not work" + fi + done + return 1 +} + +extract_vmlinux() +{ + [ -n "$1" ] || return 1 + # Prepare temp files: + vmlinuxtmp="$(mktemp /tmp/vmlinux-XXXXXX)" + trap "rm -f $vmlinuxtmp" EXIT + + # Initial attempt for uncompressed images or objects: + if check_vmlinux "$1"; then + cat "$1" > "$vmlinuxtmp" + vmlinux=$vmlinuxtmp + return 0 + fi + + # That didn't work, so retry after decompression. + try_decompress '\037\213\010' xy gunzip '' gunzip "$1" && return 0 + try_decompress '\3757zXZ\000' abcde unxz '' xz-utils "$1" && return 0 + try_decompress 'BZh' xy bunzip2 '' bzip2 "$1" && return 0 + try_decompress '\135\0\0\0' xxx unlzma '' xz-utils "$1" && return 0 + try_decompress '\211\114\132' xy 'lzop' '-d' lzop "$1" && return 0 + try_decompress '\002\041\114\030' xyy 'lz4' '-d -l' liblz4-tool "$1" && return 0 + return 1 +} + +# end of extract-vmlinux functions + +# check for mode selection inconsistency +if [ "$opt_live_explicit" = 1 ]; then + if [ -n "$opt_kernel" -o -n "$opt_config" -o -n "$opt_map" ]; then + show_usage + echo "$0: error: incompatible modes specified, use either --live or --kernel/--config/--map" + exit 1 + fi +fi + +# root check (only for live mode, for offline mode, we already checked if we could read the files) + +if [ "$opt_live" = 1 ]; then + if [ "$(id -u)" -ne 0 ]; then + _warn "Note that you should launch this script with root privileges to get accurate information." + _warn "We'll proceed but you might see permission denied errors." + _warn "To run it as root, you can try the following command: sudo $0" + _warn + fi + _info "Checking for vulnerabilities against running kernel \033[35m"$(uname -s) $(uname -r) $(uname -v) $(uname -m)"\033[0m" + _info "CPU is\033[35m"$(grep '^model name' /proc/cpuinfo | cut -d: -f2 | head -1)"\033[0m" + + # try to find the image of the current running kernel + # first, look for the BOOT_IMAGE hint in the kernel cmdline + if [ -r /proc/cmdline ] && grep -q 'BOOT_IMAGE=' /proc/cmdline; then + opt_kernel=$(grep -Eo 'BOOT_IMAGE=[^ ]+' /proc/cmdline | cut -d= -f2) + _debug "found opt_kernel=$opt_kernel in /proc/cmdline" + # if we have a dedicated /boot partition, our bootloader might have just called it / + # so try to prepend /boot and see if we find anything + [ -e "/boot/$opt_kernel" ] && opt_kernel="/boot/$opt_kernel" + _debug "opt_kernel is now $opt_kernel" + # else, the full path is already there (most probably /boot/something) + fi + # if we didn't find a kernel, default to guessing + if [ ! -e "$opt_kernel" ]; then + [ -e /boot/vmlinuz-linux ] && opt_kernel=/boot/vmlinuz-linux + [ -e /boot/vmlinuz-linux-libre ] && opt_kernel=/boot/vmlinuz-linux-libre + [ -e /boot/vmlinuz-$(uname -r) ] && opt_kernel=/boot/vmlinuz-$(uname -r) + [ -e /boot/kernel-$( uname -r) ] && opt_kernel=/boot/kernel-$( uname -r) + [ -e /boot/bzImage-$(uname -r) ] && opt_kernel=/boot/bzImage-$(uname -r) + [ -e /boot/kernel-genkernel-$(uname -m)-$(uname -r) ] && opt_kernel=/boot/kernel-genkernel-$(uname -m)-$(uname -r) + fi + + # system.map + if [ -e /proc/kallsyms ] ; then + opt_map="/proc/kallsyms" + elif [ -e /boot/System.map-$(uname -r) ] ; then + opt_map=/boot/System.map-$(uname -r) + fi + + # config + if [ -e /proc/config.gz ] ; then + dumped_config="$(mktemp /tmp/config-XXXXXX)" + gunzip -c /proc/config.gz > $dumped_config + # dumped_config will be deleted at the end of the script + opt_config=$dumped_config + elif [ -e /boot/config-$(uname -r) ]; then + opt_config=/boot/config-$(uname -r) + fi +else + _info "Checking for vulnerabilities against specified kernel" +fi +if [ -n "$opt_kernel" ]; then + _verbose "Will use vmlinux image \033[35m$opt_kernel\033[0m" +else + _verbose "Will use no vmlinux image (accuracy might be reduced)" +fi +if [ -n "$dumped_config" ]; then + _verbose "Will use kconfig \033[35m/proc/config.gz\033[0m" +elif [ -n "$opt_config" ]; then + _verbose "Will use kconfig \033[35m$opt_config\033[0m" +else + _verbose "Will use no kconfig (accuracy might be reduced)" +fi +if [ -n "$opt_map" ]; then + _verbose "Will use System.map file \033[35m$opt_map\033[0m" +else + _verbose "Will use no System.map file (accuracy might be reduced)" +fi + +if [ -e "$opt_kernel" ]; then + if ! which readelf >/dev/null 2>&1; then + vmlinux_err="missing 'readelf' tool, please install it, usually it's in the 'binutils' package" + else + extract_vmlinux "$opt_kernel" + fi +else + vmlinux_err="couldn't find your kernel image in /boot, if you used netboot, this is normal" +fi +if [ -z "$vmlinux" -o ! -r "$vmlinux" ]; then + [ -z "$vmlinux_err" ] && vmlinux_err="couldn't extract your kernel from $opt_kernel" +fi + +_info + +# end of header stuff + +# now we define some util functions and the check_*() funcs, as +# the user can choose to execute only some of those + +mount_debugfs() +{ + if [ ! -e /sys/kernel/debug/sched_features ]; then + # try to mount the debugfs hierarchy ourselves and remember it to umount afterwards + mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null && mounted_debugfs=1 + fi +} + +umount_debugfs() +{ + if [ "$mounted_debugfs" = 1 ]; then + # umount debugfs if we did mount it ourselves + umount /sys/kernel/debug + fi +} + +sys_interface_check() +{ + [ "$opt_live" = 1 -a "$opt_no_sysfs" = 0 -a -r "$1" ] || return 1 + _info_nol "* Checking whether we're safe according to the /sys interface: " + if grep -qi '^not affected' "$1"; then + # Not affected + status=OK + pstatus green YES "kernel confirms that your CPU is unaffected" + elif grep -qi '^mitigation' "$1"; then + # Mitigation: PTI + status=OK + pstatus green YES "kernel confirms that the mitigation is active" + elif grep -qi '^vulnerable' "$1"; then + # Vulnerable + status=VULN + pstatus red NO "kernel confirms your system is vulnerable" + else + status=UNK + pstatus yellow UNKNOWN "unknown value reported by kernel" + fi + msg=$(cat "$1") + _debug "sys_interface_check: $1=$msg" + return 0 +} + +################### +# SPECTRE VARIANT 1 +check_variant1() +{ + _info "\033[1;34mCVE-2017-5753 [bounds check bypass] aka 'Spectre Variant 1'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v1"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + # no /sys interface (or offline mode), fallback to our own ways + _info_nol "* Checking count of LFENCE opcodes in kernel: " + if [ -n "$vmlinux_err" ]; then + msg="couldn't check ($vmlinux_err)" + status=UNK + pstatus yellow UNKNOWN + else + if ! which objdump >/dev/null 2>&1; then + msg="missing 'objdump' tool, please install it, usually it's in the binutils package" + status=UNK + pstatus yellow UNKNOWN + else + # here we disassemble the kernel and count the number of occurences of the LFENCE opcode + # in non-patched kernels, this has been empirically determined as being around 40-50 + # in patched kernels, this is more around 70-80, sometimes way higher (100+) + # v0.13: 68 found in a 3.10.23-xxxx-std-ipv6-64 (with lots of modules compiled-in directly), which doesn't have the LFENCE patches, + # so let's push the threshold to 70. + nb_lfence=$(objdump -d "$vmlinux" | grep -wc lfence) + if [ "$nb_lfence" -lt 70 ]; then + msg="only $nb_lfence opcodes found, should be >= 70, heuristic to be improved when official patches become available" + status=VULN + pstatus red NO + else + msg="$nb_lfence opcodes found, which is >= 70, heuristic to be improved when official patches become available" + status=OK + pstatus green YES + fi + fi + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 1; then + # override status & msg in case CPU is not vulnerable after all + msg="your CPU vendor reported your CPU model as not vulnerable" + status=OK + fi + + # report status + pvulnstatus CVE-2017-5753 "$status" "$msg" +} + +################### +# SPECTRE VARIANT 2 +check_variant2() +{ + _info "\033[1;34mCVE-2017-5715 [branch target injection] aka 'Spectre Variant 2'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + _info "* Mitigation 1" + _info_nol "* Hardware (CPU microcode) support for mitigation: " + if [ ! -e /dev/cpu/0/msr ]; then + # try to load the module ourselves (and remember it so we can rmmod it afterwards) + modprobe msr 2>/dev/null && insmod_msr=1 + _debug "attempted to load module msr, ret=$insmod_msr" + fi + if [ ! -e /dev/cpu/0/msr ]; then + pstatus yellow UNKNOWN "couldn't read /dev/cpu/0/msr, is msr support enabled in your kernel?" + else + # the new MSR 'SPEC_CTRL' is at offset 0x48 + # here we use dd, it's the same as using 'rdmsr 0x48' but without needing the rdmsr tool + # if we get a read error, the MSR is not there + dd if=/dev/cpu/0/msr of=/dev/null bs=8 count=1 skip=9 2>/dev/null + if [ $? -eq 0 ]; then + pstatus green YES + else + pstatus red NO + fi + fi + + if [ "$insmod_msr" = 1 ]; then + # if we used modprobe ourselves, rmmod the module + rmmod msr 2>/dev/null + _debug "attempted to unload module msr, ret=$?" + fi + + _info_nol "* Kernel support for IBRS: " + if [ "$opt_live" = 1 ]; then + mount_debugfs + for ibrs_file in \ + /sys/kernel/debug/ibrs_enabled \ + /sys/kernel/debug/x86/ibrs_enabled \ + /proc/sys/kernel/ibrs_enabled; do + if [ -e "$ibrs_file" ]; then + # if the file is there, we have IBRS compiled-in + # /sys/kernel/debug/ibrs_enabled: vanilla + # /sys/kernel/debug/x86/ibrs_enabled: RedHat (see https://access.redhat.com/articles/3311301) + # /proc/sys/kernel/ibrs_enabled: OpenSUSE tumbleweed + pstatus green YES + ibrs_supported=1 + ibrs_enabled=$(cat "$ibrs_file" 2>/dev/null) + _debug "ibrs: found $ibrs_file=$ibrs_enabled" + break + else + _debug "ibrs: file $ibrs_file doesn't exist" + fi + done + fi + if [ "$ibrs_supported" != 1 -a -n "$opt_map" ]; then + if grep -q spec_ctrl "$opt_map"; then + pstatus green YES + ibrs_supported=1 + _debug "ibrs: found '*spec_ctrl*' symbol in $opt_map" + fi + fi + if [ "$ibrs_supported" != 1 ]; then + pstatus red NO + fi + + _info_nol "* IBRS enabled for Kernel space: " + if [ "$opt_live" = 1 ]; then + # 0 means disabled + # 1 is enabled only for kernel space + # 2 is enabled for kernel and user space + case "$ibrs_enabled" in + "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; + 0) pstatus red NO;; + 1 | 2) pstatus green YES;; + *) pstatus yellow UNKNOWN;; + esac + else + pstatus blue N/A "not testable in offline mode" + fi + + _info_nol "* IBRS enabled for User space: " + if [ "$opt_live" = 1 ]; then + case "$ibrs_enabled" in + "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; + 0 | 1) pstatus red NO;; + 2) pstatus green YES;; + *) pstatus yellow UNKNOWN;; + esac + else + pstatus blue N/A "not testable in offline mode" + fi + + _info "* Mitigation 2" + _info_nol "* Kernel compiled with retpoline option: " + # We check the RETPOLINE kernel options + if [ -r "$opt_config" ]; then + if grep -q '^CONFIG_RETPOLINE=y' "$opt_config"; then + pstatus green YES + retpoline=1 + _debug "retpoline: found "$(grep '^CONFIG_RETPOLINE' "$opt_config")" in $opt_config" + else + pstatus red NO + fi + else + pstatus yellow UNKNOWN "couldn't read your kernel configuration" + fi + + _info_nol "* Kernel compiled with a retpoline-aware compiler: " + # Now check if the compiler used to compile the kernel knows how to insert retpolines in generated asm + # For gcc, this is -mindirect-branch=thunk-extern (detected by the kernel makefiles) + # See gcc commit https://github.com/hjl-tools/gcc/commit/23b517d4a67c02d3ef80b6109218f2aadad7bd79 + # In latest retpoline LKML patches, the noretpoline_setup symbol exists only if CONFIG_RETPOLINE is set + # *AND* if the compiler is retpoline-compliant, so look for that symbol + if [ -n "$opt_map" ]; then + # look for the symbol + if grep -qw noretpoline_setup "$opt_map"; then + retpoline_compiler=1 + pstatus green YES "noretpoline_setup symbol found in System.map" + else + pstatus red NO + fi + elif [ -n "$vmlinux" ]; then + # look for the symbol + if which nm >/dev/null 2>&1; then + # the proper way: use nm and look for the symbol + if nm "$vmlinux" 2>/dev/null | grep -qw 'noretpoline_setup'; then + retpoline_compiler=1 + pstatus green YES "noretpoline_setup found in vmlinux symbols" + else + pstatus red NO + fi + elif grep -q noretpoline_setup "$vmlinux"; then + # if we don't have nm, nevermind, the symbol name is long enough to not have + # any false positive using good old grep directly on the binary + retpoline_compiler=1 + pstatus green YES "noretpoline_setup found in vmlinux" + else + pstatus red NO + fi + else + pstatus yellow UNKNOWN "couldn't find your kernel image or System.map" + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 2; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus CVE-2017-5715 OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ -z "$msg" ]; then + # if msg is empty, sysfs check didn't fill it, rely on our own test + if [ "$retpoline" = 1 -a "$retpoline_compiler" = 1 ]; then + pvulnstatus CVE-2017-5715 OK "retpoline mitigate the vulnerability" + elif [ "$opt_live" = 1 ]; then + if [ "$ibrs_enabled" = 1 -o "$ibrs_enabled" = 2 ]; then + pvulnstatus CVE-2017-5715 OK "IBRS mitigates the vulnerability" + else + pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" + fi + else + if [ "$ibrs_supported" = 1 ]; then + pvulnstatus CVE-2017-5715 OK "offline mode: IBRS will mitigate the vulnerability if enabled at runtime" + else + pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" + fi + fi + else + pvulnstatus CVE-2017-5715 "$status" "$msg" + fi +} + +######################## +# MELTDOWN aka VARIANT 3 +check_variant3() +{ + _info "\033[1;34mCVE-2017-5754 [rogue data cache load] aka 'Meltdown' aka 'Variant 3'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/meltdown"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + _info_nol "* Kernel supports Page Table Isolation (PTI): " + kpti_support=0 + kpti_can_tell=0 + if [ -n "$opt_config" ]; then + kpti_can_tell=1 + if grep -Eq '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config"; then + _debug "kpti_support: found option "$(grep -E '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config")" in $opt_config" + kpti_support=1 + fi + fi + if [ "$kpti_support" = 0 -a -n "$opt_map" ]; then + # it's not an elif: some backports don't have the PTI config but still include the patch + # so we try to find an exported symbol that is part of the PTI patch in System.map + kpti_can_tell=1 + if grep -qw kpti_force_enabled "$opt_map"; then + _debug "kpti_support: found kpti_force_enabled in $opt_map" + kpti_support=1 + fi + fi + if [ "$kpti_support" = 0 -a -n "$vmlinux" ]; then + # same as above but in case we don't have System.map and only vmlinux, look for the + # nopti option that is part of the patch (kernel command line option) + kpti_can_tell=1 + if ! which strings >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing 'strings' tool, please install it, usually it's in the binutils package" + else + if strings "$vmlinux" | grep -qw nopti; then + _debug "kpti_support: found nopti string in $vmlinux" + kpti_support=1 + fi + fi + fi + + if [ "$kpti_support" = 1 ]; then + pstatus green YES + elif [ "$kpti_can_tell" = 1 ]; then + pstatus red NO + else + pstatus yellow UNKNOWN "couldn't read your kernel configuration nor System.map file" + fi + + mount_debugfs + _info_nol "* PTI enabled and active: " + if [ "$opt_live" = 1 ]; then + dmesg_grep="Kernel/User page tables isolation: enabled" + dmesg_grep="$dmesg_grep|Kernel page table isolation enabled" + dmesg_grep="$dmesg_grep|x86/pti: Unmapping kernel while in userspace" + if grep ^flags /proc/cpuinfo | grep -qw pti; then + # vanilla PTI patch sets the 'pti' flag in cpuinfo + _debug "kpti_enabled: found 'pti' flag in /proc/cpuinfo" + kpti_enabled=1 + elif grep ^flags /proc/cpuinfo | grep -qw kaiser; then + # kernel line 4.9 sets the 'kaiser' flag in cpuinfo + _debug "kpti_enabled: found 'kaiser' flag in /proc/cpuinfo" + kpti_enabled=1 + elif [ -e /sys/kernel/debug/x86/pti_enabled ]; then + # RedHat Backport creates a dedicated file, see https://access.redhat.com/articles/3311301 + kpti_enabled=$(cat /sys/kernel/debug/x86/pti_enabled 2>/dev/null) + _debug "kpti_enabled: file /sys/kernel/debug/x86/pti_enabled exists and says: $kpti_enabled" + elif dmesg | grep -Eq "$dmesg_grep"; then + # if we can't find the flag, grep dmesg output + _debug "kpti_enabled: found hint in dmesg: "$(dmesg | grep -E "$dmesg_grep") + kpti_enabled=1 + elif [ -r /var/log/dmesg ] && grep -Eq "$dmesg_grep" /var/log/dmesg; then + # if we can't find the flag in dmesg output, grep in /var/log/dmesg when readable + _debug "kpti_enabled: found hint in /var/log/dmesg: "$(grep -E "$dmesg_grep" /var/log/dmesg) + kpti_enabled=1 + else + _debug "kpti_enabled: couldn't find any hint that PTI is enabled" + kpti_enabled=0 + fi + if [ "$kpti_enabled" = 1 ]; then + pstatus green YES + else + pstatus red NO + fi + else + pstatus blue N/A "can't verify if PTI is enabled in offline mode" + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + cve='CVE-2017-5754' + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 3; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ -z "$msg" ]; then + # if msg is empty, sysfs check didn't fill it, rely on our own test + if [ "$opt_live" = 1 ]; then + if [ "$kpti_enabled" = 1 ]; then + pvulnstatus $cve OK "PTI mitigates the vulnerability" + else + pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + fi + else + if [ "$kpti_support" = 1 ]; then + pvulnstatus $cve OK "offline mode: PTI will mitigate the vulnerability if enabled at runtime" + else + pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + fi + fi + else + pvulnstatus $cve "$status" "$msg" + fi +} + +# now run the checks the user asked for +if [ "$opt_variant1" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant1 + _info +fi +if [ "$opt_variant2" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant2 + _info +fi +if [ "$opt_variant3" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant3 + _info +fi + +_info "A false sense of security is worse than no security at all, see --disclaimer" + +# this'll umount only if we mounted debugfs ourselves +umount_debugfs + +# cleanup the temp decompressed config +[ -n "$dumped_config" ] && rm -f "$dumped_config" + +if [ "$opt_batch" = 1 -a "$opt_batch_format" = "nrpe" ]; then + if [ ! -z "$nrpe_vuln" ]; then + echo "Vulnerable:$nrpe_vuln" + else + echo "OK" + fi + [ "$nrpe_critical" = 1 ] && exit 2 # critical + [ "$nrpe_unknown" = 1 ] && exit 3 # unknown + exit 0 # ok +fi + +if [ "$opt_batch" = 1 -a "$opt_batch_format" = "json" ]; then + _echo 0 ${json_output%?}] +fi diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 46bcd06d5..e1f0a51df 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -42,12 +42,8 @@ import apt.progress from moulinette import msettings, msignals, m18n from moulinette.core import MoulinetteError, init_authenticator from moulinette.utils.log import getActionLogger -<<<<<<< b60d8ca822d08c8e3fdf8a17505ff3e285b28164 from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json -======= -from moulinette.utils.filesystem import read_json, write_to_json, read_file ->>>>>>> [mod] move spectre-meltdown check to diagnosis function from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain from yunohost.dyndns import _dyndns_available, _dyndns_provides @@ -637,13 +633,30 @@ def tools_diagnosis(auth, private=False): diagnosis['private']['regen_conf'] = service_regen_conf(with_diff=True, dry_run=True) diagnosis['security'] = { - # source https://askubuntu.com/questions/992137/how-to-check-that-kpti-is-enabled-on-my-ubuntu - "spectre-meltdown": "cpu_insecure" not in read_file("/proc/cpuinfo") + "CVE-2017-5754": { + "name": "meltdown", + "vulnerable": _check_if_vulnerable_to_meltdown(), + } } return diagnosis +def _check_if_vulnerable_to_meltdown(): + # script taken from https://github.com/speed47/spectre-meltdown-checker + # script commit id is store directly in the script + SCRIPT_PATH = "/usr/share/yunohost/yunohost-config/moulinette/spectre-meltdown-checker.sh" + + # example output from the script: + # [{"NAME":"SPECTRE VARIANT 1","CVE":"CVE-2017-5753","VULNERABLE":true,"INFOS":"only 23 opcodes found, should be >= 70, heuristic to be improved when official patches become available"},{"NAME":"SPECTRE VARIANT 2","CVE":"CVE-2017-5715","VULNERABLE":true,"INFOS":"IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability"},{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] + for CVE in json.loads(check_output("bash %s --batch json" % SCRIPT_PATH)): + # meltdown https://security-tracker.debian.org/tracker/CVE-2017-5754 + if CVE["CVE"] == "CVE-2017-5754": + return CVE["VULNERABLE"] + + raise Exception("We should never get there") + + def tools_port_available(port): """ Check availability of a local port From 3026035e41281e24fcd40cfa579570b7f9a68ac3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 13 Jan 2018 20:32:44 +0100 Subject: [PATCH 0596/1066] Use --variant 3 to directly check Meltdown only --- src/yunohost/tools.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e1f0a51df..001769108 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -643,18 +643,23 @@ def tools_diagnosis(auth, private=False): def _check_if_vulnerable_to_meltdown(): + # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754 + # script taken from https://github.com/speed47/spectre-meltdown-checker # script commit id is store directly in the script SCRIPT_PATH = "/usr/share/yunohost/yunohost-config/moulinette/spectre-meltdown-checker.sh" + # '--variant 3' corresponds to Meltdown # example output from the script: - # [{"NAME":"SPECTRE VARIANT 1","CVE":"CVE-2017-5753","VULNERABLE":true,"INFOS":"only 23 opcodes found, should be >= 70, heuristic to be improved when official patches become available"},{"NAME":"SPECTRE VARIANT 2","CVE":"CVE-2017-5715","VULNERABLE":true,"INFOS":"IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability"},{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] - for CVE in json.loads(check_output("bash %s --batch json" % SCRIPT_PATH)): - # meltdown https://security-tracker.debian.org/tracker/CVE-2017-5754 - if CVE["CVE"] == "CVE-2017-5754": - return CVE["VULNERABLE"] + # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] + try: + CVEs = json.loads(check_output("bash %s --batch json --variant 3" % SCRIPT_PATH)) + assert len(CVEs) == 1 + assert CVEs[0]["NAME"] == "MELTDOWN" + except: + raise Exception("Something wrong happened when trying to diagnose Meltdown vunerability.") - raise Exception("We should never get there") + return CVEs[0]["VULNERABLE"] def tools_port_available(port): From de4d4fdd378ca06e0549d4ae180e83ca88eabb4b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 13 Jan 2018 20:53:10 +0100 Subject: [PATCH 0597/1066] Moving spectre/meltdown checker script to vendor folder --- data/other/spectre-meltdown-checker.sh | 980 ------------------------- src/yunohost/tools.py | 2 +- 2 files changed, 1 insertion(+), 981 deletions(-) delete mode 100644 data/other/spectre-meltdown-checker.sh diff --git a/data/other/spectre-meltdown-checker.sh b/data/other/spectre-meltdown-checker.sh deleted file mode 100644 index 5658cf0a8..000000000 --- a/data/other/spectre-meltdown-checker.sh +++ /dev/null @@ -1,980 +0,0 @@ -#! /bin/sh - -# Message from YunoHost team: -# this is this version https://github.com/speed47/spectre-meltdown-checker/blob/dce917bfbb4fa5e135b7ed4c84881086766be802/spectre-meltdown-checker.sh -# that is included here -# You can find the licence (GPLv2))and author name in this script - -# Spectre & Meltdown checker -# -# Check for the latest version at: -# https://github.com/speed47/spectre-meltdown-checker -# git clone https://github.com/speed47/spectre-meltdown-checker.git -# or wget https://raw.githubusercontent.com/speed47/spectre-meltdown-checker/master/spectre-meltdown-checker.sh -# -# Stephane Lesimple - -VERSION=0.28 - -# Script configuration -show_usage() -{ - cat <] [--config ] [--map ] - - Modes: - Two modes are available. - - First mode is the "live" mode (default), it does its best to find information about the currently running kernel. - To run under this mode, just start the script without any option (you can also use --live explicitly) - - Second mode is the "offline" mode, where you can inspect a non-running kernel. - You'll need to specify the location of the vmlinux file, and if possible, the corresponding config and System.map files: - - --kernel vmlinux_file Specify a (possibly compressed) vmlinux file - --config kernel_config Specify a kernel config file - --map kernel_map_file Specify a kernel System.map file - - Options: - --no-color Don't use color codes - --verbose, -v Increase verbosity level - --no-sysfs Don't use the /sys interface even if present - --batch text Produce machine readable output, this is the default if --batch is specified alone - --batch json Produce JSON output formatted for Puppet, Ansible, Chef... - --batch nrpe Produce machine readable output formatted for NRPE - --variant [1,2,3] Specify which variant you'd like to check, by default all variants are checked - Can be specified multiple times (e.g. --variant 2 --variant 3) - - - IMPORTANT: - A false sense of security is worse than no security at all. - Please use the --disclaimer option to understand exactly what this script does. - -EOF -} - -show_disclaimer() -{ - cat <&2 -} - -_info() -{ - _echo 1 "$@" -} - -_info_nol() -{ - _echo_nol 1 "$@" -} - -_verbose() -{ - _echo 2 "$@" -} - -_debug() -{ - _echo 3 "\033[34m(debug) $@\033[0m" -} - -is_cpu_vulnerable() -{ - # param: 1, 2 or 3 (variant) - # returns 1 if vulnerable, 0 if not vulnerable, 255 on error - # by default, everything is vulnerable, we work in a "whitelist" logic here. - # usage: is_cpu_vulnerable 2 && do something if vulnerable - variant1=0 - variant2=0 - variant3=0 - if grep -q AMD /proc/cpuinfo; then - variant1=0 - variant2=1 - variant3=1 - elif grep -qi 'CPU implementer\s*:\s*0x41' /proc/cpuinfo; then - # ARM - # reference: https://developer.arm.com/support/security-update - cpupart=$(awk '/CPU part/ {print $4;exit}' /proc/cpuinfo) - cpuarch=$(awk '/CPU architecture/ {print $3;exit}' /proc/cpuinfo) - if [ -n "$cpupart" -a -n "$cpuarch" ]; then - # Cortex-R7 and Cortex-R8 are real-time and only used in medical devices or such - # I can't find their CPU part number, but it's probably not that useful anyway - # model R7 R8 A9 A15 A17 A57 A72 A73 A75 - # part ? ? 0xc09 0xc0f 0xc0e 0xd07 0xd08 0xd09 0xd0a - # arch 7? 7? 7 7 7 8 8 8 8 - if [ "$cpuarch" = 7 ] && echo "$cpupart" | grep -Eq '^0x(c09|c0f|c0e)$'; then - # armv7 vulnerable chips - variant1=0 - variant2=0 - elif [ "$cpuarch" = 8 ] && echo "$cpupart" | grep -Eq '^0x(d07|d08|d09|d0a)$'; then - # armv8 vulnerable chips - variant1=0 - variant2=0 - else - variant1=1 - variant2=1 - fi - # for variant3, only A75 is vulnerable - if [ "$cpuarch" = 8 -a "$cpupart" = 0xd0a ]; then - variant3=0 - else - variant3=1 - fi - fi - fi - [ "$1" = 1 ] && return $variant1 - [ "$1" = 2 ] && return $variant2 - [ "$1" = 3 ] && return $variant3 - return 255 -} - -show_header() -{ - _info "\033[1;34mSpectre and Meltdown mitigation detection tool v$VERSION\033[0m" - _info -} - -parse_opt_file() -{ - # parse_opt_file option_name option_value - option_name="$1" - option_value="$2" - if [ -z "$option_value" ]; then - show_header - show_usage - echo "$0: error: --$option_name expects one parameter (a file)" >&2 - exit 1 - elif [ ! -e "$option_value" ]; then - show_header - echo "$0: error: couldn't find file $option_value" >&2 - exit 1 - elif [ ! -f "$option_value" ]; then - show_header - echo "$0: error: $option_value is not a file" >&2 - exit 1 - elif [ ! -r "$option_value" ]; then - show_header - echo "$0: error: couldn't read $option_value (are you root?)" >&2 - exit 1 - fi - echo "$option_value" - exit 0 -} - -while [ -n "$1" ]; do - if [ "$1" = "--kernel" ]; then - opt_kernel=$(parse_opt_file kernel "$2") - [ $? -ne 0 ] && exit $? - shift 2 - opt_live=0 - elif [ "$1" = "--config" ]; then - opt_config=$(parse_opt_file config "$2") - [ $? -ne 0 ] && exit $? - shift 2 - opt_live=0 - elif [ "$1" = "--map" ]; then - opt_map=$(parse_opt_file map "$2") - [ $? -ne 0 ] && exit $? - shift 2 - opt_live=0 - elif [ "$1" = "--live" ]; then - opt_live_explicit=1 - shift - elif [ "$1" = "--no-color" ]; then - opt_no_color=1 - shift - elif [ "$1" = "--no-sysfs" ]; then - opt_no_sysfs=1 - shift - elif [ "$1" = "--batch" ]; then - opt_batch=1 - opt_verbose=0 - shift - case "$1" in - text|nrpe|json) opt_batch_format="$1"; shift;; - --*) ;; # allow subsequent flags - '') ;; # allow nothing at all - *) - echo "$0: error: unknown batch format '$1'" - echo "$0: error: --batch expects a format from: text, nrpe, json" - exit 1 >&2 - ;; - esac - elif [ "$1" = "-v" -o "$1" = "--verbose" ]; then - opt_verbose=$(expr $opt_verbose + 1) - shift - elif [ "$1" = "--variant" ]; then - if [ -z "$2" ]; then - echo "$0: error: option --variant expects a parameter (1, 2 or 3)" >&2 - exit 1 - fi - case "$2" in - 1) opt_variant1=1; opt_allvariants=0;; - 2) opt_variant2=1; opt_allvariants=0;; - 3) opt_variant3=1; opt_allvariants=0;; - *) - echo "$0: error: invalid parameter '$2' for --variant, expected either 1, 2 or 3" >&2; - exit 1;; - esac - shift 2 - elif [ "$1" = "-h" -o "$1" = "--help" ]; then - show_header - show_usage - exit 0 - elif [ "$1" = "--version" ]; then - opt_no_color=1 - show_header - exit 1 - elif [ "$1" = "--disclaimer" ]; then - show_header - show_disclaimer - exit 0 - else - show_header - show_usage - echo "$0: error: unknown option '$1'" - exit 1 - fi -done - -show_header - -# print status function -pstatus() -{ - if [ "$opt_no_color" = 1 ]; then - _info_nol "$2" - else - case "$1" in - red) col="\033[101m\033[30m";; - green) col="\033[102m\033[30m";; - yellow) col="\033[103m\033[30m";; - blue) col="\033[104m\033[30m";; - *) col="";; - esac - _info_nol "$col $2 \033[0m" - fi - [ -n "$3" ] && _info_nol " ($3)" - _info -} - -# Print the final status of a vulnerability (incl. batch mode) -# Arguments are: CVE UNK/OK/VULN description -pvulnstatus() -{ - if [ "$opt_batch" = 1 ]; then - case "$opt_batch_format" in - text) _echo 0 "$1: $2 ($3)";; - nrpe) - case "$2" in - UKN) nrpe_unknown="1";; - VULN) nrpe_critical="1"; nrpe_vuln="$nrpe_vuln $1";; - esac - ;; - json) - case "$1" in - CVE-2017-5753) aka="SPECTRE VARIANT 1";; - CVE-2017-5715) aka="SPECTRE VARIANT 2";; - CVE-2017-5754) aka="MELTDOWN";; - esac - case "$2" in - UKN) is_vuln="unknown";; - VULN) is_vuln="true";; - OK) is_vuln="false";; - esac - json_output="${json_output:-[}{\"NAME\":\""$aka"\",\"CVE\":\""$1"\",\"VULNERABLE\":$is_vuln,\"INFOS\":\""$3"\"}," - ;; - esac - fi - - _info_nol "> \033[46m\033[30mSTATUS:\033[0m " - vulnstatus="$2" - shift 2 - case "$vulnstatus" in - UNK) pstatus yellow UNKNOWN "$@";; - VULN) pstatus red 'VULNERABLE' "$@";; - OK) pstatus green 'NOT VULNERABLE' "$@";; - esac -} - - -# The 3 below functions are taken from the extract-linux script, available here: -# https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux -# The functions have been modified for better integration to this script -# The original header of the file has been retained below - -# ---------------------------------------------------------------------- -# extract-vmlinux - Extract uncompressed vmlinux from a kernel image -# -# Inspired from extract-ikconfig -# (c) 2009,2010 Dick Streefland -# -# (c) 2011 Corentin Chary -# -# Licensed under the GNU General Public License, version 2 (GPLv2). -# ---------------------------------------------------------------------- - -vmlinux='' -vmlinux_err='' -check_vmlinux() -{ - readelf -h "$1" > /dev/null 2>&1 || return 1 - return 0 -} - -try_decompress() -{ - # The obscure use of the "tr" filter is to work around older versions of - # "grep" that report the byte offset of the line instead of the pattern. - - # Try to find the header ($1) and decompress from here - for pos in `tr "$1\n$2" "\n$2=" < "$6" | grep -abo "^$2"` - do - _debug "try_decompress: magic for $3 found at offset $pos" - if ! which "$3" >/dev/null 2>&1; then - vmlinux_err="missing '$3' tool, please install it, usually it's in the '$5' package" - return 0 - fi - pos=${pos%%:*} - tail -c+$pos "$6" 2>/dev/null | $3 $4 > $vmlinuxtmp 2>/dev/null - if check_vmlinux "$vmlinuxtmp"; then - vmlinux="$vmlinuxtmp" - _debug "try_decompress: decompressed with $3 successfully!" - return 0 - else - _debug "try_decompress: decompression with $3 did not work" - fi - done - return 1 -} - -extract_vmlinux() -{ - [ -n "$1" ] || return 1 - # Prepare temp files: - vmlinuxtmp="$(mktemp /tmp/vmlinux-XXXXXX)" - trap "rm -f $vmlinuxtmp" EXIT - - # Initial attempt for uncompressed images or objects: - if check_vmlinux "$1"; then - cat "$1" > "$vmlinuxtmp" - vmlinux=$vmlinuxtmp - return 0 - fi - - # That didn't work, so retry after decompression. - try_decompress '\037\213\010' xy gunzip '' gunzip "$1" && return 0 - try_decompress '\3757zXZ\000' abcde unxz '' xz-utils "$1" && return 0 - try_decompress 'BZh' xy bunzip2 '' bzip2 "$1" && return 0 - try_decompress '\135\0\0\0' xxx unlzma '' xz-utils "$1" && return 0 - try_decompress '\211\114\132' xy 'lzop' '-d' lzop "$1" && return 0 - try_decompress '\002\041\114\030' xyy 'lz4' '-d -l' liblz4-tool "$1" && return 0 - return 1 -} - -# end of extract-vmlinux functions - -# check for mode selection inconsistency -if [ "$opt_live_explicit" = 1 ]; then - if [ -n "$opt_kernel" -o -n "$opt_config" -o -n "$opt_map" ]; then - show_usage - echo "$0: error: incompatible modes specified, use either --live or --kernel/--config/--map" - exit 1 - fi -fi - -# root check (only for live mode, for offline mode, we already checked if we could read the files) - -if [ "$opt_live" = 1 ]; then - if [ "$(id -u)" -ne 0 ]; then - _warn "Note that you should launch this script with root privileges to get accurate information." - _warn "We'll proceed but you might see permission denied errors." - _warn "To run it as root, you can try the following command: sudo $0" - _warn - fi - _info "Checking for vulnerabilities against running kernel \033[35m"$(uname -s) $(uname -r) $(uname -v) $(uname -m)"\033[0m" - _info "CPU is\033[35m"$(grep '^model name' /proc/cpuinfo | cut -d: -f2 | head -1)"\033[0m" - - # try to find the image of the current running kernel - # first, look for the BOOT_IMAGE hint in the kernel cmdline - if [ -r /proc/cmdline ] && grep -q 'BOOT_IMAGE=' /proc/cmdline; then - opt_kernel=$(grep -Eo 'BOOT_IMAGE=[^ ]+' /proc/cmdline | cut -d= -f2) - _debug "found opt_kernel=$opt_kernel in /proc/cmdline" - # if we have a dedicated /boot partition, our bootloader might have just called it / - # so try to prepend /boot and see if we find anything - [ -e "/boot/$opt_kernel" ] && opt_kernel="/boot/$opt_kernel" - _debug "opt_kernel is now $opt_kernel" - # else, the full path is already there (most probably /boot/something) - fi - # if we didn't find a kernel, default to guessing - if [ ! -e "$opt_kernel" ]; then - [ -e /boot/vmlinuz-linux ] && opt_kernel=/boot/vmlinuz-linux - [ -e /boot/vmlinuz-linux-libre ] && opt_kernel=/boot/vmlinuz-linux-libre - [ -e /boot/vmlinuz-$(uname -r) ] && opt_kernel=/boot/vmlinuz-$(uname -r) - [ -e /boot/kernel-$( uname -r) ] && opt_kernel=/boot/kernel-$( uname -r) - [ -e /boot/bzImage-$(uname -r) ] && opt_kernel=/boot/bzImage-$(uname -r) - [ -e /boot/kernel-genkernel-$(uname -m)-$(uname -r) ] && opt_kernel=/boot/kernel-genkernel-$(uname -m)-$(uname -r) - fi - - # system.map - if [ -e /proc/kallsyms ] ; then - opt_map="/proc/kallsyms" - elif [ -e /boot/System.map-$(uname -r) ] ; then - opt_map=/boot/System.map-$(uname -r) - fi - - # config - if [ -e /proc/config.gz ] ; then - dumped_config="$(mktemp /tmp/config-XXXXXX)" - gunzip -c /proc/config.gz > $dumped_config - # dumped_config will be deleted at the end of the script - opt_config=$dumped_config - elif [ -e /boot/config-$(uname -r) ]; then - opt_config=/boot/config-$(uname -r) - fi -else - _info "Checking for vulnerabilities against specified kernel" -fi -if [ -n "$opt_kernel" ]; then - _verbose "Will use vmlinux image \033[35m$opt_kernel\033[0m" -else - _verbose "Will use no vmlinux image (accuracy might be reduced)" -fi -if [ -n "$dumped_config" ]; then - _verbose "Will use kconfig \033[35m/proc/config.gz\033[0m" -elif [ -n "$opt_config" ]; then - _verbose "Will use kconfig \033[35m$opt_config\033[0m" -else - _verbose "Will use no kconfig (accuracy might be reduced)" -fi -if [ -n "$opt_map" ]; then - _verbose "Will use System.map file \033[35m$opt_map\033[0m" -else - _verbose "Will use no System.map file (accuracy might be reduced)" -fi - -if [ -e "$opt_kernel" ]; then - if ! which readelf >/dev/null 2>&1; then - vmlinux_err="missing 'readelf' tool, please install it, usually it's in the 'binutils' package" - else - extract_vmlinux "$opt_kernel" - fi -else - vmlinux_err="couldn't find your kernel image in /boot, if you used netboot, this is normal" -fi -if [ -z "$vmlinux" -o ! -r "$vmlinux" ]; then - [ -z "$vmlinux_err" ] && vmlinux_err="couldn't extract your kernel from $opt_kernel" -fi - -_info - -# end of header stuff - -# now we define some util functions and the check_*() funcs, as -# the user can choose to execute only some of those - -mount_debugfs() -{ - if [ ! -e /sys/kernel/debug/sched_features ]; then - # try to mount the debugfs hierarchy ourselves and remember it to umount afterwards - mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null && mounted_debugfs=1 - fi -} - -umount_debugfs() -{ - if [ "$mounted_debugfs" = 1 ]; then - # umount debugfs if we did mount it ourselves - umount /sys/kernel/debug - fi -} - -sys_interface_check() -{ - [ "$opt_live" = 1 -a "$opt_no_sysfs" = 0 -a -r "$1" ] || return 1 - _info_nol "* Checking whether we're safe according to the /sys interface: " - if grep -qi '^not affected' "$1"; then - # Not affected - status=OK - pstatus green YES "kernel confirms that your CPU is unaffected" - elif grep -qi '^mitigation' "$1"; then - # Mitigation: PTI - status=OK - pstatus green YES "kernel confirms that the mitigation is active" - elif grep -qi '^vulnerable' "$1"; then - # Vulnerable - status=VULN - pstatus red NO "kernel confirms your system is vulnerable" - else - status=UNK - pstatus yellow UNKNOWN "unknown value reported by kernel" - fi - msg=$(cat "$1") - _debug "sys_interface_check: $1=$msg" - return 0 -} - -################### -# SPECTRE VARIANT 1 -check_variant1() -{ - _info "\033[1;34mCVE-2017-5753 [bounds check bypass] aka 'Spectre Variant 1'\033[0m" - - status=UNK - sys_interface_available=0 - msg='' - if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v1"; then - # this kernel has the /sys interface, trust it over everything - sys_interface_available=1 - else - # no /sys interface (or offline mode), fallback to our own ways - _info_nol "* Checking count of LFENCE opcodes in kernel: " - if [ -n "$vmlinux_err" ]; then - msg="couldn't check ($vmlinux_err)" - status=UNK - pstatus yellow UNKNOWN - else - if ! which objdump >/dev/null 2>&1; then - msg="missing 'objdump' tool, please install it, usually it's in the binutils package" - status=UNK - pstatus yellow UNKNOWN - else - # here we disassemble the kernel and count the number of occurences of the LFENCE opcode - # in non-patched kernels, this has been empirically determined as being around 40-50 - # in patched kernels, this is more around 70-80, sometimes way higher (100+) - # v0.13: 68 found in a 3.10.23-xxxx-std-ipv6-64 (with lots of modules compiled-in directly), which doesn't have the LFENCE patches, - # so let's push the threshold to 70. - nb_lfence=$(objdump -d "$vmlinux" | grep -wc lfence) - if [ "$nb_lfence" -lt 70 ]; then - msg="only $nb_lfence opcodes found, should be >= 70, heuristic to be improved when official patches become available" - status=VULN - pstatus red NO - else - msg="$nb_lfence opcodes found, which is >= 70, heuristic to be improved when official patches become available" - status=OK - pstatus green YES - fi - fi - fi - fi - - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 1; then - # override status & msg in case CPU is not vulnerable after all - msg="your CPU vendor reported your CPU model as not vulnerable" - status=OK - fi - - # report status - pvulnstatus CVE-2017-5753 "$status" "$msg" -} - -################### -# SPECTRE VARIANT 2 -check_variant2() -{ - _info "\033[1;34mCVE-2017-5715 [branch target injection] aka 'Spectre Variant 2'\033[0m" - - status=UNK - sys_interface_available=0 - msg='' - if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then - # this kernel has the /sys interface, trust it over everything - sys_interface_available=1 - else - _info "* Mitigation 1" - _info_nol "* Hardware (CPU microcode) support for mitigation: " - if [ ! -e /dev/cpu/0/msr ]; then - # try to load the module ourselves (and remember it so we can rmmod it afterwards) - modprobe msr 2>/dev/null && insmod_msr=1 - _debug "attempted to load module msr, ret=$insmod_msr" - fi - if [ ! -e /dev/cpu/0/msr ]; then - pstatus yellow UNKNOWN "couldn't read /dev/cpu/0/msr, is msr support enabled in your kernel?" - else - # the new MSR 'SPEC_CTRL' is at offset 0x48 - # here we use dd, it's the same as using 'rdmsr 0x48' but without needing the rdmsr tool - # if we get a read error, the MSR is not there - dd if=/dev/cpu/0/msr of=/dev/null bs=8 count=1 skip=9 2>/dev/null - if [ $? -eq 0 ]; then - pstatus green YES - else - pstatus red NO - fi - fi - - if [ "$insmod_msr" = 1 ]; then - # if we used modprobe ourselves, rmmod the module - rmmod msr 2>/dev/null - _debug "attempted to unload module msr, ret=$?" - fi - - _info_nol "* Kernel support for IBRS: " - if [ "$opt_live" = 1 ]; then - mount_debugfs - for ibrs_file in \ - /sys/kernel/debug/ibrs_enabled \ - /sys/kernel/debug/x86/ibrs_enabled \ - /proc/sys/kernel/ibrs_enabled; do - if [ -e "$ibrs_file" ]; then - # if the file is there, we have IBRS compiled-in - # /sys/kernel/debug/ibrs_enabled: vanilla - # /sys/kernel/debug/x86/ibrs_enabled: RedHat (see https://access.redhat.com/articles/3311301) - # /proc/sys/kernel/ibrs_enabled: OpenSUSE tumbleweed - pstatus green YES - ibrs_supported=1 - ibrs_enabled=$(cat "$ibrs_file" 2>/dev/null) - _debug "ibrs: found $ibrs_file=$ibrs_enabled" - break - else - _debug "ibrs: file $ibrs_file doesn't exist" - fi - done - fi - if [ "$ibrs_supported" != 1 -a -n "$opt_map" ]; then - if grep -q spec_ctrl "$opt_map"; then - pstatus green YES - ibrs_supported=1 - _debug "ibrs: found '*spec_ctrl*' symbol in $opt_map" - fi - fi - if [ "$ibrs_supported" != 1 ]; then - pstatus red NO - fi - - _info_nol "* IBRS enabled for Kernel space: " - if [ "$opt_live" = 1 ]; then - # 0 means disabled - # 1 is enabled only for kernel space - # 2 is enabled for kernel and user space - case "$ibrs_enabled" in - "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; - 0) pstatus red NO;; - 1 | 2) pstatus green YES;; - *) pstatus yellow UNKNOWN;; - esac - else - pstatus blue N/A "not testable in offline mode" - fi - - _info_nol "* IBRS enabled for User space: " - if [ "$opt_live" = 1 ]; then - case "$ibrs_enabled" in - "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; - 0 | 1) pstatus red NO;; - 2) pstatus green YES;; - *) pstatus yellow UNKNOWN;; - esac - else - pstatus blue N/A "not testable in offline mode" - fi - - _info "* Mitigation 2" - _info_nol "* Kernel compiled with retpoline option: " - # We check the RETPOLINE kernel options - if [ -r "$opt_config" ]; then - if grep -q '^CONFIG_RETPOLINE=y' "$opt_config"; then - pstatus green YES - retpoline=1 - _debug "retpoline: found "$(grep '^CONFIG_RETPOLINE' "$opt_config")" in $opt_config" - else - pstatus red NO - fi - else - pstatus yellow UNKNOWN "couldn't read your kernel configuration" - fi - - _info_nol "* Kernel compiled with a retpoline-aware compiler: " - # Now check if the compiler used to compile the kernel knows how to insert retpolines in generated asm - # For gcc, this is -mindirect-branch=thunk-extern (detected by the kernel makefiles) - # See gcc commit https://github.com/hjl-tools/gcc/commit/23b517d4a67c02d3ef80b6109218f2aadad7bd79 - # In latest retpoline LKML patches, the noretpoline_setup symbol exists only if CONFIG_RETPOLINE is set - # *AND* if the compiler is retpoline-compliant, so look for that symbol - if [ -n "$opt_map" ]; then - # look for the symbol - if grep -qw noretpoline_setup "$opt_map"; then - retpoline_compiler=1 - pstatus green YES "noretpoline_setup symbol found in System.map" - else - pstatus red NO - fi - elif [ -n "$vmlinux" ]; then - # look for the symbol - if which nm >/dev/null 2>&1; then - # the proper way: use nm and look for the symbol - if nm "$vmlinux" 2>/dev/null | grep -qw 'noretpoline_setup'; then - retpoline_compiler=1 - pstatus green YES "noretpoline_setup found in vmlinux symbols" - else - pstatus red NO - fi - elif grep -q noretpoline_setup "$vmlinux"; then - # if we don't have nm, nevermind, the symbol name is long enough to not have - # any false positive using good old grep directly on the binary - retpoline_compiler=1 - pstatus green YES "noretpoline_setup found in vmlinux" - else - pstatus red NO - fi - else - pstatus yellow UNKNOWN "couldn't find your kernel image or System.map" - fi - fi - - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 2; then - # override status & msg in case CPU is not vulnerable after all - pvulnstatus CVE-2017-5715 OK "your CPU vendor reported your CPU model as not vulnerable" - elif [ -z "$msg" ]; then - # if msg is empty, sysfs check didn't fill it, rely on our own test - if [ "$retpoline" = 1 -a "$retpoline_compiler" = 1 ]; then - pvulnstatus CVE-2017-5715 OK "retpoline mitigate the vulnerability" - elif [ "$opt_live" = 1 ]; then - if [ "$ibrs_enabled" = 1 -o "$ibrs_enabled" = 2 ]; then - pvulnstatus CVE-2017-5715 OK "IBRS mitigates the vulnerability" - else - pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" - fi - else - if [ "$ibrs_supported" = 1 ]; then - pvulnstatus CVE-2017-5715 OK "offline mode: IBRS will mitigate the vulnerability if enabled at runtime" - else - pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" - fi - fi - else - pvulnstatus CVE-2017-5715 "$status" "$msg" - fi -} - -######################## -# MELTDOWN aka VARIANT 3 -check_variant3() -{ - _info "\033[1;34mCVE-2017-5754 [rogue data cache load] aka 'Meltdown' aka 'Variant 3'\033[0m" - - status=UNK - sys_interface_available=0 - msg='' - if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/meltdown"; then - # this kernel has the /sys interface, trust it over everything - sys_interface_available=1 - else - _info_nol "* Kernel supports Page Table Isolation (PTI): " - kpti_support=0 - kpti_can_tell=0 - if [ -n "$opt_config" ]; then - kpti_can_tell=1 - if grep -Eq '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config"; then - _debug "kpti_support: found option "$(grep -E '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config")" in $opt_config" - kpti_support=1 - fi - fi - if [ "$kpti_support" = 0 -a -n "$opt_map" ]; then - # it's not an elif: some backports don't have the PTI config but still include the patch - # so we try to find an exported symbol that is part of the PTI patch in System.map - kpti_can_tell=1 - if grep -qw kpti_force_enabled "$opt_map"; then - _debug "kpti_support: found kpti_force_enabled in $opt_map" - kpti_support=1 - fi - fi - if [ "$kpti_support" = 0 -a -n "$vmlinux" ]; then - # same as above but in case we don't have System.map and only vmlinux, look for the - # nopti option that is part of the patch (kernel command line option) - kpti_can_tell=1 - if ! which strings >/dev/null 2>&1; then - pstatus yellow UNKNOWN "missing 'strings' tool, please install it, usually it's in the binutils package" - else - if strings "$vmlinux" | grep -qw nopti; then - _debug "kpti_support: found nopti string in $vmlinux" - kpti_support=1 - fi - fi - fi - - if [ "$kpti_support" = 1 ]; then - pstatus green YES - elif [ "$kpti_can_tell" = 1 ]; then - pstatus red NO - else - pstatus yellow UNKNOWN "couldn't read your kernel configuration nor System.map file" - fi - - mount_debugfs - _info_nol "* PTI enabled and active: " - if [ "$opt_live" = 1 ]; then - dmesg_grep="Kernel/User page tables isolation: enabled" - dmesg_grep="$dmesg_grep|Kernel page table isolation enabled" - dmesg_grep="$dmesg_grep|x86/pti: Unmapping kernel while in userspace" - if grep ^flags /proc/cpuinfo | grep -qw pti; then - # vanilla PTI patch sets the 'pti' flag in cpuinfo - _debug "kpti_enabled: found 'pti' flag in /proc/cpuinfo" - kpti_enabled=1 - elif grep ^flags /proc/cpuinfo | grep -qw kaiser; then - # kernel line 4.9 sets the 'kaiser' flag in cpuinfo - _debug "kpti_enabled: found 'kaiser' flag in /proc/cpuinfo" - kpti_enabled=1 - elif [ -e /sys/kernel/debug/x86/pti_enabled ]; then - # RedHat Backport creates a dedicated file, see https://access.redhat.com/articles/3311301 - kpti_enabled=$(cat /sys/kernel/debug/x86/pti_enabled 2>/dev/null) - _debug "kpti_enabled: file /sys/kernel/debug/x86/pti_enabled exists and says: $kpti_enabled" - elif dmesg | grep -Eq "$dmesg_grep"; then - # if we can't find the flag, grep dmesg output - _debug "kpti_enabled: found hint in dmesg: "$(dmesg | grep -E "$dmesg_grep") - kpti_enabled=1 - elif [ -r /var/log/dmesg ] && grep -Eq "$dmesg_grep" /var/log/dmesg; then - # if we can't find the flag in dmesg output, grep in /var/log/dmesg when readable - _debug "kpti_enabled: found hint in /var/log/dmesg: "$(grep -E "$dmesg_grep" /var/log/dmesg) - kpti_enabled=1 - else - _debug "kpti_enabled: couldn't find any hint that PTI is enabled" - kpti_enabled=0 - fi - if [ "$kpti_enabled" = 1 ]; then - pstatus green YES - else - pstatus red NO - fi - else - pstatus blue N/A "can't verify if PTI is enabled in offline mode" - fi - fi - - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it - cve='CVE-2017-5754' - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 3; then - # override status & msg in case CPU is not vulnerable after all - pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" - elif [ -z "$msg" ]; then - # if msg is empty, sysfs check didn't fill it, rely on our own test - if [ "$opt_live" = 1 ]; then - if [ "$kpti_enabled" = 1 ]; then - pvulnstatus $cve OK "PTI mitigates the vulnerability" - else - pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" - fi - else - if [ "$kpti_support" = 1 ]; then - pvulnstatus $cve OK "offline mode: PTI will mitigate the vulnerability if enabled at runtime" - else - pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" - fi - fi - else - pvulnstatus $cve "$status" "$msg" - fi -} - -# now run the checks the user asked for -if [ "$opt_variant1" = 1 -o "$opt_allvariants" = 1 ]; then - check_variant1 - _info -fi -if [ "$opt_variant2" = 1 -o "$opt_allvariants" = 1 ]; then - check_variant2 - _info -fi -if [ "$opt_variant3" = 1 -o "$opt_allvariants" = 1 ]; then - check_variant3 - _info -fi - -_info "A false sense of security is worse than no security at all, see --disclaimer" - -# this'll umount only if we mounted debugfs ourselves -umount_debugfs - -# cleanup the temp decompressed config -[ -n "$dumped_config" ] && rm -f "$dumped_config" - -if [ "$opt_batch" = 1 -a "$opt_batch_format" = "nrpe" ]; then - if [ ! -z "$nrpe_vuln" ]; then - echo "Vulnerable:$nrpe_vuln" - else - echo "OK" - fi - [ "$nrpe_critical" = 1 ] && exit 2 # critical - [ "$nrpe_unknown" = 1 ] && exit 3 # unknown - exit 0 # ok -fi - -if [ "$opt_batch" = 1 -a "$opt_batch_format" = "json" ]; then - _echo 0 ${json_output%?}] -fi diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 001769108..8b6e5cc44 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -647,7 +647,7 @@ def _check_if_vulnerable_to_meltdown(): # script taken from https://github.com/speed47/spectre-meltdown-checker # script commit id is store directly in the script - SCRIPT_PATH = "/usr/share/yunohost/yunohost-config/moulinette/spectre-meltdown-checker.sh" + SCRIPT_PATH = "./vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" # '--variant 3' corresponds to Meltdown # example output from the script: From 80cfa3a786173976cf61a4a0202291f0c08fc8fc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 13 Jan 2018 21:22:22 +0100 Subject: [PATCH 0598/1066] Squashed 'src/yunohost/vendor/spectre-meltdown-checker/' content from commit 7f92717 git-subtree-dir: src/yunohost/vendor/spectre-meltdown-checker git-subtree-split: 7f92717a2c720a55785f8814a872eed7d380fdcf --- LICENSE | 674 +++++++++++++++++++++++++ README.md | 45 ++ spectre-meltdown-checker.sh | 982 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1701 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100755 spectre-meltdown-checker.sh diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 000000000..518b3ec9b --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +Spectre & Meltdown Checker +========================== + +A simple shell script to tell if your Linux installation is vulnerable against the 3 "speculative execution" CVEs that were made public early 2018. + +Without options, it'll inspect your currently running kernel. +You can also specify a kernel image on the command line, if you'd like to inspect a kernel you're not running. + +The script will do its best to detect mitigations, including backported non-vanilla patches, regardless of the advertised kernel version number. + +## Example of script output + +![checker](https://framapic.org/6O4v4AAwMenv/M6J4CFWwsB3z.png) + +## Quick summary of the CVEs + +**CVE-2017-5753** bounds check bypass (Spectre Variant 1) + + - Impact: Kernel & all software + - Mitigation: recompile software *and* kernel with a modified compiler that introduces the LFENCE opcode at the proper positions in the resulting code + - Performance impact of the mitigation: negligible + +**CVE-2017-5715** branch target injection (Spectre Variant 2) + + - Impact: Kernel + - Mitigation 1: new opcode via microcode update that should be used by up to date compilers to protect the BTB (by flushing indirect branch predictors) + - Mitigation 2: introducing "retpoline" into compilers, and recompile software/OS with it + - Performance impact of the mitigation: high for mitigation 1, medium for mitigation 2, depending on your CPU + +**CVE-2017-5754** rogue data cache load (Meltdown) + + - Impact: Kernel + - Mitigation: updated kernel (with PTI/KPTI patches), updating the kernel is enough + - Performance impact of the mitigation: low to medium + +## Disclaimer + +This tool does its best to determine whether your system is immune (or has proper mitigations in place) for the collectively named "speculative execution" vulnerabilities. It doesn't attempt to run any kind of exploit, and can't guarantee that your system is secure, but rather helps you verifying whether your system has the known correct mitigations in place. +However, some mitigations could also exist in your kernel that this script doesn't know (yet) how to detect, or it might falsely detect mitigations that in the end don't work as expected (for example, on backported or modified kernels). + +Your system exposure also depends on your CPU. As of now, AMD and ARM processors are marked as immune to some or all of these vulnerabilities (except some specific ARM models). All Intel processors manufactured since circa 1995 are thought to be vulnerable. Whatever processor one uses, one might seek more information from the manufacturer of that processor and/or of the device in which it runs. + +The nature of the discovered vulnerabilities being quite new, the landscape of vulnerable processors can be expected to change over time, which is why this script makes the assumption that all CPUs are vulnerable, except if the manufacturer explicitly stated otherwise in a verifiable public announcement. + +This tool has been released in the hope that it'll be useful, but don't use it to jump to conclusions about your security. diff --git a/spectre-meltdown-checker.sh b/spectre-meltdown-checker.sh new file mode 100755 index 000000000..f71deb5bf --- /dev/null +++ b/spectre-meltdown-checker.sh @@ -0,0 +1,982 @@ +#! /bin/sh +# Spectre & Meltdown checker +# +# Check for the latest version at: +# https://github.com/speed47/spectre-meltdown-checker +# git clone https://github.com/speed47/spectre-meltdown-checker.git +# or wget https://raw.githubusercontent.com/speed47/spectre-meltdown-checker/master/spectre-meltdown-checker.sh +# +# Stephane Lesimple +# +VERSION=0.29 + +# Script configuration +show_usage() +{ + cat <] [--config ] [--map ] + + Modes: + Two modes are available. + + First mode is the "live" mode (default), it does its best to find information about the currently running kernel. + To run under this mode, just start the script without any option (you can also use --live explicitly) + + Second mode is the "offline" mode, where you can inspect a non-running kernel. + You'll need to specify the location of the vmlinux file, and if possible, the corresponding config and System.map files: + + --kernel vmlinux_file Specify a (possibly compressed) vmlinux file + --config kernel_config Specify a kernel config file + --map kernel_map_file Specify a kernel System.map file + + Options: + --no-color Don't use color codes + --verbose, -v Increase verbosity level + --no-sysfs Don't use the /sys interface even if present + --batch text Produce machine readable output, this is the default if --batch is specified alone + --batch json Produce JSON output formatted for Puppet, Ansible, Chef... + --batch nrpe Produce machine readable output formatted for NRPE + --variant [1,2,3] Specify which variant you'd like to check, by default all variants are checked + Can be specified multiple times (e.g. --variant 2 --variant 3) + + + IMPORTANT: + A false sense of security is worse than no security at all. + Please use the --disclaimer option to understand exactly what this script does. + +EOF +} + +show_disclaimer() +{ + cat <&2 +} + +_info() +{ + _echo 1 "$@" +} + +_info_nol() +{ + _echo_nol 1 "$@" +} + +_verbose() +{ + _echo 2 "$@" +} + +_debug() +{ + _echo 3 "\033[34m(debug) $@\033[0m" +} + +is_cpu_vulnerable() +{ + # param: 1, 2 or 3 (variant) + # returns 0 if vulnerable, 1 if not vulnerable + # (note that in shell, a return of 0 is success) + # by default, everything is vulnerable, we work in a "whitelist" logic here. + # usage: is_cpu_vulnerable 2 && do something if vulnerable + variant1=0 + variant2=0 + variant3=0 + + if grep -q AMD /proc/cpuinfo; then + # AMD revised their statement about variant2 => vulnerable + # https://www.amd.com/en/corporate/speculative-execution + variant3=1 + elif grep -qi 'CPU implementer\s*:\s*0x41' /proc/cpuinfo; then + # ARM + # reference: https://developer.arm.com/support/security-update + cpupart=$(awk '/CPU part/ {print $4;exit}' /proc/cpuinfo) + cpuarch=$(awk '/CPU architecture/ {print $3;exit}' /proc/cpuinfo) + if [ -n "$cpupart" -a -n "$cpuarch" ]; then + # Cortex-R7 and Cortex-R8 are real-time and only used in medical devices or such + # I can't find their CPU part number, but it's probably not that useful anyway + # model R7 R8 A9 A15 A17 A57 A72 A73 A75 + # part ? ? 0xc09 0xc0f 0xc0e 0xd07 0xd08 0xd09 0xd0a + # arch 7? 7? 7 7 7 8 8 8 8 + if [ "$cpuarch" = 7 ] && echo "$cpupart" | grep -Eq '^0x(c09|c0f|c0e)$'; then + # armv7 vulnerable chips + : + elif [ "$cpuarch" = 8 ] && echo "$cpupart" | grep -Eq '^0x(d07|d08|d09|d0a)$'; then + # armv8 vulnerable chips + : + else + variant1=1 + variant2=1 + fi + # for variant3, only A75 is vulnerable + if ! [ "$cpuarch" = 8 -a "$cpupart" = 0xd0a ]; then + variant3=1 + fi + fi + fi + + [ "$1" = 1 ] && return $variant1 + [ "$1" = 2 ] && return $variant2 + [ "$1" = 3 ] && return $variant3 + echo "$0: error: invalid variant '$1' passed to is_cpu_vulnerable()" >&2 + exit 1 +} + +show_header() +{ + _info "\033[1;34mSpectre and Meltdown mitigation detection tool v$VERSION\033[0m" + _info +} + +parse_opt_file() +{ + # parse_opt_file option_name option_value + option_name="$1" + option_value="$2" + if [ -z "$option_value" ]; then + show_header + show_usage + echo "$0: error: --$option_name expects one parameter (a file)" >&2 + exit 1 + elif [ ! -e "$option_value" ]; then + show_header + echo "$0: error: couldn't find file $option_value" >&2 + exit 1 + elif [ ! -f "$option_value" ]; then + show_header + echo "$0: error: $option_value is not a file" >&2 + exit 1 + elif [ ! -r "$option_value" ]; then + show_header + echo "$0: error: couldn't read $option_value (are you root?)" >&2 + exit 1 + fi + echo "$option_value" + exit 0 +} + +while [ -n "$1" ]; do + if [ "$1" = "--kernel" ]; then + opt_kernel=$(parse_opt_file kernel "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--config" ]; then + opt_config=$(parse_opt_file config "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--map" ]; then + opt_map=$(parse_opt_file map "$2") + [ $? -ne 0 ] && exit $? + shift 2 + opt_live=0 + elif [ "$1" = "--live" ]; then + opt_live_explicit=1 + shift + elif [ "$1" = "--no-color" ]; then + opt_no_color=1 + shift + elif [ "$1" = "--no-sysfs" ]; then + opt_no_sysfs=1 + shift + elif [ "$1" = "--batch" ]; then + opt_batch=1 + opt_verbose=0 + shift + case "$1" in + text|nrpe|json) opt_batch_format="$1"; shift;; + --*) ;; # allow subsequent flags + '') ;; # allow nothing at all + *) + echo "$0: error: unknown batch format '$1'" + echo "$0: error: --batch expects a format from: text, nrpe, json" + exit 1 >&2 + ;; + esac + elif [ "$1" = "-v" -o "$1" = "--verbose" ]; then + opt_verbose=$(expr $opt_verbose + 1) + shift + elif [ "$1" = "--variant" ]; then + if [ -z "$2" ]; then + echo "$0: error: option --variant expects a parameter (1, 2 or 3)" >&2 + exit 1 + fi + case "$2" in + 1) opt_variant1=1; opt_allvariants=0;; + 2) opt_variant2=1; opt_allvariants=0;; + 3) opt_variant3=1; opt_allvariants=0;; + *) + echo "$0: error: invalid parameter '$2' for --variant, expected either 1, 2 or 3" >&2; + exit 1;; + esac + shift 2 + elif [ "$1" = "-h" -o "$1" = "--help" ]; then + show_header + show_usage + exit 0 + elif [ "$1" = "--version" ]; then + opt_no_color=1 + show_header + exit 1 + elif [ "$1" = "--disclaimer" ]; then + show_header + show_disclaimer + exit 0 + else + show_header + show_usage + echo "$0: error: unknown option '$1'" + exit 1 + fi +done + +show_header + +# print status function +pstatus() +{ + if [ "$opt_no_color" = 1 ]; then + _info_nol "$2" + else + case "$1" in + red) col="\033[101m\033[30m";; + green) col="\033[102m\033[30m";; + yellow) col="\033[103m\033[30m";; + blue) col="\033[104m\033[30m";; + *) col="";; + esac + _info_nol "$col $2 \033[0m" + fi + [ -n "$3" ] && _info_nol " ($3)" + _info +} + +# Print the final status of a vulnerability (incl. batch mode) +# Arguments are: CVE UNK/OK/VULN description +pvulnstatus() +{ + if [ "$opt_batch" = 1 ]; then + case "$opt_batch_format" in + text) _echo 0 "$1: $2 ($3)";; + nrpe) + case "$2" in + UKN) nrpe_unknown="1";; + VULN) nrpe_critical="1"; nrpe_vuln="$nrpe_vuln $1";; + esac + ;; + json) + case "$1" in + CVE-2017-5753) aka="SPECTRE VARIANT 1";; + CVE-2017-5715) aka="SPECTRE VARIANT 2";; + CVE-2017-5754) aka="MELTDOWN";; + esac + case "$2" in + UKN) is_vuln="unknown";; + VULN) is_vuln="true";; + OK) is_vuln="false";; + esac + json_output="${json_output:-[}{\"NAME\":\""$aka"\",\"CVE\":\""$1"\",\"VULNERABLE\":$is_vuln,\"INFOS\":\""$3"\"}," + ;; + esac + fi + + _info_nol "> \033[46m\033[30mSTATUS:\033[0m " + vulnstatus="$2" + shift 2 + case "$vulnstatus" in + UNK) pstatus yellow UNKNOWN "$@";; + VULN) pstatus red 'VULNERABLE' "$@";; + OK) pstatus green 'NOT VULNERABLE' "$@";; + esac +} + + +# The 3 below functions are taken from the extract-linux script, available here: +# https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux +# The functions have been modified for better integration to this script +# The original header of the file has been retained below + +# ---------------------------------------------------------------------- +# extract-vmlinux - Extract uncompressed vmlinux from a kernel image +# +# Inspired from extract-ikconfig +# (c) 2009,2010 Dick Streefland +# +# (c) 2011 Corentin Chary +# +# Licensed under the GNU General Public License, version 2 (GPLv2). +# ---------------------------------------------------------------------- + +vmlinux='' +vmlinux_err='' +check_vmlinux() +{ + readelf -h "$1" > /dev/null 2>&1 || return 1 + return 0 +} + +try_decompress() +{ + # The obscure use of the "tr" filter is to work around older versions of + # "grep" that report the byte offset of the line instead of the pattern. + + # Try to find the header ($1) and decompress from here + for pos in `tr "$1\n$2" "\n$2=" < "$6" | grep -abo "^$2"` + do + _debug "try_decompress: magic for $3 found at offset $pos" + if ! which "$3" >/dev/null 2>&1; then + vmlinux_err="missing '$3' tool, please install it, usually it's in the '$5' package" + return 0 + fi + pos=${pos%%:*} + tail -c+$pos "$6" 2>/dev/null | $3 $4 > $vmlinuxtmp 2>/dev/null + if check_vmlinux "$vmlinuxtmp"; then + vmlinux="$vmlinuxtmp" + _debug "try_decompress: decompressed with $3 successfully!" + return 0 + else + _debug "try_decompress: decompression with $3 did not work" + fi + done + return 1 +} + +extract_vmlinux() +{ + [ -n "$1" ] || return 1 + # Prepare temp files: + vmlinuxtmp="$(mktemp /tmp/vmlinux-XXXXXX)" + trap "rm -f $vmlinuxtmp" EXIT + + # Initial attempt for uncompressed images or objects: + if check_vmlinux "$1"; then + cat "$1" > "$vmlinuxtmp" + vmlinux=$vmlinuxtmp + return 0 + fi + + # That didn't work, so retry after decompression. + try_decompress '\037\213\010' xy gunzip '' gunzip "$1" && return 0 + try_decompress '\3757zXZ\000' abcde unxz '' xz-utils "$1" && return 0 + try_decompress 'BZh' xy bunzip2 '' bzip2 "$1" && return 0 + try_decompress '\135\0\0\0' xxx unlzma '' xz-utils "$1" && return 0 + try_decompress '\211\114\132' xy 'lzop' '-d' lzop "$1" && return 0 + try_decompress '\002\041\114\030' xyy 'lz4' '-d -l' liblz4-tool "$1" && return 0 + return 1 +} + +# end of extract-vmlinux functions + +# check for mode selection inconsistency +if [ "$opt_live_explicit" = 1 ]; then + if [ -n "$opt_kernel" -o -n "$opt_config" -o -n "$opt_map" ]; then + show_usage + echo "$0: error: incompatible modes specified, use either --live or --kernel/--config/--map" + exit 1 + fi +fi + +# root check (only for live mode, for offline mode, we already checked if we could read the files) + +if [ "$opt_live" = 1 ]; then + if [ "$(id -u)" -ne 0 ]; then + _warn "Note that you should launch this script with root privileges to get accurate information." + _warn "We'll proceed but you might see permission denied errors." + _warn "To run it as root, you can try the following command: sudo $0" + _warn + fi + _info "Checking for vulnerabilities against running kernel \033[35m"$(uname -s) $(uname -r) $(uname -v) $(uname -m)"\033[0m" + _info "CPU is\033[35m"$(grep '^model name' /proc/cpuinfo | cut -d: -f2 | head -1)"\033[0m" + + # try to find the image of the current running kernel + # first, look for the BOOT_IMAGE hint in the kernel cmdline + if [ -r /proc/cmdline ] && grep -q 'BOOT_IMAGE=' /proc/cmdline; then + opt_kernel=$(grep -Eo 'BOOT_IMAGE=[^ ]+' /proc/cmdline | cut -d= -f2) + _debug "found opt_kernel=$opt_kernel in /proc/cmdline" + # if we have a dedicated /boot partition, our bootloader might have just called it / + # so try to prepend /boot and see if we find anything + [ -e "/boot/$opt_kernel" ] && opt_kernel="/boot/$opt_kernel" + _debug "opt_kernel is now $opt_kernel" + # else, the full path is already there (most probably /boot/something) + fi + # if we didn't find a kernel, default to guessing + if [ ! -e "$opt_kernel" ]; then + [ -e /boot/vmlinuz-linux ] && opt_kernel=/boot/vmlinuz-linux + [ -e /boot/vmlinuz-linux-libre ] && opt_kernel=/boot/vmlinuz-linux-libre + [ -e /boot/vmlinuz-$(uname -r) ] && opt_kernel=/boot/vmlinuz-$(uname -r) + [ -e /boot/kernel-$( uname -r) ] && opt_kernel=/boot/kernel-$( uname -r) + [ -e /boot/bzImage-$(uname -r) ] && opt_kernel=/boot/bzImage-$(uname -r) + [ -e /boot/kernel-genkernel-$(uname -m)-$(uname -r) ] && opt_kernel=/boot/kernel-genkernel-$(uname -m)-$(uname -r) + fi + + # system.map + if [ -e /proc/kallsyms ] ; then + opt_map="/proc/kallsyms" + elif [ -e /boot/System.map-$(uname -r) ] ; then + opt_map=/boot/System.map-$(uname -r) + fi + + # config + if [ -e /proc/config.gz ] ; then + dumped_config="$(mktemp /tmp/config-XXXXXX)" + gunzip -c /proc/config.gz > $dumped_config + # dumped_config will be deleted at the end of the script + opt_config=$dumped_config + elif [ -e /boot/config-$(uname -r) ]; then + opt_config=/boot/config-$(uname -r) + fi +else + _info "Checking for vulnerabilities against specified kernel" +fi + +if [ -n "$opt_kernel" ]; then + _verbose "Will use vmlinux image \033[35m$opt_kernel\033[0m" +else + _verbose "Will use no vmlinux image (accuracy might be reduced)" + bad_accuracy=1 +fi +if [ -n "$dumped_config" ]; then + _verbose "Will use kconfig \033[35m/proc/config.gz\033[0m" +elif [ -n "$opt_config" ]; then + _verbose "Will use kconfig \033[35m$opt_config\033[0m" +else + _verbose "Will use no kconfig (accuracy might be reduced)" + bad_accuracy=1 +fi +if [ -n "$opt_map" ]; then + _verbose "Will use System.map file \033[35m$opt_map\033[0m" +else + _verbose "Will use no System.map file (accuracy might be reduced)" + bad_accuracy=1 +fi + +if [ "$bad_accuracy" = 1 ]; then + _info "We're missing some kernel info (see -v), accuracy might be reduced" +fi + +if [ -e "$opt_kernel" ]; then + if ! which readelf >/dev/null 2>&1; then + vmlinux_err="missing 'readelf' tool, please install it, usually it's in the 'binutils' package" + else + extract_vmlinux "$opt_kernel" + fi +else + vmlinux_err="couldn't find your kernel image in /boot, if you used netboot, this is normal" +fi +if [ -z "$vmlinux" -o ! -r "$vmlinux" ]; then + [ -z "$vmlinux_err" ] && vmlinux_err="couldn't extract your kernel from $opt_kernel" +fi + +_info + +# end of header stuff + +# now we define some util functions and the check_*() funcs, as +# the user can choose to execute only some of those + +mount_debugfs() +{ + if [ ! -e /sys/kernel/debug/sched_features ]; then + # try to mount the debugfs hierarchy ourselves and remember it to umount afterwards + mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null && mounted_debugfs=1 + fi +} + +umount_debugfs() +{ + if [ "$mounted_debugfs" = 1 ]; then + # umount debugfs if we did mount it ourselves + umount /sys/kernel/debug + fi +} + +sys_interface_check() +{ + [ "$opt_live" = 1 -a "$opt_no_sysfs" = 0 -a -r "$1" ] || return 1 + _info_nol "* Checking whether we're safe according to the /sys interface: " + if grep -qi '^not affected' "$1"; then + # Not affected + status=OK + pstatus green YES "kernel confirms that your CPU is unaffected" + elif grep -qi '^mitigation' "$1"; then + # Mitigation: PTI + status=OK + pstatus green YES "kernel confirms that the mitigation is active" + elif grep -qi '^vulnerable' "$1"; then + # Vulnerable + status=VULN + pstatus red NO "kernel confirms your system is vulnerable" + else + status=UNK + pstatus yellow UNKNOWN "unknown value reported by kernel" + fi + msg=$(cat "$1") + _debug "sys_interface_check: $1=$msg" + return 0 +} + +################### +# SPECTRE VARIANT 1 +check_variant1() +{ + _info "\033[1;34mCVE-2017-5753 [bounds check bypass] aka 'Spectre Variant 1'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v1"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + # no /sys interface (or offline mode), fallback to our own ways + _info_nol "* Checking count of LFENCE opcodes in kernel: " + if [ -n "$vmlinux_err" ]; then + msg="couldn't check ($vmlinux_err)" + status=UNK + pstatus yellow UNKNOWN + else + if ! which objdump >/dev/null 2>&1; then + msg="missing 'objdump' tool, please install it, usually it's in the binutils package" + status=UNK + pstatus yellow UNKNOWN + else + # here we disassemble the kernel and count the number of occurrences of the LFENCE opcode + # in non-patched kernels, this has been empirically determined as being around 40-50 + # in patched kernels, this is more around 70-80, sometimes way higher (100+) + # v0.13: 68 found in a 3.10.23-xxxx-std-ipv6-64 (with lots of modules compiled-in directly), which doesn't have the LFENCE patches, + # so let's push the threshold to 70. + nb_lfence=$(objdump -d "$vmlinux" | grep -wc lfence) + if [ "$nb_lfence" -lt 70 ]; then + msg="only $nb_lfence opcodes found, should be >= 70, heuristic to be improved when official patches become available" + status=VULN + pstatus red NO + else + msg="$nb_lfence opcodes found, which is >= 70, heuristic to be improved when official patches become available" + status=OK + pstatus green YES + fi + fi + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 1; then + # override status & msg in case CPU is not vulnerable after all + msg="your CPU vendor reported your CPU model as not vulnerable" + status=OK + fi + + # report status + pvulnstatus CVE-2017-5753 "$status" "$msg" +} + +################### +# SPECTRE VARIANT 2 +check_variant2() +{ + _info "\033[1;34mCVE-2017-5715 [branch target injection] aka 'Spectre Variant 2'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + _info "* Mitigation 1" + _info_nol "* Hardware (CPU microcode) support for mitigation: " + if [ ! -e /dev/cpu/0/msr ]; then + # try to load the module ourselves (and remember it so we can rmmod it afterwards) + modprobe msr 2>/dev/null && insmod_msr=1 + _debug "attempted to load module msr, ret=$insmod_msr" + fi + if [ ! -e /dev/cpu/0/msr ]; then + pstatus yellow UNKNOWN "couldn't read /dev/cpu/0/msr, is msr support enabled in your kernel?" + else + # the new MSR 'SPEC_CTRL' is at offset 0x48 + # here we use dd, it's the same as using 'rdmsr 0x48' but without needing the rdmsr tool + # if we get a read error, the MSR is not there + dd if=/dev/cpu/0/msr of=/dev/null bs=8 count=1 skip=9 2>/dev/null + if [ $? -eq 0 ]; then + pstatus green YES + else + pstatus red NO + fi + fi + + if [ "$insmod_msr" = 1 ]; then + # if we used modprobe ourselves, rmmod the module + rmmod msr 2>/dev/null + _debug "attempted to unload module msr, ret=$?" + fi + + _info_nol "* Kernel support for IBRS: " + if [ "$opt_live" = 1 ]; then + mount_debugfs + for ibrs_file in \ + /sys/kernel/debug/ibrs_enabled \ + /sys/kernel/debug/x86/ibrs_enabled \ + /proc/sys/kernel/ibrs_enabled; do + if [ -e "$ibrs_file" ]; then + # if the file is there, we have IBRS compiled-in + # /sys/kernel/debug/ibrs_enabled: vanilla + # /sys/kernel/debug/x86/ibrs_enabled: RedHat (see https://access.redhat.com/articles/3311301) + # /proc/sys/kernel/ibrs_enabled: OpenSUSE tumbleweed + pstatus green YES + ibrs_supported=1 + ibrs_enabled=$(cat "$ibrs_file" 2>/dev/null) + _debug "ibrs: found $ibrs_file=$ibrs_enabled" + break + else + _debug "ibrs: file $ibrs_file doesn't exist" + fi + done + fi + if [ "$ibrs_supported" != 1 -a -n "$opt_map" ]; then + if grep -q spec_ctrl "$opt_map"; then + pstatus green YES + ibrs_supported=1 + _debug "ibrs: found '*spec_ctrl*' symbol in $opt_map" + fi + fi + if [ "$ibrs_supported" != 1 ]; then + pstatus red NO + fi + + _info_nol "* IBRS enabled for Kernel space: " + if [ "$opt_live" = 1 ]; then + # 0 means disabled + # 1 is enabled only for kernel space + # 2 is enabled for kernel and user space + case "$ibrs_enabled" in + "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; + 0) pstatus red NO;; + 1 | 2) pstatus green YES;; + *) pstatus yellow UNKNOWN;; + esac + else + pstatus blue N/A "not testable in offline mode" + fi + + _info_nol "* IBRS enabled for User space: " + if [ "$opt_live" = 1 ]; then + case "$ibrs_enabled" in + "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; + 0 | 1) pstatus red NO;; + 2) pstatus green YES;; + *) pstatus yellow UNKNOWN;; + esac + else + pstatus blue N/A "not testable in offline mode" + fi + + _info "* Mitigation 2" + _info_nol "* Kernel compiled with retpoline option: " + # We check the RETPOLINE kernel options + if [ -r "$opt_config" ]; then + if grep -q '^CONFIG_RETPOLINE=y' "$opt_config"; then + pstatus green YES + retpoline=1 + _debug "retpoline: found "$(grep '^CONFIG_RETPOLINE' "$opt_config")" in $opt_config" + else + pstatus red NO + fi + else + pstatus yellow UNKNOWN "couldn't read your kernel configuration" + fi + + _info_nol "* Kernel compiled with a retpoline-aware compiler: " + # Now check if the compiler used to compile the kernel knows how to insert retpolines in generated asm + # For gcc, this is -mindirect-branch=thunk-extern (detected by the kernel makefiles) + # See gcc commit https://github.com/hjl-tools/gcc/commit/23b517d4a67c02d3ef80b6109218f2aadad7bd79 + # In latest retpoline LKML patches, the noretpoline_setup symbol exists only if CONFIG_RETPOLINE is set + # *AND* if the compiler is retpoline-compliant, so look for that symbol + if [ -n "$opt_map" ]; then + # look for the symbol + if grep -qw noretpoline_setup "$opt_map"; then + retpoline_compiler=1 + pstatus green YES "noretpoline_setup symbol found in System.map" + else + pstatus red NO + fi + elif [ -n "$vmlinux" ]; then + # look for the symbol + if which nm >/dev/null 2>&1; then + # the proper way: use nm and look for the symbol + if nm "$vmlinux" 2>/dev/null | grep -qw 'noretpoline_setup'; then + retpoline_compiler=1 + pstatus green YES "noretpoline_setup found in vmlinux symbols" + else + pstatus red NO + fi + elif grep -q noretpoline_setup "$vmlinux"; then + # if we don't have nm, nevermind, the symbol name is long enough to not have + # any false positive using good old grep directly on the binary + retpoline_compiler=1 + pstatus green YES "noretpoline_setup found in vmlinux" + else + pstatus red NO + fi + else + pstatus yellow UNKNOWN "couldn't find your kernel image or System.map" + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 2; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus CVE-2017-5715 OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ -z "$msg" ]; then + # if msg is empty, sysfs check didn't fill it, rely on our own test + if [ "$retpoline" = 1 -a "$retpoline_compiler" = 1 ]; then + pvulnstatus CVE-2017-5715 OK "retpoline mitigate the vulnerability" + elif [ "$opt_live" = 1 ]; then + if [ "$ibrs_enabled" = 1 -o "$ibrs_enabled" = 2 ]; then + pvulnstatus CVE-2017-5715 OK "IBRS mitigates the vulnerability" + else + pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" + fi + else + if [ "$ibrs_supported" = 1 ]; then + pvulnstatus CVE-2017-5715 OK "offline mode: IBRS will mitigate the vulnerability if enabled at runtime" + else + pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" + fi + fi + else + pvulnstatus CVE-2017-5715 "$status" "$msg" + fi +} + +######################## +# MELTDOWN aka VARIANT 3 +check_variant3() +{ + _info "\033[1;34mCVE-2017-5754 [rogue data cache load] aka 'Meltdown' aka 'Variant 3'\033[0m" + + status=UNK + sys_interface_available=0 + msg='' + if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/meltdown"; then + # this kernel has the /sys interface, trust it over everything + sys_interface_available=1 + else + _info_nol "* Kernel supports Page Table Isolation (PTI): " + kpti_support=0 + kpti_can_tell=0 + if [ -n "$opt_config" ]; then + kpti_can_tell=1 + if grep -Eq '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config"; then + _debug "kpti_support: found option "$(grep -E '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config")" in $opt_config" + kpti_support=1 + fi + fi + if [ "$kpti_support" = 0 -a -n "$opt_map" ]; then + # it's not an elif: some backports don't have the PTI config but still include the patch + # so we try to find an exported symbol that is part of the PTI patch in System.map + kpti_can_tell=1 + if grep -qw kpti_force_enabled "$opt_map"; then + _debug "kpti_support: found kpti_force_enabled in $opt_map" + kpti_support=1 + fi + fi + if [ "$kpti_support" = 0 -a -n "$vmlinux" ]; then + # same as above but in case we don't have System.map and only vmlinux, look for the + # nopti option that is part of the patch (kernel command line option) + kpti_can_tell=1 + if ! which strings >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing 'strings' tool, please install it, usually it's in the binutils package" + else + if strings "$vmlinux" | grep -qw nopti; then + _debug "kpti_support: found nopti string in $vmlinux" + kpti_support=1 + fi + fi + fi + + if [ "$kpti_support" = 1 ]; then + pstatus green YES + elif [ "$kpti_can_tell" = 1 ]; then + pstatus red NO + else + pstatus yellow UNKNOWN "couldn't read your kernel configuration nor System.map file" + fi + + mount_debugfs + _info_nol "* PTI enabled and active: " + if [ "$opt_live" = 1 ]; then + dmesg_grep="Kernel/User page tables isolation: enabled" + dmesg_grep="$dmesg_grep|Kernel page table isolation enabled" + dmesg_grep="$dmesg_grep|x86/pti: Unmapping kernel while in userspace" + if grep ^flags /proc/cpuinfo | grep -qw pti; then + # vanilla PTI patch sets the 'pti' flag in cpuinfo + _debug "kpti_enabled: found 'pti' flag in /proc/cpuinfo" + kpti_enabled=1 + elif grep ^flags /proc/cpuinfo | grep -qw kaiser; then + # kernel line 4.9 sets the 'kaiser' flag in cpuinfo + _debug "kpti_enabled: found 'kaiser' flag in /proc/cpuinfo" + kpti_enabled=1 + elif [ -e /sys/kernel/debug/x86/pti_enabled ]; then + # RedHat Backport creates a dedicated file, see https://access.redhat.com/articles/3311301 + kpti_enabled=$(cat /sys/kernel/debug/x86/pti_enabled 2>/dev/null) + _debug "kpti_enabled: file /sys/kernel/debug/x86/pti_enabled exists and says: $kpti_enabled" + elif dmesg | grep -Eq "$dmesg_grep"; then + # if we can't find the flag, grep dmesg output + _debug "kpti_enabled: found hint in dmesg: "$(dmesg | grep -E "$dmesg_grep") + kpti_enabled=1 + elif [ -r /var/log/dmesg ] && grep -Eq "$dmesg_grep" /var/log/dmesg; then + # if we can't find the flag in dmesg output, grep in /var/log/dmesg when readable + _debug "kpti_enabled: found hint in /var/log/dmesg: "$(grep -E "$dmesg_grep" /var/log/dmesg) + kpti_enabled=1 + else + _debug "kpti_enabled: couldn't find any hint that PTI is enabled" + kpti_enabled=0 + fi + if [ "$kpti_enabled" = 1 ]; then + pstatus green YES + else + pstatus red NO + fi + else + pstatus blue N/A "can't verify if PTI is enabled in offline mode" + fi + fi + + # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it + cve='CVE-2017-5754' + if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 3; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ -z "$msg" ]; then + # if msg is empty, sysfs check didn't fill it, rely on our own test + if [ "$opt_live" = 1 ]; then + if [ "$kpti_enabled" = 1 ]; then + pvulnstatus $cve OK "PTI mitigates the vulnerability" + else + pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + fi + else + if [ "$kpti_support" = 1 ]; then + pvulnstatus $cve OK "offline mode: PTI will mitigate the vulnerability if enabled at runtime" + else + pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + fi + fi + else + pvulnstatus $cve "$status" "$msg" + fi +} + +# now run the checks the user asked for +if [ "$opt_variant1" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant1 + _info +fi +if [ "$opt_variant2" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant2 + _info +fi +if [ "$opt_variant3" = 1 -o "$opt_allvariants" = 1 ]; then + check_variant3 + _info +fi + +_info "A false sense of security is worse than no security at all, see --disclaimer" + +# this'll umount only if we mounted debugfs ourselves +umount_debugfs + +# cleanup the temp decompressed config +[ -n "$dumped_config" ] && rm -f "$dumped_config" + +if [ "$opt_batch" = 1 -a "$opt_batch_format" = "nrpe" ]; then + if [ ! -z "$nrpe_vuln" ]; then + echo "Vulnerable:$nrpe_vuln" + else + echo "OK" + fi + [ "$nrpe_critical" = 1 ] && exit 2 # critical + [ "$nrpe_unknown" = 1 ] && exit 3 # unknown + exit 0 # ok +fi + +if [ "$opt_batch" = 1 -a "$opt_batch_format" = "json" ]; then + _echo 0 ${json_output%?}] +fi From 979123bf61a446feb9be4b55531e28cc5c60dbba Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 13 Jan 2018 23:01:29 +0100 Subject: [PATCH 0599/1066] [fix] other part of the code didn't expected this new datastructure --- src/yunohost/utils/packages.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 7df311406..d19e6d774 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -431,12 +431,15 @@ def get_installed_version(*pkgnames, **kwargs): raise UninstalledPackage(pkgname) repo = "" - versions[pkgname] = { - "version": version, - # when we don't have component it's because it's from a local - # install or from an image (like in vagrant) - "repo": repo if repo else "local", - } + if as_dict: + versions[pkgname] = { + "version": version, + # when we don't have component it's because it's from a local + # install or from an image (like in vagrant) + "repo": repo if repo else "local", + } + else: + versions[pkgname] = version if len(pkgnames) == 1 and not as_dict: return versions[pkgnames[0]] @@ -458,4 +461,5 @@ def ynh_packages_version(*args, **kwargs): """Return the version of each YunoHost package""" return get_installed_version( 'yunohost', 'yunohost-admin', 'moulinette', 'ssowat', + as_dict=True ) From 27262f0e4bfb6afbed6e54460a236cdd660a846c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Jan 2018 00:30:06 +0100 Subject: [PATCH 0600/1066] Fix english --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 5f5eb957f..8f83b709c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -101,7 +101,7 @@ "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", - "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably miss to remount or plug your hard dirve or usb key.", + "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", "backup_system_part_failed": "Unable to backup the '{part:s}' system part", From 6ccf054d5c2b265a37b4e495ec476ed9dff301ab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Jan 2018 00:40:44 +0100 Subject: [PATCH 0601/1066] Forgot to import errno --- src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 15f092b75..9cfd64842 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -4,6 +4,7 @@ import requests import base64 import time import json +import errno from moulinette import m18n from moulinette.core import MoulinetteError From d8435eaccc6627b02a2ded47f80a755ef5dbece9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Jan 2018 00:59:48 +0100 Subject: [PATCH 0602/1066] Add missing translation --- locales/en.json | 1 + src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index a2df29a54..ffb3233d4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,6 +217,7 @@ "migrate_tsig_wait_2": "2min...", "migrate_tsig_wait_3": "1min...", "migrate_tsig_wait_4": "30 secondes...", + "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 9cfd64842..5d495b06c 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -30,7 +30,7 @@ class MyMigration(Migration): (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) assert "+157" in private_key_path except (MoulinetteError, AssertionError): - logger.warning("migrate_tsig_not_needed") + logger.warning(m18n.n("migrate_tsig_not_needed")) return logger.warning(m18n.n('migrate_tsig_start', domain=domain)) From 4eeeb783af42ce41bbf65949d8ed24fe9d5fa8cd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Jan 2018 02:11:11 +0100 Subject: [PATCH 0603/1066] Keep only css and javascript for gzip types. --- data/templates/nginx/plain/global.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/global.conf b/data/templates/nginx/plain/global.conf index 341f08620..ca8721afb 100644 --- a/data/templates/nginx/plain/global.conf +++ b/data/templates/nginx/plain/global.conf @@ -1,2 +1,2 @@ server_tokens off; -gzip_types text/plain text/css application/javascript text/xml application/xml application/xml+rss text/javascript; +gzip_types text/css text/javascript application/javascript; From 235a2ed2b71189c13f332ef11b067c59b84c6036 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 14 Jan 2018 02:59:03 +0100 Subject: [PATCH 0604/1066] [fix] as_dict was used for manifest comparison --- src/yunohost/utils/packages.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index d19e6d774..3917ef563 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -406,6 +406,7 @@ def get_installed_version(*pkgnames, **kwargs): # Retrieve options as_dict = kwargs.get('as_dict', False) strict = kwargs.get('strict', False) + with_repo = kwargs.get('with_repo', False) for pkgname in pkgnames: try: @@ -431,7 +432,7 @@ def get_installed_version(*pkgnames, **kwargs): raise UninstalledPackage(pkgname) repo = "" - if as_dict: + if with_repo: versions[pkgname] = { "version": version, # when we don't have component it's because it's from a local @@ -461,5 +462,5 @@ def ynh_packages_version(*args, **kwargs): """Return the version of each YunoHost package""" return get_installed_version( 'yunohost', 'yunohost-admin', 'moulinette', 'ssowat', - as_dict=True + with_repo=True ) From e9c87da02830aecc1d0860164e1e0fa5217644a3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 15 Jan 2018 22:59:20 +0100 Subject: [PATCH 0605/1066] [fix] previous patch mistakely remove variable name --- data/helpers.d/string | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index 43c4aab1a..f708b31b1 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -24,7 +24,7 @@ ynh_replace_string () { local delimit=@ local match_string=$1 local replace_string=$2 - local workfile=$ + local workfile=$3 # Escape the delimiter if it's in the string. match_string=${match_string//${delimit}/"\\${delimit}"} From e1f2dd6a597e2bb342d9e34463ae18c988c37487 Mon Sep 17 00:00:00 2001 From: David B Date: Fri, 22 Dec 2017 14:16:48 +0000 Subject: [PATCH 0606/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 79.4% (282 of 355 strings) --- locales/es.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/locales/es.json b/locales/es.json index f1c125d02..eba43fac2 100644 --- a/locales/es.json +++ b/locales/es.json @@ -13,7 +13,7 @@ "app_install_files_invalid": "Los archivos de instalación no son válidos", "app_location_already_used": "Una aplicación ya está instalada en esta localización", "app_location_install_failed": "No se puede instalar la aplicación en esta localización", - "app_manifest_invalid": "El manifiesto de la aplicación no es válido", + "app_manifest_invalid": "El manifiesto de la aplicación no es válido: {error}", "app_no_upgrade": "No hay aplicaciones para actualizar", "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", "app_not_installed": "{app:s} 9 no está instalada", @@ -29,9 +29,9 @@ "app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación", "app_upgrade_failed": "No se pudo actualizar la aplicación {app:s}", "app_upgraded": "{app:s} ha sido actualizada", - "appslist_fetched": "Lista de aplicaciones ha sido descargada", - "appslist_removed": "La lista de aplicaciones ha sido eliminada", - "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones: {error}", + "appslist_fetched": "La lista de aplicaciones {appslist:s} ha sido descargada", + "appslist_removed": "La lista de aplicaciones {appslist:s} ha sido eliminada", + "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones {appslist:s} : {error}", "appslist_unknown": "Lista de aplicaciones desconocida", "ask_current_admin_password": "Contraseña administrativa actual", "ask_email": "Dirección de correo electrónico", @@ -272,7 +272,7 @@ "certmanager_acme_not_configured_for_domain": "El certificado para el dominio {domain:s} no parece instalado correctamente. Ejecute primero cert-install para este dominio.", "certmanager_http_check_timeout": "Plazo expirado, el servidor no ha podido contactarse a si mismo a través de HTTP usando su dirección IP pública (dominio {domain:s} con ip {ip:s}). Puede ser debido a hairpinning o a una mala configuración del cortafuego/router al que está conectado su servidor.", "certmanager_couldnt_fetch_intermediate_cert": "Plazo expirado, no se ha podido descargar el certificado intermedio de Let's Encrypt. La instalación/renovación del certificado ha sido cancelada - vuelva a intentarlo más tarde.", - "appslist_retrieve_bad_format": "El archivo recuperado no es una lista de aplicaciones válida", + "appslist_retrieve_bad_format": "El archivo obtenido para la lista de aplicaciones {appslist:s} no es válido", "domain_hostname_failed": "Error al establecer nuevo nombre de host", "yunohost_ca_creation_success": "Se ha creado la autoridad de certificación local.", "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. No se puede cambiar el URL únicamente mediante esta función. Compruebe si está disponible la opción 'app changeurl'.", @@ -283,10 +283,11 @@ "app_change_url_success": "El URL de la aplicación {app:s} ha sido cambiado correctamente a {domain:s} {path:s}", "app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada.", "app_already_up_to_date": "La aplicación {app:s} ya está actualizada", - "appslist_name_already_tracked": "Ya existe una lista de aplicaciones registrada con nombre {name:s}.", + "appslist_name_already_tracked": "Ya existe una lista de aplicaciones registrada con el nombre {name:s}.", "appslist_url_already_tracked": "Ya existe una lista de aplicaciones registrada con el URL {url:s}.", "appslist_migrating": "Migrando la lista de aplicaciones {applist:s} ...", "appslist_could_not_migrate": "No se pudo migrar la lista de aplicaciones {appslist:s}! No se pudo analizar el URL ... El antiguo cronjob se ha mantenido en {bkp_file:s}.", "appslist_corrupted_json": "No se pudieron cargar las listas de aplicaciones. Parece que {filename: s} está dañado.", - "invalid_url_format": "Formato de URL no válido" + "invalid_url_format": "Formato de URL no válido", + "app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones" } From f132031b38fe745eadfbfa15698f63fc1c98c2a5 Mon Sep 17 00:00:00 2001 From: Lapineige Date: Sun, 7 Jan 2018 19:06:07 +0000 Subject: [PATCH 0607/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (357 of 357 strings) --- locales/fr.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 8baccbd70..498e1f469 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -10,21 +10,21 @@ "app_argument_required": "Le paramètre « {name:s} » est requis", "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", "app_id_invalid": "Id d'application incorrect", - "app_incompatible": "L'application est incompatible avec votre version de YunoHost", + "app_incompatible": "L'application {app} est incompatible avec votre version de YunoHost", "app_install_files_invalid": "Fichiers d'installation incorrects", - "app_location_already_used": "Une application est déjà installée à cet emplacement", - "app_location_install_failed": "Impossible d'installer l'application à cet emplacement", + "app_location_already_used": "L'application '{app}' est déjà installée à cet emplacement ({path})", + "app_location_install_failed": "Impossible d'installer l'application à cet emplacement pour cause de conflit avec l'app '{other_app}' déjà installée sur '{other_path}'", "app_manifest_invalid": "Manifeste d'application incorrect : {error}", "app_no_upgrade": "Aucune application à mettre à jour", "app_not_correctly_installed": "{app:s} semble être mal installé", "app_not_installed": "{app:s} n'est pas installé", "app_not_properly_removed": "{app:s} n'a pas été supprimé correctement", - "app_package_need_update": "Le paquet de l'application doit être mis à jour pour suivre les changements de YunoHost", + "app_package_need_update": "Le paquet de l'application {app} doit être mis à jour pour suivre les changements de YunoHost", "app_recent_version_required": "{app:s} nécessite une version plus récente de YunoHost", "app_removed": "{app:s} a été supprimé", - "app_requirements_checking": "Vérification des paquets requis...", - "app_requirements_failed": "Impossible de satisfaire les pré-requis : {error}", - "app_requirements_unmeet": "Les pré-requis ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", + "app_requirements_checking": "Vérification des paquets requis pour {app}...", + "app_requirements_failed": "Impossible de satisfaire les pré-requis pour {app} : {error}", + "app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources", "app_unknown": "Application inconnue", "app_unsupported_remote_type": "Le type distant utilisé par l'application n'est pas supporté", @@ -367,5 +367,7 @@ "app_upgrade_some_app_failed": "Impossible de mettre à jour certaines applications", "ask_path": "Chemin", "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.", - "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}." + "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}.", + "app_make_default_location_already_used": "Impossible de configurer l'app '{app}' par défaut pour le domaine {domain}, déjà utilisé par l'autre app '{other_app}'", + "app_upgrade_app_name": "Mise à jour de l'application {app}..." } From eab5038edc48ab2b531bb37414407ada39e7ccb9 Mon Sep 17 00:00:00 2001 From: Lapineige Date: Sun, 7 Jan 2018 19:06:56 +0000 Subject: [PATCH 0608/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (358 of 358 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 498e1f469..00b98840c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -369,5 +369,6 @@ "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.", "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}.", "app_make_default_location_already_used": "Impossible de configurer l'app '{app}' par défaut pour le domaine {domain}, déjà utilisé par l'autre app '{other_app}'", - "app_upgrade_app_name": "Mise à jour de l'application {app}..." + "app_upgrade_app_name": "Mise à jour de l'application {app}...", + "backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier d’archives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas, vous avez probablement oublié de monter ou de connecter votre disque / clef USB." } From b23bc434f8d0beaa1e624b502783c1596a3b8ee8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 Jan 2018 18:49:31 +0100 Subject: [PATCH 0609/1066] Bigger depreciation / more explicit depreciation warning about checkurl... --- locales/en.json | 1 + src/yunohost/app.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/locales/en.json b/locales/en.json index 8f83b709c..ca03fbb17 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,6 +14,7 @@ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", + "app_checkurl_is_deprecated": "Packagers /!\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app {app} is incompatible with your YunoHost version", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9ccc0886d..0ee0dd7d3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1149,6 +1149,9 @@ def app_checkurl(auth, url, app=None): app -- Write domain & path to app settings for further checks """ + + logger.warning(m18n.n("app_checkurl_is_deprecated")) + from yunohost.domain import domain_list if "https://" == url[:8]: From eb38100265d1758938641b07989b994c2f093ada Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 Jan 2018 17:40:32 -0500 Subject: [PATCH 0610/1066] Update changelog for 2.7.6 release --- debian/changelog | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/debian/changelog b/debian/changelog index bd7430cb7..22188585a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,31 @@ +yunohost (2.7.6) testing; urgency=low + + Major changes: + + * [enh] Add new api entry point to check for Meltdown vulnerability + * [enh] New command 'app change-label' + + Misc fixes/improvements: + + * [helpers] Fix upgrade of fake package + * [helpers] Fix ynh_use_logrotate + * [helpers] Fix broken ynh_replace_string + * [helpers] Use local variables + * [enh/fix] Save the conf/ directory of app during installation and upgrade + * [enh] Improve UX for app messages + * [enh] Keep SSH sessions alive + * [enh] --version now display stable/testing/unstable information + * [enh] Backup: add ability to symlink the archives dir + * [enh] Add regen-conf messages, nginx -t and backports .deb to diagnosis output + * [fix] Comment line syntax for DNS zone recommendation (use ';') + * [fix] Fix a bug in disk diagnosis + * [mod] Use systemctl for all service operations + * [i18n] Improved Spanish and French translations + + Thanks to all contributors (Maniack, Josue, Bram, ljf, Aleks, Jocelyn, JimboeJoe, David B, Lapineige, ...) ! <3 + + -- Alexandre Aubin Tue, 16 Jan 2018 17:17:34 -0500 + yunohost (2.7.5) stable; urgency=low (Bumping version number for stable release) From 8a2434eb44514b3fa2bf2d5e069471960cda8414 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 17 Jan 2018 17:19:06 +0100 Subject: [PATCH 0611/1066] [fix] Nginx traversal issue --- data/templates/nginx/plain/yunohost_admin.conf.inc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf.inc b/data/templates/nginx/plain/yunohost_admin.conf.inc index b0ab4cef6..92e1e0ccf 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf.inc +++ b/data/templates/nginx/plain/yunohost_admin.conf.inc @@ -1,4 +1,6 @@ -location /yunohost/admin { +# Fix the nginx traversal weak ( #1034 ) +rewrite ^/yunohost/admin$ /yunohost/admin/ permanent; +location /yunohost/admin/ { alias /usr/share/yunohost/admin/; default_type text/html; index index.html; From 5cf6895ba20a34268b7d51a8bb2177549bd5809b Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 17 Jan 2018 17:22:13 +0100 Subject: [PATCH 0612/1066] [fix] Bad issue number in a comment --- data/templates/nginx/plain/yunohost_admin.conf.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf.inc b/data/templates/nginx/plain/yunohost_admin.conf.inc index 92e1e0ccf..f516a9d4b 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf.inc +++ b/data/templates/nginx/plain/yunohost_admin.conf.inc @@ -1,4 +1,4 @@ -# Fix the nginx traversal weak ( #1034 ) +# Fix the nginx traversal weak ( #1037 ) rewrite ^/yunohost/admin$ /yunohost/admin/ permanent; location /yunohost/admin/ { alias /usr/share/yunohost/admin/; From fcd587392730ad7e3e9b834ee332aafc74f40381 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 17 Jan 2018 18:17:38 +0100 Subject: [PATCH 0613/1066] [fix] Cron issue during custom backup --- src/yunohost/backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 0c957db7e..15c793802 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1543,9 +1543,13 @@ class BackupMethod(object): # Can create a hard link only if files are on the same fs # (i.e. we can't if it's on a different fs) if os.stat(src).st_dev == os.stat(dest_dir).st_dev: - os.link(src, dest) - # Success, go to next file to organize - continue + # Don't hardlink /etc/cron.d files to avoid cron bug + # 'NUMBER OF HARD LINKS > 1' see #1043 + cron_path = os.path.abspath('/etc/cron') + '.' + if not os.path.abspath(src).startswith(cron_path): + os.link(src, dest) + # Success, go to next file to organize + continue # If mountbind or hardlink couldnt be created, # prepare a list of files that need to be copied From 6078e42ed97863680a304a1a0800e7546afd6386 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 17 Jan 2018 18:59:34 +0100 Subject: [PATCH 0614/1066] [fix] wrap 'nginx -t' diagnosis call in try/catch --- src/yunohost/tools.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8b6e5cc44..c209254b8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -593,7 +593,12 @@ def tools_diagnosis(auth, private=False): } # nginx -t - diagnosis['nginx'] = check_output("nginx -t").strip().split("\n") + try: + diagnosis['nginx'] = check_output("nginx -t").strip().split("\n") + except Exception as e: + import traceback + traceback.print_exc() + logger.warning("Unable to check 'nginx -t', exception: %s" % e) # Services status services = service_status() From a58449ad51ff8aae85d6069033a99488894fccbb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 17 Jan 2018 18:59:48 +0100 Subject: [PATCH 0615/1066] [fix] wrap 'meltdown' diagnosis call in try/catch --- src/yunohost/tools.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index c209254b8..d6d288108 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -637,12 +637,17 @@ def tools_diagnosis(auth, private=False): diagnosis['private']['regen_conf'] = service_regen_conf(with_diff=True, dry_run=True) - diagnosis['security'] = { - "CVE-2017-5754": { - "name": "meltdown", - "vulnerable": _check_if_vulnerable_to_meltdown(), + try: + diagnosis['security'] = { + "CVE-2017-5754": { + "name": "meltdown", + "vulnerable": _check_if_vulnerable_to_meltdown(), + } } - } + except Exception as e: + import traceback + traceback.print_exc() + logger.warning("Unable to check for meltdown vulnerability: %s" % e) return diagnosis From 177914ee00e1e85cdea5c8afcee191c9b6f823b0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 17 Jan 2018 19:00:24 +0100 Subject: [PATCH 0616/1066] [fix] useful error messages for meltdown check --- src/yunohost/tools.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index d6d288108..dfc97222e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -663,11 +663,22 @@ def _check_if_vulnerable_to_meltdown(): # example output from the script: # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] try: - CVEs = json.loads(check_output("bash %s --batch json --variant 3" % SCRIPT_PATH)) + call = subprocess.Popen("bash %s --batch json --variant 3" % + SCRIPT_PATH, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + output, _ = call.communicate() + assert call.returncode == 0 + + CVEs = json.loads(output) assert len(CVEs) == 1 assert CVEs[0]["NAME"] == "MELTDOWN" - except: - raise Exception("Something wrong happened when trying to diagnose Meltdown vunerability.") + except Exception as e: + import traceback + traceback.print_exc() + logger.warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e) + raise Exception("Command output for failed meltdown check: '%s'" % output) return CVEs[0]["VULNERABLE"] From 54c9b8be10a9ed99e4492260fb340277cfa99c1c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 17 Jan 2018 19:00:35 +0100 Subject: [PATCH 0617/1066] [fix] we need absolute path for meltdown check --- src/yunohost/tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index dfc97222e..959da2df7 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -657,7 +657,8 @@ def _check_if_vulnerable_to_meltdown(): # script taken from https://github.com/speed47/spectre-meltdown-checker # script commit id is store directly in the script - SCRIPT_PATH = "./vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh" + file_dir = os.path.split(__file__)[0] + SCRIPT_PATH = os.path.join(file_dir, "./vendor/spectre-meltdown-checker/spectre-meltdown-checker.sh") # '--variant 3' corresponds to Meltdown # example output from the script: From 6e4b2673742a796d86731ab34b4a5d780db9fbca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 17 Jan 2018 13:12:12 -0500 Subject: [PATCH 0618/1066] Update changelog for 2.7.6.1 release --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 22188585a..eb1666fe5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (2.7.6.1) testing; urgency=low + + * [fix] Fix Meltdown diagnosis + * [fix] Improve error handling of 'nginx -t' and Metdown diagnosis + + -- Alexandre Aubin Wed, 17 Jan 2018 13:11:02 -0500 + yunohost (2.7.6) testing; urgency=low Major changes: From 6f2acb7eb6a9ebb54837bac017eecd512d49a32d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 18 Jan 2018 21:10:08 +0100 Subject: [PATCH 0619/1066] Wording --- data/templates/nginx/plain/yunohost_admin.conf.inc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf.inc b/data/templates/nginx/plain/yunohost_admin.conf.inc index f516a9d4b..2ab72293d 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf.inc +++ b/data/templates/nginx/plain/yunohost_admin.conf.inc @@ -1,5 +1,6 @@ -# Fix the nginx traversal weak ( #1037 ) +# Avoid the nginx path/alias traversal weakness ( #1037 ) rewrite ^/yunohost/admin$ /yunohost/admin/ permanent; + location /yunohost/admin/ { alias /usr/share/yunohost/admin/; default_type text/html; From 324ab9494b9b759375980121e03f7ea91a75f2fb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 18 Jan 2018 17:45:34 -0500 Subject: [PATCH 0620/1066] Update changelog for 2.7.7 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index eb1666fe5..599bf9cc8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.7.7) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Thu, 18 Jan 2018 17:45:21 -0500 + yunohost (2.7.6.1) testing; urgency=low * [fix] Fix Meltdown diagnosis From 8a51b46d4750cad047f8e98e6cd141db1959c78c Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 22 Jan 2018 19:06:37 +0100 Subject: [PATCH 0621/1066] [Fix] Fix ynh_restore_upgradebackup --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 030fddcde..3dc0c9bfc 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -64,7 +64,7 @@ ynh_backup_before_upgrade () { echo "This app doesn't have any backup script." >&2 return fi - local backup_number=1 + backup_number=1 local old_backup_number=2 local app_bck=${app//_/-} # Replace all '_' by '-' From 37a97cf9b5b6405a28c062774889d1976adcd679 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Wed, 24 Jan 2018 09:18:01 +0000 Subject: [PATCH 0622/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (366 of 366 strings) --- locales/fr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 00b98840c..dd9c40b34 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -370,5 +370,13 @@ "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}.", "app_make_default_location_already_used": "Impossible de configurer l'app '{app}' par défaut pour le domaine {domain}, déjà utilisé par l'autre app '{other_app}'", "app_upgrade_app_name": "Mise à jour de l'application {app}...", - "backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier d’archives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas, vous avez probablement oublié de monter ou de connecter votre disque / clef USB." + "backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier d’archives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas, vous avez probablement oublié de monter ou de connecter votre disque / clef USB.", + "migrate_tsig_end": "La migration à hmac-sha512 est terminée", + "migrate_tsig_failed": "La migration du domaine dyndns {domain} à hmac-sha512 a échoué, annulation des modifications. Erreur : {error_code} - {error}", + "migrate_tsig_start": "L’algorithme de génération des clefs n’est pas suffisamment sécurisé pour la signature TSIG du domaine « {domain} », lancement de la migration vers hmac-sha512 qui est plus sécurisé", + "migrate_tsig_wait": "Attendons 3 minutes pour que le serveur dyndns prenne en compte la nouvelle clef…", + "migrate_tsig_wait_2": "2 minutes…", + "migrate_tsig_wait_3": "1 minute…", + "migrate_tsig_wait_4": "30 secondes…", + "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine dyndns, donc aucune migration n’est nécessaire !" } From 1c752db22b74b4bb1771e78f9159e8abd8ce909f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Jan 2018 12:16:25 -0500 Subject: [PATCH 0623/1066] Update changelog for 2.7.8 release --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 599bf9cc8..8adb7004a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (2.7.8) testing; urgency=low + + * [fix] Use HMAC-SHA512 for DynDNS TSIG + * [fix] Fix ynh_restore_upgradebackup + * [i18n] Improve french translation + + Thanks to all contributors (Bram, Maniack, jibec, Aleks) ! <3 + + -- Alexandre Aubin Wed, 24 Jan 2018 12:15:12 -0500 + yunohost (2.7.7) stable; urgency=low (Bumping version number for stable release) From 4dfb1ee77703d579bc6070381b31b5fad601ece5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 03:19:22 +0100 Subject: [PATCH 0624/1066] Move get_public_ip to an 'util' file --- src/yunohost/domain.py | 38 --------------------------- src/yunohost/utils/network.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 src/yunohost/utils/network.py diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 727a63df3..19a5e55a7 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -30,8 +30,6 @@ import yaml import errno import requests -from urllib import urlopen - from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger @@ -260,42 +258,6 @@ def domain_url_available(auth, domain, path): return available -def get_public_ip(protocol=4): - """Retrieve the public IP address from ip.yunohost.org""" - if protocol == 4: - url = 'https://ip.yunohost.org' - elif protocol == 6: - url = 'https://ip6.yunohost.org' - else: - raise ValueError("invalid protocol version") - - try: - return urlopen(url).read().strip() - except IOError: - logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) - raise MoulinetteError(errno.ENETUNREACH, - m18n.n('no_internet_connection')) - -def get_public_ips(): - """ - Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org - - Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not - found. - """ - - try: - ipv4 = get_public_ip() - except: - ipv4 = None - try: - ipv6 = get_public_ip(6) - except: - ipv6 = None - - return (ipv4, ipv6) - - def _get_maindomain(): with open('/etc/yunohost/current_host', 'r') as f: maindomain = f.readline().rstrip() diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py new file mode 100644 index 000000000..902d99278 --- /dev/null +++ b/src/yunohost/utils/network.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2015 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import logging +from urllib import urlopen + +logger = logging.getLogger('yunohost.utils.network') + +def get_public_ip(protocol=4): + """Retrieve the public IP address from ip.yunohost.org""" + + if protocol == 4: + url = 'https://ip.yunohost.org' + elif protocol == 6: + url = 'https://ip6.yunohost.org' + else: + raise ValueError("invalid protocol version") + + try: + return urlopen(url).read().strip() + except IOError: + return None + + +def get_public_ips(): + + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) + + return (ipv4, ipv6) + From e80f3a5a55044346b1cdd352752c5f6d77105ca3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 03:39:35 +0100 Subject: [PATCH 0625/1066] Fix imports and get_public_ip usage --- src/yunohost/certificate.py | 14 +++++--------- src/yunohost/domain.py | 12 +++--------- src/yunohost/dyndns.py | 3 ++- src/yunohost/monitor.py | 8 +++----- src/yunohost/tools.py | 14 +++++--------- 5 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b6fb0e275..310c5d131 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -44,6 +44,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger import yunohost.domain +from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf @@ -809,7 +810,7 @@ def _backup_current_cert(domain): def _check_domain_is_ready_for_ACME(domain): - public_ip = yunohost.domain.get_public_ip() + public_ip = get_public_ip() # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): @@ -856,14 +857,9 @@ def _regen_dnsmasq_if_needed(): """ Update the dnsmasq conf if some IPs are not up to date... """ - try: - ipv4 = yunohost.domain.get_public_ip() - except: - ipv4 = None - try: - ipv6 = yunohost.domain.get_public_ip(6) - except: - ipv6 = None + + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) do_regen = False diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 19a5e55a7..026c4da36 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -37,6 +37,7 @@ from moulinette.utils.log import getActionLogger import yunohost.certificate from yunohost.service import service_regen_conf +from yunohost.utils.network import get_public_ip logger = getActionLogger('yunohost.domain') @@ -318,15 +319,8 @@ def _build_dns_conf(domain, ttl=3600): } """ - try: - ipv4 = get_public_ip() - except: - ipv4 = None - - try: - ipv6 = get_public_ip(6) - except: - ipv6 = None + ipv4 = get_public_ip() + ipv6 = get_public_ip(6) basic = [] diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 851d04f45..8c4efd777 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -39,7 +39,8 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, rm from moulinette.utils.network import download_json -from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf +from yunohost.domain import _get_maindomain, _build_dns_conf +from yunohost.utils.network import get_public_ips logger = getActionLogger('yunohost.dyndns') diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index d99ac1688..ed13d532d 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -41,7 +41,8 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.domain import get_public_ip, _get_maindomain +from yunohost.utils.network import get_public_ip +from yunohost.domain import _get_maindomain logger = getActionLogger('yunohost.monitor') @@ -210,10 +211,7 @@ def monitor_network(units=None, human_readable=False): else: logger.debug('interface name %s was not found', iname) elif u == 'infos': - try: - p_ipv4 = get_public_ip() - except: - p_ipv4 = 'unknown' + p_ipv4 = get_public_ip() or 'unknown' l_ip = 'unknown' for name, addrs in devices.items(): diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index b997961be..381cd07e0 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -45,12 +45,13 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron -from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain +from yunohost.domain import domain_add, domain_list, _get_maindomain, _set_maindomain from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.firewall import firewall_upnp from yunohost.service import service_status, service_regen_conf, service_log, service_start, service_enable from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version +from yunohost.utils.network import get_public_ip # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -621,16 +622,11 @@ def tools_diagnosis(auth, private=False): # Private data if private: diagnosis['private'] = OrderedDict() + # Public IP diagnosis['private']['public_ip'] = {} - try: - diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4) - except MoulinetteError as e: - pass - try: - diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6) - except MoulinetteError as e: - pass + diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4) + diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6) # Domains diagnosis['private']['domains'] = domain_list(auth)['domains'] From f5b5edb3bb598ff401a58714a48a1c8647b4ef9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 03:41:34 +0100 Subject: [PATCH 0626/1066] This get_public_ips isn't really relevant anymore --- src/yunohost/dyndns.py | 5 +++-- src/yunohost/utils/network.py | 8 -------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 8c4efd777..ec3bf88c8 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -40,7 +40,7 @@ from moulinette.utils.filesystem import read_file, write_to_file, rm from moulinette.utils.network import download_json from yunohost.domain import _get_maindomain, _build_dns_conf -from yunohost.utils.network import get_public_ips +from yunohost.utils.network import get_public_ip logger = getActionLogger('yunohost.dyndns') @@ -194,7 +194,8 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, old_ipv6 = read_file(OLD_IPV6_FILE).rstrip() # Get current IPv4 and IPv6 - (ipv4_, ipv6_) = get_public_ips() + ipv4_ = get_public_ip() + ipv6_ = get_public_ip(6) if ipv4 is None: ipv4 = ipv4_ diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 902d99278..9cdbc676c 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -38,11 +38,3 @@ def get_public_ip(protocol=4): except IOError: return None - -def get_public_ips(): - - ipv4 = get_public_ip() - ipv6 = get_public_ip(6) - - return (ipv4, ipv6) - From 9c4ddcca39d9d6d92bd5f9a23978337e48d0a4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Fri, 26 Jan 2018 21:17:18 +0100 Subject: [PATCH 0627/1066] Add service name as arg (optionnal) --- data/helpers.d/backend | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 8fef412cf..c04f2230b 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -64,6 +64,10 @@ ynh_remove_logrotate () { # Create a dedicated systemd config # +# usage: ynh_add_systemd_config [Service name] [Source file] +# | arg: Service name +# | arg: Systemd source file (for example appname.service) +# # This will use a template in ../conf/systemd.service # and will replace the following keywords with # global variables that should be defined before calling @@ -74,9 +78,11 @@ ynh_remove_logrotate () { # # usage: ynh_add_systemd_config ynh_add_systemd_config () { - finalsystemdconf="/etc/systemd/system/$app.service" + local service_name="${1:-$app}" + + finalsystemdconf="/etc/systemd/system/$service_name.service" ynh_backup_if_checksum_is_different "$finalsystemdconf" - sudo cp ../conf/systemd.service "$finalsystemdconf" + sudo cp ../conf/${2:-systemd.service} "$finalsystemdconf" # To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable. # Substitute in a nginx config file only if the variable is not empty @@ -89,19 +95,25 @@ ynh_add_systemd_config () { ynh_store_file_checksum "$finalsystemdconf" sudo chown root: "$finalsystemdconf" - sudo systemctl enable $app + sudo systemctl enable $service_name sudo systemctl daemon-reload } # Remove the dedicated systemd config # +# usage: ynh_remove_systemd_config [Service name] +# | arg: Service name +# # usage: ynh_remove_systemd_config ynh_remove_systemd_config () { - local finalsystemdconf="/etc/systemd/system/$app.service" + local service_name="${1:-$app}" + + local finalsystemdconf="/etc/systemd/system/$service_name.service" if [ -e "$finalsystemdconf" ]; then - sudo systemctl stop $app - sudo systemctl disable $app + sudo systemctl stop $service_name + sudo systemctl disable $service_name ynh_secure_remove "$finalsystemdconf" + sudo systemctl daemon-reload fi } From a975e5e6843c97dbacdc7401fa38d4d1a72922e7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 22:21:14 +0100 Subject: [PATCH 0628/1066] Improve comment / helper description --- data/helpers.d/backend | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c04f2230b..33d87db2c 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -64,13 +64,13 @@ ynh_remove_logrotate () { # Create a dedicated systemd config # -# usage: ynh_add_systemd_config [Service name] [Source file] -# | arg: Service name -# | arg: Systemd source file (for example appname.service) +# usage: ynh_add_systemd_config [Service name] [Template name] +# | arg: Service name (optionnal, $app by default) +# | arg: Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template) # -# This will use a template in ../conf/systemd.service -# and will replace the following keywords with -# global variables that should be defined before calling +# This will use the template ../conf/.service +# to generate a systemd config, by replacing the following keywords +# with global variables that should be defined before calling # this helper : # # __APP__ by $app @@ -102,7 +102,7 @@ ynh_add_systemd_config () { # Remove the dedicated systemd config # # usage: ynh_remove_systemd_config [Service name] -# | arg: Service name +# | arg: Service name (optionnal, $app by default) # # usage: ynh_remove_systemd_config ynh_remove_systemd_config () { From a1831ce0f8f0e18d0b1b831509c9727895f87e08 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 22:40:33 +0100 Subject: [PATCH 0629/1066] Manage etckeeper.conf to make etckeeper quiet --- data/hooks/conf_regen/01-yunohost | 3 ++ data/templates/yunohost/etckeeper.conf | 43 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 data/templates/yunohost/etckeeper.conf diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index f8bef0614..e1daa7c3d 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -53,6 +53,9 @@ do_pre_regen() { else sudo cp services.yml /etc/yunohost/services.yml fi + + mkdir -p "$pending_dir"/etc/etckeeper/ + cp etckeeper.conf "$pending_dir"/etc/etckeeper/ } _update_services() { diff --git a/data/templates/yunohost/etckeeper.conf b/data/templates/yunohost/etckeeper.conf new file mode 100644 index 000000000..2d11c3dc6 --- /dev/null +++ b/data/templates/yunohost/etckeeper.conf @@ -0,0 +1,43 @@ +# The VCS to use. +#VCS="hg" +VCS="git" +#VCS="bzr" +#VCS="darcs" + +# Options passed to git commit when run by etckeeper. +GIT_COMMIT_OPTIONS="--quiet" + +# Options passed to hg commit when run by etckeeper. +HG_COMMIT_OPTIONS="" + +# Options passed to bzr commit when run by etckeeper. +BZR_COMMIT_OPTIONS="" + +# Options passed to darcs record when run by etckeeper. +DARCS_COMMIT_OPTIONS="-a" + +# Uncomment to avoid etckeeper committing existing changes +# to /etc automatically once per day. +#AVOID_DAILY_AUTOCOMMITS=1 + +# Uncomment the following to avoid special file warning +# (the option is enabled automatically by cronjob regardless). +#AVOID_SPECIAL_FILE_WARNING=1 + +# Uncomment to avoid etckeeper committing existing changes to +# /etc before installation. It will cancel the installation, +# so you can commit the changes by hand. +#AVOID_COMMIT_BEFORE_INSTALL=1 + +# The high-level package manager that's being used. +# (apt, pacman-g2, yum, zypper etc) +HIGHLEVEL_PACKAGE_MANAGER=apt + +# The low-level package manager that's being used. +# (dpkg, rpm, pacman, pacman-g2, etc) +LOWLEVEL_PACKAGE_MANAGER=dpkg + +# To push each commit to a remote, put the name of the remote here. +# (eg, "origin" for git). Space-separated lists of multiple remotes +# also work (eg, "origin gitlab github" for git). +PUSH_REMOTE="" From 9511e01f5a8f577fed65c867b5dfe7c40eddb4c2 Mon Sep 17 00:00:00 2001 From: Jimmy Monin Date: Sat, 27 Jan 2018 16:16:42 +0100 Subject: [PATCH 0630/1066] Add access to conf folder when executing change_url script --- src/yunohost/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9ccc0886d..58c397542 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -485,6 +485,12 @@ def app_change_url(auth, app, domain, path): shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"), os.path.join(APP_TMP_FOLDER, "scripts")) + if os.path.exists(os.path.join(APP_TMP_FOLDER, "conf")): + shutil.rmtree(os.path.join(APP_TMP_FOLDER, "conf")) + + shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "conf"), + os.path.join(APP_TMP_FOLDER, "conf")) + # Execute App change_url script os.system('chown -R admin: %s' % INSTALL_TMP) os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) From 1056977dc6433da6ee41d7187706edbc090030de Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 27 Jan 2018 20:03:42 +0100 Subject: [PATCH 0631/1066] Microdecision: those weren't space, wtf --- data/helpers.d/mysql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 56741ec0e..8aa81a1fe 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -8,7 +8,7 @@ MYSQL_ROOT_PWD_FILE=/etc/yunohost/mysql # usage: ynh_mysql_connect_as user pwd [db] # | arg: user - the user name to connect as # | arg: pwd - the user password -# | arg: db - the database to connect to +# | arg: db - the database to connect to ynh_mysql_connect_as() { mysql -u "$1" --password="$2" -B "${3:-}" } @@ -17,7 +17,7 @@ ynh_mysql_connect_as() { # # usage: ynh_mysql_execute_as_root sql [db] # | arg: sql - the SQL command to execute -# | arg: db - the database to connect to +# | arg: db - the database to connect to ynh_mysql_execute_as_root() { ynh_mysql_connect_as "root" "$(sudo cat $MYSQL_ROOT_PWD_FILE)" \ "${2:-}" <<< "$1" @@ -27,7 +27,7 @@ ynh_mysql_execute_as_root() { # # usage: ynh_mysql_execute_file_as_root file [db] # | arg: file - the file containing SQL commands -# | arg: db - the database to connect to +# | arg: db - the database to connect to ynh_mysql_execute_file_as_root() { ynh_mysql_connect_as "root" "$(sudo cat $MYSQL_ROOT_PWD_FILE)" \ "${2:-}" < "$1" From e15ccdf0b749e89ebec2e659fd6af626f80105e2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 27 Jan 2018 20:06:16 +0100 Subject: [PATCH 0632/1066] Microdecison: more weird spaces --- data/helpers.d/user | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/user b/data/helpers.d/user index 8e214691c..47e6eb88a 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -1,4 +1,4 @@ -# Check if a YunoHost user exists +# Check if a YunoHost user exists # # example: ynh_user_exists 'toto' || exit 1 # @@ -31,7 +31,7 @@ ynh_user_list() { | awk '/^##username$/{getline; print}' } -# Check if a user exists on the system +# Check if a user exists on the system # # usage: ynh_system_user_exists username # | arg: username - the username to check From 50bd20fce9d0a716114d0165d222f9a90e8c3f0f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Lescher Date: Tue, 30 Jan 2018 17:16:46 +0100 Subject: [PATCH 0633/1066] [Fix] Stronger match for acme-challenge nginx location If an application (for instance roundcube) installed at the root of a subdomain has the following nginx configuration: location ~ ^/(.+/|)\. { deny all; } acme-challenge matching location: location '/.well-known/acme-challenge' { default_type "text/plain"; alias /tmp/acme-challenge-public/; } will not be used. This fix prevents further matching by regular expressions. Co-authored-by: Tomo59 --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b6fb0e275..2394f26d5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -463,7 +463,7 @@ def _configure_for_acme_challenge(auth, domain): nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder nginx_configuration = ''' -location '/.well-known/acme-challenge' +location ^~ '/.well-known/acme-challenge' { default_type "text/plain"; alias %s; From 61a60d0f5c6c0e95c7484cb12dbd72e13ab86db1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 30 Jan 2018 17:43:01 +0000 Subject: [PATCH 0634/1066] Update changelog for 2.7.9 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8adb7004a..4ebe4e14c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.7.9) stable; urgency=low + + (Bumping version number for stable release) + + -- Alexandre Aubin Tue, 30 Jan 2018 17:42:00 +0000 + yunohost (2.7.8) testing; urgency=low * [fix] Use HMAC-SHA512 for DynDNS TSIG From ebdc67e02ffaa8bf2b30abb82e0e01bea852ceff Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 4 Feb 2018 19:21:24 +0100 Subject: [PATCH 0635/1066] [enh] Allow to disable the backup during the upgrade --- data/helpers.d/utils | 70 ++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 3dc0c9bfc..db51578db 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -37,16 +37,22 @@ ynh_get_plain_key() { ynh_restore_upgradebackup () { echo "Upgrade failed." >&2 local app_bck=${app//_/-} # Replace all '_' by '-' - - # Check if an existing backup can be found before removing and restoring the application. - if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number - then - # Remove the application then restore it - sudo yunohost app remove $app - # Restore the backup - sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force - ynh_die "The app was restored to the way it was before the failed upgrade." - fi + NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} + + if [ "$NO_BACKUP_UPGRADE" -eq 0 ] + then + # Check if an existing backup can be found before removing and restoring the application. + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number + then + # Remove the application then restore it + sudo yunohost app remove $app + # Restore the backup + sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force + ynh_die "The app was restored to the way it was before the failed upgrade." + fi + else + echo "\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" >&2 + fi } # Make a backup in case of failed upgrade @@ -67,28 +73,34 @@ ynh_backup_before_upgrade () { backup_number=1 local old_backup_number=2 local app_bck=${app//_/-} # Replace all '_' by '-' + NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} - # Check if a backup already exists with the prefix 1 - if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1 - then - # Prefix becomes 2 to preserve the previous backup - backup_number=2 - old_backup_number=1 - fi - - # Create backup - sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number - if [ "$?" -eq 0 ] - then - # If the backup succeeded, remove the previous backup - if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number + if [ "$NO_BACKUP_UPGRADE" -eq 0 ] + then + # Check if a backup already exists with the prefix 1 + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade1 then - # Remove the previous backup only if it exists - sudo yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null + # Prefix becomes 2 to preserve the previous backup + backup_number=2 + old_backup_number=1 fi - else - ynh_die "Backup failed, the upgrade process was aborted." - fi + + # Create backup + sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number + if [ "$?" -eq 0 ] + then + # If the backup succeeded, remove the previous backup + if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$old_backup_number + then + # Remove the previous backup only if it exists + sudo yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null + fi + else + ynh_die "Backup failed, the upgrade process was aborted." + fi + else + echo "\$NO_BACKUP_UPGRADE is set, backup will be avoided. Be careful, this upgrade is going to be operated without securi$ + fi } # Download, check integrity, uncompress and patch the source from app.src From 8e92bb832867abcd3ebd6d5872bb99c2f8653835 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 4 Feb 2018 19:26:49 +0100 Subject: [PATCH 0636/1066] [enh] --verbose for backup during upgrade --- data/helpers.d/utils | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 3dc0c9bfc..288ac393c 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -44,7 +44,7 @@ ynh_restore_upgradebackup () { # Remove the application then restore it sudo yunohost app remove $app # Restore the backup - sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force + sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force --verbose ynh_die "The app was restored to the way it was before the failed upgrade." fi } @@ -77,7 +77,7 @@ ynh_backup_before_upgrade () { fi # Create backup - sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number + sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number --verbose if [ "$?" -eq 0 ] then # If the backup succeeded, remove the previous backup From e5a41be5182d87c5e9ab6bcf3ddd3d34dc4426f7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 7 Feb 2018 21:42:27 +0100 Subject: [PATCH 0637/1066] [fix] backslash needs to be double escaped --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 8f995c9bf..66fa93f45 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,7 +14,7 @@ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", - "app_checkurl_is_deprecated": "Packagers /!\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", + "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app {app} is incompatible with your YunoHost version", From 52a54c5ab1ce096005f086739efe600eef545d32 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 7 Feb 2018 21:46:38 +0100 Subject: [PATCH 0638/1066] [mod] we are in 2017 --- src/yunohost/utils/network.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 9cdbc676c..e22d1644d 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -2,7 +2,7 @@ """ License - Copyright (C) 2015 YUNOHOST.ORG + Copyright (C) 2017 YUNOHOST.ORG This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -37,4 +37,3 @@ def get_public_ip(protocol=4): return urlopen(url).read().strip() except IOError: return None - From 89848a9b57a8ae76810132df6db28ccab7ec6f36 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 7 Feb 2018 20:33:49 +0000 Subject: [PATCH 0639/1066] Added translation using Weblate (Arabic) --- locales/ar.json | 369 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 locales/ar.json diff --git a/locales/ar.json b/locales/ar.json new file mode 100644 index 000000000..8f995c9bf --- /dev/null +++ b/locales/ar.json @@ -0,0 +1,369 @@ +{ + "action_invalid": "Invalid action '{action:s}'", + "admin_password": "Administration password", + "admin_password_change_failed": "Unable to change password", + "admin_password_changed": "The administration password has been changed", + "app_already_installed": "{app:s} is already installed", + "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", + "app_already_up_to_date": "{app:s} is already up to date", + "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", + "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", + "app_argument_required": "Argument '{name:s}' is required", + "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", + "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", + "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", + "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", + "app_checkurl_is_deprecated": "Packagers /!\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", + "app_extraction_failed": "Unable to extract installation files", + "app_id_invalid": "Invalid app id", + "app_incompatible": "The app {app} is incompatible with your YunoHost version", + "app_install_files_invalid": "Invalid installation files", + "app_location_already_used": "The app '{app}' is already installed on that location ({path})", + "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", + "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", + "app_location_unavailable": "This url is not available or conflicts with an already installed app", + "app_manifest_invalid": "Invalid app manifest: {error}", + "app_no_upgrade": "No app to upgrade", + "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", + "app_not_installed": "{app:s} is not installed", + "app_not_properly_removed": "{app:s} has not been properly removed", + "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", + "app_removed": "{app:s} has been removed", + "app_requirements_checking": "Checking required packages for {app}...", + "app_requirements_failed": "Unable to meet requirements for {app}: {error}", + "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", + "app_sources_fetch_failed": "Unable to fetch sources files", + "app_unknown": "Unknown app", + "app_unsupported_remote_type": "Unsupported remote type used for the app", + "app_upgrade_app_name": "Upgrading app {app}...", + "app_upgrade_failed": "Unable to upgrade {app:s}", + "app_upgrade_some_app_failed": "Unable to upgrade some applications", + "app_upgraded": "{app:s} has been upgraded", + "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", + "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", + "appslist_fetched": "The application list {appslist:s} has been fetched", + "appslist_migrating": "Migrating application list {appslist:s} ...", + "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", + "appslist_removed": "The application list {appslist:s} has been removed", + "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", + "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", + "appslist_unknown": "Application list {appslist:s} unknown.", + "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", + "ask_current_admin_password": "Current administration password", + "ask_email": "Email address", + "ask_firstname": "First name", + "ask_lastname": "Last name", + "ask_list_to_remove": "List to remove", + "ask_main_domain": "Main domain", + "ask_new_admin_password": "New administration password", + "ask_password": "Password", + "ask_path": "Path", + "backup_abstract_method": "This backup method hasn't yet been implemented", + "backup_action_required": "You must specify something to save", + "backup_app_failed": "Unable to back up the app '{app:s}'", + "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", + "backup_applying_method_copy": "Copying all files to backup...", + "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", + "backup_applying_method_tar": "Creating the backup tar archive...", + "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", + "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", + "backup_archive_mount_failed": "Mounting the backup archive failed", + "backup_archive_name_exists": "The backup's archive name already exists", + "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", + "backup_archive_open_failed": "Unable to open the backup archive", + "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", + "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", + "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?", + "backup_borg_not_implemented": "Borg backup method is not yet implemented", + "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", + "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", + "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", + "backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.", + "backup_created": "Backup created", + "backup_creating_archive": "Creating the backup archive...", + "backup_creation_failed": "Backup creation failed", + "backup_csv_addition_failed": "Unable to add files to backup into the CSV file", + "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", + "backup_custom_backup_error": "Custom backup method failure on 'backup' step", + "backup_custom_mount_error": "Custom backup method failure on 'mount' step", + "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", + "backup_delete_error": "Unable to delete '{path:s}'", + "backup_deleted": "The backup has been deleted", + "backup_extracting_archive": "Extracting the backup archive...", + "backup_hook_unknown": "Backup hook '{hook:s}' unknown", + "backup_invalid_archive": "Invalid backup archive", + "backup_method_borg_finished": "Backup into borg finished", + "backup_method_copy_finished": "Backup copy finished", + "backup_method_custom_finished": "Custom backup method '{method:s}' finished", + "backup_method_tar_finished": "Backup tar archive created", + "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", + "backup_nothings_done": "There is nothing to save", + "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", + "backup_output_directory_not_empty": "The output directory is not empty", + "backup_output_directory_required": "You must provide an output directory for the backup", + "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", + "backup_running_app_script": "Running backup script of app '{app:s}'...", + "backup_running_hooks": "Running backup hooks...", + "backup_system_part_failed": "Unable to backup the '{part:s}' system part", + "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", + "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.", + "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.", + "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", + "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", + "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_signing_failed": "Signing the new certificate failed", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", + "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", + "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", + "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", + "custom_appslist_name_required": "You must provide a name for your custom app list", + "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", + "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}", + "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", + "diagnosis_monitor_network_error": "Can't monitor network: {error}", + "diagnosis_monitor_system_error": "Can't monitor system: {error}", + "diagnosis_no_apps": "No installed application", + "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", + "domain_cert_gen_failed": "Unable to generate certificate", + "domain_created": "The domain has been created", + "domain_creation_failed": "Unable to create domain", + "domain_deleted": "The domain has been deleted", + "domain_deletion_failed": "Unable to delete domain", + "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", + "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", + "domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}", + "domain_dyndns_invalid": "Invalid domain to use with DynDNS", + "domain_dyndns_root_unknown": "Unknown DynDNS root domain", + "domain_exists": "Domain already exists", + "domain_hostname_failed": "Failed to set new hostname", + "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", + "domain_unknown": "Unknown domain", + "domain_zone_exists": "DNS zone file already exists", + "domain_zone_not_found": "DNS zone file not found for domain {:s}", + "domains_available": "Available domains:", + "done": "Done", + "downloading": "Downloading...", + "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.", + "dyndns_cron_installed": "The DynDNS cron job has been installed", + "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", + "dyndns_cron_removed": "The DynDNS cron job has been removed", + "dyndns_ip_update_failed": "Unable to update IP address on DynDNS", + "dyndns_ip_updated": "Your IP address has been updated on DynDNS", + "dyndns_key_generating": "DNS key is being generated, it may take a while...", + "dyndns_key_not_found": "DNS key not found for the domain", + "dyndns_no_domain_registered": "No domain has been registered with DynDNS", + "dyndns_registered": "The DynDNS domain has been registered", + "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}", + "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.", + "dyndns_unavailable": "Domain {domain:s} is not available.", + "executing_command": "Executing command '{command:s}'...", + "executing_script": "Executing script '{script:s}'...", + "extracting": "Extracting...", + "field_invalid": "Invalid field '{:s}'", + "firewall_reload_failed": "Unable to reload the firewall", + "firewall_reloaded": "The firewall has been reloaded", + "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", + "format_datetime_short": "%m/%d/%Y %I:%M %p", + "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}", + "global_settings_cant_serialize_settings": "Failed to serialize settings data, reason: {reason:s}", + "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}", + "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'", + "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", + "global_settings_setting_example_bool": "Example boolean option", + "global_settings_setting_example_enum": "Example enum option", + "global_settings_setting_example_int": "Example int option", + "global_settings_setting_example_string": "Example string option", + "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", + "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "hook_exec_failed": "Script execution failed: {path:s}", + "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", + "hook_list_by_invalid": "Invalid property to list hook by", + "hook_name_unknown": "Unknown hook name '{name:s}'", + "installation_complete": "Installation complete", + "installation_failed": "Installation failed", + "invalid_url_format": "Invalid URL format", + "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", + "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", + "ldap_initialized": "LDAP has been initialized", + "license_undefined": "undefined", + "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", + "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", + "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", + "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", + "maindomain_change_failed": "Unable to change the main domain", + "maindomain_changed": "The main domain has been changed", + "migrate_tsig_end": "Migration to hmac-sha512 finished", + "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}", + "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512", + "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", + "migrate_tsig_wait_2": "2min...", + "migrate_tsig_wait_3": "1min...", + "migrate_tsig_wait_4": "30 secondes...", + "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", + "migrations_backward": "Migrating backward.", + "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", + "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", + "migrations_current_target": "Migration target is {}", + "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", + "migrations_forward": "Migrating forward", + "migrations_loading_migration": "Loading migration {number} {name}...", + "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", + "migrations_no_migrations_to_run": "No migrations to run", + "migrations_show_currently_running_migration": "Running migration {number} {name}...", + "migrations_show_last_migration": "Last ran migration is {}", + "migrations_skip_migration": "Skipping migration {number} {name}...", + "monitor_disabled": "The server monitoring has been disabled", + "monitor_enabled": "The server monitoring has been enabled", + "monitor_glances_con_failed": "Unable to connect to Glances server", + "monitor_not_enabled": "Server monitoring is not enabled", + "monitor_period_invalid": "Invalid time period", + "monitor_stats_file_not_found": "Statistics file not found", + "monitor_stats_no_update": "No monitoring statistics to update", + "monitor_stats_period_unavailable": "No available statistics for the period", + "mountpoint_unknown": "Unknown mountpoint", + "mysql_db_creation_failed": "MySQL database creation failed", + "mysql_db_init_failed": "MySQL database init failed", + "mysql_db_initialized": "The MySQL database has been initialized", + "network_check_mx_ko": "DNS MX record is not set", + "network_check_smtp_ko": "Outbound mail (SMTP port 25) seems to be blocked by your network", + "network_check_smtp_ok": "Outbound mail (SMTP port 25) is not blocked", + "new_domain_required": "You must provide the new main domain", + "no_appslist_found": "No app list found", + "no_internet_connection": "Server is not connected to the Internet", + "no_ipv6_connectivity": "IPv6 connectivity is not available", + "no_restore_script": "No restore script found for the app '{app:s}'", + "not_enough_disk_space": "Not enough free disk space on '{path:s}'", + "package_not_installed": "Package '{pkgname}' is not installed", + "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'", + "package_unknown": "Unknown package '{pkgname}'", + "packages_no_upgrade": "There is no package to upgrade", + "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", + "packages_upgrade_failed": "Unable to upgrade all of the packages", + "path_removal_failed": "Unable to remove path {:s}", + "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", + "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", + "pattern_email": "Must be a valid email address (e.g. someone@domain.org)", + "pattern_firstname": "Must be a valid first name", + "pattern_lastname": "Must be a valid last name", + "pattern_listname": "Must be alphanumeric and underscore characters only", + "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota", + "pattern_password": "Must be at least 3 characters long", + "pattern_port": "Must be a valid port number (i.e. 0-65535)", + "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", + "pattern_positive_number": "Must be a positive number", + "pattern_username": "Must be lower-case alphanumeric and underscore characters only", + "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections", + "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", + "port_available": "Port {port:d} is available", + "port_unavailable": "Port {port:d} is not available", + "restore_action_required": "You must specify something to restore", + "restore_already_installed_app": "An app is already installed with the id '{app:s}'", + "restore_app_failed": "Unable to restore the app '{app:s}'", + "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", + "restore_complete": "Restore complete", + "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", + "restore_extracting": "Extracting needed files from the archive...", + "restore_failed": "Unable to restore the system", + "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", + "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_mounting_archive": "Mounting archive into '{path:s}'", + "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_nothings_done": "Nothing has been restored", + "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", + "restore_running_app_script": "Running restore script of app '{app:s}'...", + "restore_running_hooks": "Running restoration hooks...", + "restore_system_part_failed": "Unable to restore the '{part:s}' system part", + "server_shutdown": "The server will shutdown", + "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", + "server_reboot": "The server will reboot", + "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", + "service_add_failed": "Unable to add service '{service:s}'", + "service_added": "The service '{service:s}' has been added", + "service_already_started": "Service '{service:s}' has already been started", + "service_already_stopped": "Service '{service:s}' has already been stopped", + "service_cmd_exec_failed": "Unable to execute command '{command:s}'", + "service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'", + "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'", + "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", + "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", + "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created", + "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", + "service_conf_file_removed": "The configuration file '{conf}' has been removed", + "service_conf_file_updated": "The configuration file '{conf}' has been updated", + "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", + "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", + "service_conf_updated": "The configuration has been updated for service '{service}'", + "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", + "service_disable_failed": "Unable to disable service '{service:s}'", + "service_disabled": "The service '{service:s}' has been disabled", + "service_enable_failed": "Unable to enable service '{service:s}'", + "service_enabled": "The service '{service:s}' has been enabled", + "service_no_log": "No log to display for service '{service:s}'", + "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", + "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}", + "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", + "service_remove_failed": "Unable to remove service '{service:s}'", + "service_removed": "The service '{service:s}' has been removed", + "service_start_failed": "Unable to start service '{service:s}'", + "service_started": "The service '{service:s}' has been started", + "service_status_failed": "Unable to determine status of service '{service:s}'", + "service_stop_failed": "Unable to stop service '{service:s}'", + "service_stopped": "The service '{service:s}' has been stopped", + "service_unknown": "Unknown service '{service:s}'", + "ssowat_conf_generated": "The SSOwat configuration has been generated", + "ssowat_conf_updated": "The SSOwat configuration has been updated", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "system_upgraded": "The system has been upgraded", + "system_username_exists": "Username already exists in the system users", + "unbackup_app": "App '{app:s}' will not be saved", + "unexpected_error": "An unexpected error occured", + "unit_unknown": "Unknown unit '{unit:s}'", + "unlimit": "No quota", + "unrestore_app": "App '{app:s}' will not be restored", + "update_cache_failed": "Unable to update APT cache", + "updating_apt_cache": "Updating the list of available packages...", + "upgrade_complete": "Upgrade complete", + "upgrading_packages": "Upgrading packages...", + "upnp_dev_not_found": "No UPnP device found", + "upnp_disabled": "UPnP has been disabled", + "upnp_enabled": "UPnP has been enabled", + "upnp_port_open_failed": "Unable to open UPnP ports", + "user_created": "The user has been created", + "user_creation_failed": "Unable to create user", + "user_deleted": "The user has been deleted", + "user_deletion_failed": "Unable to delete user", + "user_home_creation_failed": "Unable to create user home folder", + "user_info_failed": "Unable to retrieve user information", + "user_unknown": "Unknown user: {user:s}", + "user_update_failed": "Unable to update user", + "user_updated": "The user has been updated", + "yunohost_already_installed": "YunoHost is already installed", + "yunohost_ca_creation_failed": "Unable to create certificate authority", + "yunohost_ca_creation_success": "The local certification authority has been created.", + "yunohost_configured": "YunoHost has been configured", + "yunohost_installing": "Installing YunoHost...", + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" +} From d6311b62fe29667b957f71892299cf0331d8ffb0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 7 Feb 2018 22:04:35 +0100 Subject: [PATCH 0640/1066] [fix] double backslash again --- locales/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 8f995c9bf..66fa93f45 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -14,7 +14,7 @@ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", - "app_checkurl_is_deprecated": "Packagers /!\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", + "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app {app} is incompatible with your YunoHost version", From 78b87a6288bdf43442f80376a39623a9396bb1af Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Wed, 7 Feb 2018 22:09:41 +0100 Subject: [PATCH 0641/1066] reset file --- locales/ar.json | 367 ------------------------------------------------ 1 file changed, 367 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 66fa93f45..2c63c0851 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,369 +1,2 @@ { - "action_invalid": "Invalid action '{action:s}'", - "admin_password": "Administration password", - "admin_password_change_failed": "Unable to change password", - "admin_password_changed": "The administration password has been changed", - "app_already_installed": "{app:s} is already installed", - "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", - "app_already_up_to_date": "{app:s} is already up to date", - "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", - "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", - "app_argument_required": "Argument '{name:s}' is required", - "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", - "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", - "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", - "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", - "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", - "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", - "app_extraction_failed": "Unable to extract installation files", - "app_id_invalid": "Invalid app id", - "app_incompatible": "The app {app} is incompatible with your YunoHost version", - "app_install_files_invalid": "Invalid installation files", - "app_location_already_used": "The app '{app}' is already installed on that location ({path})", - "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", - "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", - "app_location_unavailable": "This url is not available or conflicts with an already installed app", - "app_manifest_invalid": "Invalid app manifest: {error}", - "app_no_upgrade": "No app to upgrade", - "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", - "app_not_installed": "{app:s} is not installed", - "app_not_properly_removed": "{app:s} has not been properly removed", - "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", - "app_removed": "{app:s} has been removed", - "app_requirements_checking": "Checking required packages for {app}...", - "app_requirements_failed": "Unable to meet requirements for {app}: {error}", - "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", - "app_sources_fetch_failed": "Unable to fetch sources files", - "app_unknown": "Unknown app", - "app_unsupported_remote_type": "Unsupported remote type used for the app", - "app_upgrade_app_name": "Upgrading app {app}...", - "app_upgrade_failed": "Unable to upgrade {app:s}", - "app_upgrade_some_app_failed": "Unable to upgrade some applications", - "app_upgraded": "{app:s} has been upgraded", - "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", - "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", - "appslist_fetched": "The application list {appslist:s} has been fetched", - "appslist_migrating": "Migrating application list {appslist:s} ...", - "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", - "appslist_removed": "The application list {appslist:s} has been removed", - "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", - "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", - "appslist_unknown": "Application list {appslist:s} unknown.", - "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", - "ask_current_admin_password": "Current administration password", - "ask_email": "Email address", - "ask_firstname": "First name", - "ask_lastname": "Last name", - "ask_list_to_remove": "List to remove", - "ask_main_domain": "Main domain", - "ask_new_admin_password": "New administration password", - "ask_password": "Password", - "ask_path": "Path", - "backup_abstract_method": "This backup method hasn't yet been implemented", - "backup_action_required": "You must specify something to save", - "backup_app_failed": "Unable to back up the app '{app:s}'", - "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", - "backup_applying_method_copy": "Copying all files to backup...", - "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", - "backup_applying_method_tar": "Creating the backup tar archive...", - "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", - "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", - "backup_archive_mount_failed": "Mounting the backup archive failed", - "backup_archive_name_exists": "The backup's archive name already exists", - "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", - "backup_archive_open_failed": "Unable to open the backup archive", - "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", - "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", - "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?", - "backup_borg_not_implemented": "Borg backup method is not yet implemented", - "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", - "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", - "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", - "backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.", - "backup_created": "Backup created", - "backup_creating_archive": "Creating the backup archive...", - "backup_creation_failed": "Backup creation failed", - "backup_csv_addition_failed": "Unable to add files to backup into the CSV file", - "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", - "backup_custom_backup_error": "Custom backup method failure on 'backup' step", - "backup_custom_mount_error": "Custom backup method failure on 'mount' step", - "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", - "backup_delete_error": "Unable to delete '{path:s}'", - "backup_deleted": "The backup has been deleted", - "backup_extracting_archive": "Extracting the backup archive...", - "backup_hook_unknown": "Backup hook '{hook:s}' unknown", - "backup_invalid_archive": "Invalid backup archive", - "backup_method_borg_finished": "Backup into borg finished", - "backup_method_copy_finished": "Backup copy finished", - "backup_method_custom_finished": "Custom backup method '{method:s}' finished", - "backup_method_tar_finished": "Backup tar archive created", - "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", - "backup_nothings_done": "There is nothing to save", - "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", - "backup_output_directory_not_empty": "The output directory is not empty", - "backup_output_directory_required": "You must provide an output directory for the backup", - "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", - "backup_running_app_script": "Running backup script of app '{app:s}'...", - "backup_running_hooks": "Running backup hooks...", - "backup_system_part_failed": "Unable to backup the '{part:s}' system part", - "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", - "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.", - "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.", - "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", - "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", - "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", - "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", - "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", - "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", - "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_cert_signing_failed": "Signing the new certificate failed", - "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", - "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", - "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", - "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", - "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", - "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_unknown": "Unknown domain {domain:s}", - "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", - "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", - "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", - "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", - "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", - "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", - "custom_appslist_name_required": "You must provide a name for your custom app list", - "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", - "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}", - "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", - "diagnosis_monitor_network_error": "Can't monitor network: {error}", - "diagnosis_monitor_system_error": "Can't monitor system: {error}", - "diagnosis_no_apps": "No installed application", - "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", - "domain_cert_gen_failed": "Unable to generate certificate", - "domain_created": "The domain has been created", - "domain_creation_failed": "Unable to create domain", - "domain_deleted": "The domain has been deleted", - "domain_deletion_failed": "Unable to delete domain", - "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", - "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", - "domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}", - "domain_dyndns_invalid": "Invalid domain to use with DynDNS", - "domain_dyndns_root_unknown": "Unknown DynDNS root domain", - "domain_exists": "Domain already exists", - "domain_hostname_failed": "Failed to set new hostname", - "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", - "domain_unknown": "Unknown domain", - "domain_zone_exists": "DNS zone file already exists", - "domain_zone_not_found": "DNS zone file not found for domain {:s}", - "domains_available": "Available domains:", - "done": "Done", - "downloading": "Downloading...", - "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.", - "dyndns_cron_installed": "The DynDNS cron job has been installed", - "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", - "dyndns_cron_removed": "The DynDNS cron job has been removed", - "dyndns_ip_update_failed": "Unable to update IP address on DynDNS", - "dyndns_ip_updated": "Your IP address has been updated on DynDNS", - "dyndns_key_generating": "DNS key is being generated, it may take a while...", - "dyndns_key_not_found": "DNS key not found for the domain", - "dyndns_no_domain_registered": "No domain has been registered with DynDNS", - "dyndns_registered": "The DynDNS domain has been registered", - "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}", - "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.", - "dyndns_unavailable": "Domain {domain:s} is not available.", - "executing_command": "Executing command '{command:s}'...", - "executing_script": "Executing script '{script:s}'...", - "extracting": "Extracting...", - "field_invalid": "Invalid field '{:s}'", - "firewall_reload_failed": "Unable to reload the firewall", - "firewall_reloaded": "The firewall has been reloaded", - "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", - "format_datetime_short": "%m/%d/%Y %I:%M %p", - "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}", - "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}", - "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}", - "global_settings_cant_serialize_settings": "Failed to serialize settings data, reason: {reason:s}", - "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}", - "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'", - "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", - "global_settings_setting_example_bool": "Example boolean option", - "global_settings_setting_example_enum": "Example enum option", - "global_settings_setting_example_int": "Example int option", - "global_settings_setting_example_string": "Example string option", - "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", - "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", - "hook_exec_failed": "Script execution failed: {path:s}", - "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", - "hook_list_by_invalid": "Invalid property to list hook by", - "hook_name_unknown": "Unknown hook name '{name:s}'", - "installation_complete": "Installation complete", - "installation_failed": "Installation failed", - "invalid_url_format": "Invalid URL format", - "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", - "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", - "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", - "ldap_initialized": "LDAP has been initialized", - "license_undefined": "undefined", - "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", - "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", - "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", - "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", - "maindomain_change_failed": "Unable to change the main domain", - "maindomain_changed": "The main domain has been changed", - "migrate_tsig_end": "Migration to hmac-sha512 finished", - "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}", - "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512", - "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", - "migrate_tsig_wait_2": "2min...", - "migrate_tsig_wait_3": "1min...", - "migrate_tsig_wait_4": "30 secondes...", - "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", - "migrations_backward": "Migrating backward.", - "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", - "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", - "migrations_current_target": "Migration target is {}", - "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", - "migrations_forward": "Migrating forward", - "migrations_loading_migration": "Loading migration {number} {name}...", - "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", - "migrations_no_migrations_to_run": "No migrations to run", - "migrations_show_currently_running_migration": "Running migration {number} {name}...", - "migrations_show_last_migration": "Last ran migration is {}", - "migrations_skip_migration": "Skipping migration {number} {name}...", - "monitor_disabled": "The server monitoring has been disabled", - "monitor_enabled": "The server monitoring has been enabled", - "monitor_glances_con_failed": "Unable to connect to Glances server", - "monitor_not_enabled": "Server monitoring is not enabled", - "monitor_period_invalid": "Invalid time period", - "monitor_stats_file_not_found": "Statistics file not found", - "monitor_stats_no_update": "No monitoring statistics to update", - "monitor_stats_period_unavailable": "No available statistics for the period", - "mountpoint_unknown": "Unknown mountpoint", - "mysql_db_creation_failed": "MySQL database creation failed", - "mysql_db_init_failed": "MySQL database init failed", - "mysql_db_initialized": "The MySQL database has been initialized", - "network_check_mx_ko": "DNS MX record is not set", - "network_check_smtp_ko": "Outbound mail (SMTP port 25) seems to be blocked by your network", - "network_check_smtp_ok": "Outbound mail (SMTP port 25) is not blocked", - "new_domain_required": "You must provide the new main domain", - "no_appslist_found": "No app list found", - "no_internet_connection": "Server is not connected to the Internet", - "no_ipv6_connectivity": "IPv6 connectivity is not available", - "no_restore_script": "No restore script found for the app '{app:s}'", - "not_enough_disk_space": "Not enough free disk space on '{path:s}'", - "package_not_installed": "Package '{pkgname}' is not installed", - "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'", - "package_unknown": "Unknown package '{pkgname}'", - "packages_no_upgrade": "There is no package to upgrade", - "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", - "packages_upgrade_failed": "Unable to upgrade all of the packages", - "path_removal_failed": "Unable to remove path {:s}", - "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", - "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", - "pattern_email": "Must be a valid email address (e.g. someone@domain.org)", - "pattern_firstname": "Must be a valid first name", - "pattern_lastname": "Must be a valid last name", - "pattern_listname": "Must be alphanumeric and underscore characters only", - "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota", - "pattern_password": "Must be at least 3 characters long", - "pattern_port": "Must be a valid port number (i.e. 0-65535)", - "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", - "pattern_positive_number": "Must be a positive number", - "pattern_username": "Must be lower-case alphanumeric and underscore characters only", - "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections", - "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", - "port_available": "Port {port:d} is available", - "port_unavailable": "Port {port:d} is not available", - "restore_action_required": "You must specify something to restore", - "restore_already_installed_app": "An app is already installed with the id '{app:s}'", - "restore_app_failed": "Unable to restore the app '{app:s}'", - "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", - "restore_complete": "Restore complete", - "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", - "restore_extracting": "Extracting needed files from the archive...", - "restore_failed": "Unable to restore the system", - "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", - "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", - "restore_mounting_archive": "Mounting archive into '{path:s}'", - "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", - "restore_nothings_done": "Nothing has been restored", - "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", - "restore_running_app_script": "Running restore script of app '{app:s}'...", - "restore_running_hooks": "Running restoration hooks...", - "restore_system_part_failed": "Unable to restore the '{part:s}' system part", - "server_shutdown": "The server will shutdown", - "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", - "server_reboot": "The server will reboot", - "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", - "service_add_failed": "Unable to add service '{service:s}'", - "service_added": "The service '{service:s}' has been added", - "service_already_started": "Service '{service:s}' has already been started", - "service_already_stopped": "Service '{service:s}' has already been stopped", - "service_cmd_exec_failed": "Unable to execute command '{command:s}'", - "service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'", - "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'", - "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", - "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", - "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created", - "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", - "service_conf_file_removed": "The configuration file '{conf}' has been removed", - "service_conf_file_updated": "The configuration file '{conf}' has been updated", - "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", - "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", - "service_conf_updated": "The configuration has been updated for service '{service}'", - "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", - "service_disable_failed": "Unable to disable service '{service:s}'", - "service_disabled": "The service '{service:s}' has been disabled", - "service_enable_failed": "Unable to enable service '{service:s}'", - "service_enabled": "The service '{service:s}' has been enabled", - "service_no_log": "No log to display for service '{service:s}'", - "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", - "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}", - "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", - "service_remove_failed": "Unable to remove service '{service:s}'", - "service_removed": "The service '{service:s}' has been removed", - "service_start_failed": "Unable to start service '{service:s}'", - "service_started": "The service '{service:s}' has been started", - "service_status_failed": "Unable to determine status of service '{service:s}'", - "service_stop_failed": "Unable to stop service '{service:s}'", - "service_stopped": "The service '{service:s}' has been stopped", - "service_unknown": "Unknown service '{service:s}'", - "ssowat_conf_generated": "The SSOwat configuration has been generated", - "ssowat_conf_updated": "The SSOwat configuration has been updated", - "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", - "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", - "system_upgraded": "The system has been upgraded", - "system_username_exists": "Username already exists in the system users", - "unbackup_app": "App '{app:s}' will not be saved", - "unexpected_error": "An unexpected error occured", - "unit_unknown": "Unknown unit '{unit:s}'", - "unlimit": "No quota", - "unrestore_app": "App '{app:s}' will not be restored", - "update_cache_failed": "Unable to update APT cache", - "updating_apt_cache": "Updating the list of available packages...", - "upgrade_complete": "Upgrade complete", - "upgrading_packages": "Upgrading packages...", - "upnp_dev_not_found": "No UPnP device found", - "upnp_disabled": "UPnP has been disabled", - "upnp_enabled": "UPnP has been enabled", - "upnp_port_open_failed": "Unable to open UPnP ports", - "user_created": "The user has been created", - "user_creation_failed": "Unable to create user", - "user_deleted": "The user has been deleted", - "user_deletion_failed": "Unable to delete user", - "user_home_creation_failed": "Unable to create user home folder", - "user_info_failed": "Unable to retrieve user information", - "user_unknown": "Unknown user: {user:s}", - "user_update_failed": "Unable to update user", - "user_updated": "The user has been updated", - "yunohost_already_installed": "YunoHost is already installed", - "yunohost_ca_creation_failed": "Unable to create certificate authority", - "yunohost_ca_creation_success": "The local certification authority has been created.", - "yunohost_configured": "YunoHost has been configured", - "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" } From c824f403a421e63ede69922c4bb931dad599c1fa Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 9 Feb 2018 16:10:31 +0100 Subject: [PATCH 0642/1066] [Fix] Referrer, CSP bad conf. cf. Another pr. --- data/templates/nginx/server.tpl.conf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 11f503c98..20301b2c1 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -43,8 +43,7 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header 'Referrer-Policy' 'same-origin'; - add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; + add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; From 4f616fe8c7a0d2de9f40b80b541a6cbd2b11eea2 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 9 Feb 2018 16:11:41 +0100 Subject: [PATCH 0643/1066] [Fix] CSP cf. another PR. --- data/templates/nginx/plain/yunohost_admin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index eedbd61b3..51424f289 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -39,7 +39,7 @@ server { add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; add_header 'Referrer-Policy' 'same-origin'; - add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';report-uri /csp-violation-report-endpoint/"; + add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; From 03273e3b946a74972185ad429fd9fe7ea49da474 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 9 Feb 2018 16:20:29 +0100 Subject: [PATCH 0644/1066] [fix] typo --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 20301b2c1..d5356fd6a 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -43,7 +43,7 @@ server { #ssl_dhparam /etc/ssl/private/dh2048.pem; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval';"; + add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; From 4276a187a05c01378cd4574f7a09bcff30a9f00f Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 9 Feb 2018 16:24:16 +0100 Subject: [PATCH 0645/1066] [enh] Comment with the URL of the Mozilla Directives --- data/templates/nginx/server.tpl.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index d5356fd6a..ac2ff8486 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -42,6 +42,9 @@ server { # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 #ssl_dhparam /etc/ssl/private/dh2048.pem; + # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners + # https://wiki.mozilla.org/Security/Guidelines/Web_Security + # https://observatory.mozilla.org/ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; From 6ab29260cf7669da78aa25fa088735b7f5f69846 Mon Sep 17 00:00:00 2001 From: frju365 Date: Fri, 9 Feb 2018 16:25:09 +0100 Subject: [PATCH 0646/1066] [enh] Mozilla directives. --- data/templates/nginx/plain/yunohost_admin.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 51424f289..156d61bd6 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -37,6 +37,9 @@ server { # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 #ssl_dhparam /etc/ssl/private/dh2048.pem; + # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners + # https://wiki.mozilla.org/Security/Guidelines/Web_Security + # https://observatory.mozilla.org/ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; add_header 'Referrer-Policy' 'same-origin'; add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'"; From f70949b35019f51be47bd4a7e3a9b106bc2ee3fe Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 11 Feb 2018 05:39:31 +0100 Subject: [PATCH 0647/1066] [fix] handle uncatched exception --- src/yunohost/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 381cd07e0..f98d48fc5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -33,8 +33,9 @@ import logging import subprocess import pwd import socket -from collections import OrderedDict +from xmlrpclib import Fault from importlib import import_module +from collections import OrderedDict import apt import apt.progress @@ -569,7 +570,7 @@ def tools_diagnosis(auth, private=False): diagnosis['system'] = OrderedDict() try: disks = monitor_disk(units=['filesystem'], human_readable=True) - except MoulinetteError as e: + except (MoulinetteError, Fault) as e: logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1) else: diagnosis['system']['disks'] = {} From 88d7a31bda1780b9cc81ff073092fc41629d58b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 11 Feb 2018 22:23:40 +0100 Subject: [PATCH 0648/1066] [fix] Microdecision : add mailutils as a dependency --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index dcdd0dd9a..c15c5eec5 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,7 @@ Depends: ${python:Depends}, ${misc:Depends} , ca-certificates, netcat-openbsd, iproute , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd - , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail + , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl From 79c7ba7615bc63a2acce648a8c18d8a976af102e Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 22 Feb 2018 11:52:55 +0100 Subject: [PATCH 0649/1066] Indentation fix... --- data/helpers.d/utils | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index d39bb78f5..81a76fdf3 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -38,10 +38,10 @@ ynh_restore_upgradebackup () { echo "Upgrade failed." >&2 local app_bck=${app//_/-} # Replace all '_' by '-' - NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} + NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0} - if [ "$NO_BACKUP_UPGRADE" -eq 0 ] - then + if [ "$NO_BACKUP_UPGRADE" -eq 0 ] + then # Check if an existing backup can be found before removing and restoring the application. if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number then @@ -51,9 +51,9 @@ ynh_restore_upgradebackup () { sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force --verbose ynh_die "The app was restored to the way it was before the failed upgrade." fi - else - echo "\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" >&2 - fi + else + echo "\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" >&2 + fi } # Make a backup in case of failed upgrade From 9a80635dbc33babd4da63eff4d2d157991f069e6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Feb 2018 08:54:11 -0500 Subject: [PATCH 0650/1066] [fix] Fail2ban conf/filter was not matching failed login attempts... --- data/templates/fail2ban/jail.conf | 3 ++- data/templates/fail2ban/yunohost.conf | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index d34763e48..648d44fa8 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -581,5 +581,6 @@ enabled = true port = http,https protocol = tcp filter = yunohost -logpath = /var/log/nginx*/*error.log +logpath = /var/log/nginx/*error.log + /var/log/nginx/*access.log maxretry = 6 diff --git a/data/templates/fail2ban/yunohost.conf b/data/templates/fail2ban/yunohost.conf index 3ca8f1c8f..a501c10ba 100644 --- a/data/templates/fail2ban/yunohost.conf +++ b/data/templates/fail2ban/yunohost.conf @@ -14,8 +14,8 @@ # (?:::f{4,6}:)?(?P[\w\-.^_]+) # Values: TEXT # -failregex = helpers.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: - ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22 +failregex = helpers.lua:[0-9]+: authenticate\(\): Connection failed for: .*, client: + ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 # Option: ignoreregex # Notes.: regex to ignore. If this regex matches, the line is ignored. From 89215f0402ba5502940abc7b5e22f0d8c2326dce Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Feb 2018 08:54:11 -0500 Subject: [PATCH 0651/1066] [fix] Fail2ban conf/filter was not matching failed login attempts... --- data/templates/fail2ban/jail.conf | 3 ++- data/templates/fail2ban/yunohost.conf | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index d34763e48..648d44fa8 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -581,5 +581,6 @@ enabled = true port = http,https protocol = tcp filter = yunohost -logpath = /var/log/nginx*/*error.log +logpath = /var/log/nginx/*error.log + /var/log/nginx/*access.log maxretry = 6 diff --git a/data/templates/fail2ban/yunohost.conf b/data/templates/fail2ban/yunohost.conf index 3ca8f1c8f..a501c10ba 100644 --- a/data/templates/fail2ban/yunohost.conf +++ b/data/templates/fail2ban/yunohost.conf @@ -14,8 +14,8 @@ # (?:::f{4,6}:)?(?P[\w\-.^_]+) # Values: TEXT # -failregex = helpers.lua:[1-9]+: authenticate\(\): Connection failed for: .*, client: - ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 22 +failregex = helpers.lua:[0-9]+: authenticate\(\): Connection failed for: .*, client: + ^ -.*\"POST /yunohost/api/login HTTP/1.1\" 401 # Option: ignoreregex # Notes.: regex to ignore. If this regex matches, the line is ignored. From 00481af499fea58f8ade0a8b65474442def326c8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 7 Mar 2018 12:44:15 +0000 Subject: [PATCH 0652/1066] Update changelog for 2.7.10 release --- debian/changelog | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 4ebe4e14c..989419109 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ +yunohost (2.7.10) stable; urgency=low + + * [fix] Fail2ban conf/filter was not matching failed login attempts... + + -- Alexandre Aubin Wed, 07 Mar 2018 12:43:35 +0000 + yunohost (2.7.9) stable; urgency=low (Bumping version number for stable release) - -- Alexandre Aubin Tue, 30 Jan 2018 17:42:00 +0000 + -- Alexandre Aubin Tue, 30 Jan 2018 17:42:00 +0000 yunohost (2.7.8) testing; urgency=low From 8652435d8a5e05975810991a45f25e42223fe182 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 8 Mar 2018 18:53:21 +0100 Subject: [PATCH 0653/1066] [fix] Remove warning from equivs --- data/helpers.d/package | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/helpers.d/package b/data/helpers.d/package index c616105d1..870147f2e 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -97,6 +97,10 @@ ynh_package_install_from_equivs () { # Build and install the package local TMPDIR=$(mktemp -d) + + # Force the compatibility level at 10, levels below are deprecated + echo 10 > /usr/share/equivs/template/debian/compat + # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg From 935b972d6e61a8966da862cdee4421ce8e5ffb19 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 9 Mar 2018 17:55:07 +0100 Subject: [PATCH 0654/1066] Misc fixes in the helpers to clean the autodoc --- data/helpers.d/backend | 17 ++++++++--------- data/helpers.d/ip | 10 +++++----- data/helpers.d/mysql | 4 ++-- data/helpers.d/system | 32 +++++++++++++++++--------------- 4 files changed, 32 insertions(+), 31 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index c2c626829..28c5b8e91 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -2,7 +2,7 @@ # # usage: ynh_use_logrotate [logfile] [--non-append] # | arg: logfile - absolute path of logfile -# | option: --non-append - Replace the config file instead of appending this new config. +# | arg: --non-append - (Option) Replace the config file instead of appending this new config. # # If no argument provided, a standard directory will be use. /var/log/${app} # You can provide a path with the directory only or with the logfile. @@ -64,9 +64,9 @@ ynh_remove_logrotate () { # Create a dedicated systemd config # -# usage: ynh_add_systemd_config [Service name] [Template name] -# | arg: Service name (optionnal, $app by default) -# | arg: Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template) +# usage: ynh_add_systemd_config [service] [template] +# | arg: service - Service name (optionnal, $app by default) +# | arg: template - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template) # # This will use the template ../conf/.service # to generate a systemd config, by replacing the following keywords @@ -76,7 +76,6 @@ ynh_remove_logrotate () { # __APP__ by $app # __FINALPATH__ by $final_path # -# usage: ynh_add_systemd_config ynh_add_systemd_config () { local service_name="${1:-$app}" @@ -101,10 +100,9 @@ ynh_add_systemd_config () { # Remove the dedicated systemd config # -# usage: ynh_remove_systemd_config [Service name] -# | arg: Service name (optionnal, $app by default) +# usage: ynh_remove_systemd_config [service] +# | arg: service - Service name (optionnal, $app by default) # -# usage: ynh_remove_systemd_config ynh_remove_systemd_config () { local service_name="${1:-$app}" @@ -119,6 +117,8 @@ ynh_remove_systemd_config () { # Create a dedicated nginx config # +# usage: ynh_add_nginx_config +# # This will use a template in ../conf/nginx.conf # __PATH__ by $path_url # __DOMAIN__ by $domain @@ -126,7 +126,6 @@ ynh_remove_systemd_config () { # __NAME__ by $app # __FINALPATH__ by $final_path # -# usage: ynh_add_nginx_config ynh_add_nginx_config () { finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" ynh_backup_if_checksum_is_different "$finalnginxconf" diff --git a/data/helpers.d/ip b/data/helpers.d/ip index 874675c9d..092cdff4b 100644 --- a/data/helpers.d/ip +++ b/data/helpers.d/ip @@ -1,10 +1,10 @@ # Validate an IP address # +# usage: ynh_validate_ip [family] [ip_address] +# | ret: 0 for valid ip addresses, 1 otherwise +# # example: ynh_validate_ip 4 111.222.333.444 # -# usage: ynh_validate_ip -# -# exit code : 0 for valid ip addresses, 1 otherwise ynh_validate_ip() { # http://stackoverflow.com/questions/319279/how-to-validate-ip-address-in-python#319298 @@ -31,8 +31,8 @@ EOF # example: ynh_validate_ip4 111.222.333.444 # # usage: ynh_validate_ip4 +# | ret: 0 for valid ipv4 addresses, 1 otherwise # -# exit code : 0 for valid ipv4 addresses, 1 otherwise ynh_validate_ip4() { ynh_validate_ip 4 $1 @@ -44,8 +44,8 @@ ynh_validate_ip4() # example: ynh_validate_ip6 2000:dead:beef::1 # # usage: ynh_validate_ip6 +# | ret: 0 for valid ipv6 addresses, 1 otherwise # -# exit code : 0 for valid ipv6 addresses, 1 otherwise ynh_validate_ip6() { ynh_validate_ip 6 $1 diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 8aa81a1fe..452a95eec 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -90,7 +90,7 @@ ynh_mysql_create_user() { # # usage: ynh_mysql_user_exists user # | arg: user - the user for which to check existence -function ynh_mysql_user_exists() +ynh_mysql_user_exists() { local user=$1 if [[ -z $(ynh_mysql_execute_as_root "SELECT User from mysql.user WHERE User = '$user';") ]] @@ -153,7 +153,7 @@ ynh_mysql_remove_db () { # Sanitize a string intended to be the name of a database # (More specifically : replace - and . by _) # -# Exemple: dbname=$(ynh_sanitize_dbid $app) +# example: dbname=$(ynh_sanitize_dbid $app) # # usage: ynh_sanitize_dbid name # | arg: name - name to correct/sanitize diff --git a/data/helpers.d/system b/data/helpers.d/system index f204c836a..dbd21a4ec 100644 --- a/data/helpers.d/system +++ b/data/helpers.d/system @@ -1,18 +1,17 @@ # Manage a fail of the script # -# Print a warning to inform that the script was failed -# Execute the ynh_clean_setup function if used in the app script -# -# usage of ynh_clean_setup function -# This function provide a way to clean some residual of installation that not managed by remove script. -# To use it, simply add in your script: +# usage: +# ynh_exit_properly is used only by the helper ynh_abort_if_errors. +# You should not use it directly. +# Instead, add to your script: # ynh_clean_setup () { # instructions... # } -# This function is optionnal. # -# Usage: ynh_exit_properly is used only by the helper ynh_abort_if_errors. -# You must not use it directly. +# This function provide a way to clean some residual of installation that not managed by remove script. +# +# It prints a warning to inform that the script was failed, and execute the ynh_clean_setup function if used in the app script +# ynh_exit_properly () { local exit_code=$? if [ "$exit_code" -eq 0 ]; then @@ -31,20 +30,23 @@ ynh_exit_properly () { ynh_die # Exit with error status } -# Exit if an error occurs during the execution of the script. +# Exits if an error occurs during the execution of the script. # -# Stop immediatly the execution if an error occured or if a empty variable is used. -# The execution of the script is derivate to ynh_exit_properly function before exit. +# usage: ynh_abort_if_errors +# +# This configure the rest of the script execution such that, if an error occurs +# or if an empty variable is used, the execution of the script stops +# immediately and a call to `ynh_exit_properly` is triggered. # -# Usage: ynh_abort_if_errors ynh_abort_if_errors () { set -eu # Exit if a command fail, and if a variable is used unset. trap ynh_exit_properly EXIT # Capturing exit signals on shell script } -# Return the Debian release codename (i.e. jessie, stretch, etc.) +# Fetch the Debian release codename # # usage: ynh_get_debian_release +# | ret: The Debian release codename (i.e. jessie, stretch, ...) ynh_get_debian_release () { echo $(lsb_release --codename --short) -} \ No newline at end of file +} From af22474a5068093f22b0074c01eaa784d29a0d5d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 10 Mar 2018 19:01:33 +0100 Subject: [PATCH 0655/1066] Add some [internal] flags in the comment to hide a few helpers from autodoc --- data/helpers.d/filesystem | 7 +++++++ data/helpers.d/mysql | 8 ++++++++ data/helpers.d/package | 4 ++++ data/helpers.d/print | 4 ++++ data/helpers.d/system | 5 ++++- 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index 6361d278e..6836aeebe 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -136,6 +136,8 @@ ynh_restore () { # Return the path in the archive where has been stocked the origin path # +# [internal] +# # usage: _get_archive_path ORIGIN_PATH _get_archive_path () { # For security reasons we use csv python library to read the CSV @@ -203,6 +205,9 @@ ynh_restore_file () { } # Deprecated helper since it's a dangerous one! +# +# [internal] +# ynh_bind_or_cp() { local AS_ROOT=${3:-0} local NO_ROOT=0 @@ -213,6 +218,8 @@ ynh_bind_or_cp() { # Create a directory under /tmp # +# [internal] +# # Deprecated helper # # usage: ynh_mkdir_tmp diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 452a95eec..7bc93fad5 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -35,6 +35,8 @@ ynh_mysql_execute_file_as_root() { # Create a database and grant optionnaly privilegies to a user # +# [internal] +# # usage: ynh_mysql_create_db db [user [pwd]] # | arg: db - the database name to create # | arg: user - the user to grant privilegies @@ -56,6 +58,8 @@ ynh_mysql_create_db() { # Drop a database # +# [internal] +# # If you intend to drop the database *and* the associated user, # consider using ynh_mysql_remove_db instead. # @@ -78,6 +82,8 @@ ynh_mysql_dump_db() { # Create a user # +# [internal] +# # usage: ynh_mysql_create_user user pwd [host] # | arg: user - the user name to create # | arg: pwd - the password to identify user by @@ -103,6 +109,8 @@ ynh_mysql_user_exists() # Drop a user # +# [internal] +# # usage: ynh_mysql_drop_user user # | arg: user - the user name to drop ynh_mysql_drop_user() { diff --git a/data/helpers.d/package b/data/helpers.d/package index c616105d1..4d147488c 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -26,6 +26,8 @@ ynh_package_version() { # APT wrapper for non-interactive operation # +# [internal] +# # usage: ynh_apt update ynh_apt() { DEBIAN_FRONTEND=noninteractive sudo apt-get -y -qq $@ @@ -73,6 +75,8 @@ ynh_package_autopurge() { # Build and install a package from an equivs control file # +# [internal] +# # example: generate an empty control file with `equivs-control`, adjust its # content and use helper to build and install the package: # ynh_package_install_from_equivs /path/to/controlfile diff --git a/data/helpers.d/print b/data/helpers.d/print index d9c8f1ec4..b13186d62 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -6,7 +6,11 @@ ynh_die() { } # Ignore the yunohost-cli log to prevent errors with conditionals commands +# +# [internal] +# # usage: ynh_no_log COMMAND +# # Simply duplicate the log, execute the yunohost command and replace the log without the result of this command # It's a very badly hack... ynh_no_log() { diff --git a/data/helpers.d/system b/data/helpers.d/system index dbd21a4ec..70cc57493 100644 --- a/data/helpers.d/system +++ b/data/helpers.d/system @@ -1,5 +1,7 @@ # Manage a fail of the script # +# [internal] +# # usage: # ynh_exit_properly is used only by the helper ynh_abort_if_errors. # You should not use it directly. @@ -36,7 +38,8 @@ ynh_exit_properly () { # # This configure the rest of the script execution such that, if an error occurs # or if an empty variable is used, the execution of the script stops -# immediately and a call to `ynh_exit_properly` is triggered. +# immediately and a call to `ynh_clean_setup` is triggered if it has been +# defined by your script. # ynh_abort_if_errors () { set -eu # Exit if a command fail, and if a variable is used unset. From bdf923aafcea2c0aacfdbb80e6cd4b1140ab6f51 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Thu, 15 Mar 2018 18:58:11 +0100 Subject: [PATCH 0656/1066] Solve issue with ynh_restore_file (#384) * Solve issue with ynh_restore_file While we use the `ynh_restore_file` if the destination already exist the `mv` fonction don't work correctly. By this commit the old directory is renamed. * Move to /home/yunohost.conf file if already exist in restoration * Remove if the file is bigger than 500Mo * Use a local variable * Fix DEST_PATH * Fix comment and typo * More precise comment regarding the behavior if DEST_PATH exists --- data/helpers.d/filesystem | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index d8e7dc4c7..d4146ad8f 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -175,6 +175,9 @@ with open(sys.argv[1], 'r') as backup_file: # the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in # the archive, the destination will be searched into backup.csv # +# If DEST_PATH already exists and is lighter than 500 Mo, a backup will be made in +# /home/yunohost.conf/backup/. Otherwise, the existing file is removed. +# # examples: # ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf" # # if apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf exists, restore it into @@ -196,6 +199,20 @@ ynh_restore_file () { ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" fi + # Move the old directory if it already exists + if [[ -e "${DEST_PATH}" ]] + then + # Check if the file/dir size is less than 500 Mo + if [[ $(du -sb ${DEST_PATH} | cut -d"/" -f1) -le "500000000" ]] + then + local backup_file="/home/yunohost.conf/backup/${DEST_PATH}.backup.$(date '+%Y%m%d.%H%M%S')" + mkdir -p "$(dirname "$backup_file")" + mv "${DEST_PATH}" "$backup_file" # Move the current file or directory + else + ynh_secure_remove ${DEST_PATH} + fi + fi + # Restore ORIGIN_PATH into DEST_PATH mkdir -p $(dirname "$DEST_PATH") From 020dea653190afc16f928e063c354511823fbaf4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 01:42:33 +0100 Subject: [PATCH 0657/1066] Change the way we list migrations (always load the module) and the way we manage its infos --- locales/en.json | 4 +-- src/yunohost/tools.py | 59 +++++++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/locales/en.json b/locales/en.json index 66fa93f45..f5f706d69 100644 --- a/locales/en.json +++ b/locales/en.json @@ -226,9 +226,9 @@ "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", "migrations_current_target": "Migration target is {}", - "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", + "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {migration_id}", "migrations_forward": "Migrating forward", - "migrations_loading_migration": "Loading migration {number} {name}...", + "migrations_loading_migration": "Loading migration {migration_id}...", "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", "migrations_no_migrations_to_run": "No migrations to run", "migrations_show_currently_running_migration": "Running migration {number} {name}...", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f98d48fc5..fcea5ffb3 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -741,11 +741,7 @@ def tools_migrations_list(): migrations = {"migrations": []} for migration in _get_migrations_list(): - migrations["migrations"].append({ - "number": int(migration.split("_", 1)[0]), - "name": migration.split("_", 1)[1], - "file_name": migration, - }) + migrations["migrations"].append(migration.infos()) return migrations @@ -905,55 +901,57 @@ def _get_migrations_list(): logger.warn(m18n.n('migrations_cant_reach_migration_file', migrations_path)) return migrations - for migration in filter(lambda x: re.match("^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)): - migrations.append(migration[:-len(".py")]) + for migration_file in filter(lambda x: re.match("^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)): + migrations.append(_load_migration(migration_file)) - return sorted(migrations) + return sorted(migrations, key=lambda m: m.id) -def _get_migration_by_name(migration_name, with_module=True): +def _get_migration_by_name(migration_name): """ Low-level / "private" function to find a migration by its name """ - migrations = tools_migrations_list()["migrations"] + try: + import data_migrations + except ImportError: + raise AssertionError("Unable to find migration with name %s" % migration_name) - matches = [ m for m in migrations if m["name"] == migration_name ] + migrations_path = data_migrations.__path__[0] + migrations_found = filter(lambda x: re.match("^\d+_%s\.py$" % migration_name, x), os.listdir(migrations_path)) - assert len(matches) == 1, "Unable to find migration with name %s" % migration_name + assert len(migrations_found) == 1, "Unable to find migration with name %s" % migration_name - migration = matches[0] - - if with_module: - migration["module"] = _get_migration_module(migration) - - return migration + return _load_migration(migrations_found[0]) -def _get_migration_module(migration): +def _load_migration(migration_file): + + migration_id = migration_file[:-len(".py")] logger.debug(m18n.n('migrations_loading_migration', - number=migration["number"], - name=migration["name"], + migration_id=migration_id, )) try: # this is python builtin method to import a module using a name, we # use that to import the migration as a python object so we'll be # able to run it in the next loop - return import_module("yunohost.data_migrations.{file_name}".format(**migration)) + module = import_module("yunohost.data_migrations.{}".format(migration_id)) + return module.MyMigration(migration_id) except Exception: import traceback traceback.print_exc() raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', - number=migration["number"], - name=migration["name"], + migration_id=migration_id, )) class Migration(object): + # forward() and backward() are to be implemented by daughter classes + def migrate(self): self.forward() @@ -962,3 +960,16 @@ class Migration(object): def backward(self): pass + + # The followings shouldn't be overriden + + def __init__(self, id_): + self.id = id_ + + def infos(self): + + return { + "id": self.id, + "number": int(self.id.split("_", 1)[0]), + "name": self.id.split("_", 1)[1], + } From 7f359e363b1bbced3984d13230c49f315857ba77 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 02:59:03 +0100 Subject: [PATCH 0658/1066] Add mode, description and optionnal disclaimer for migrations --- locales/en.json | 2 ++ src/yunohost/tools.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index f5f706d69..49376a1e6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -222,6 +222,8 @@ "migrate_tsig_wait_3": "1min...", "migrate_tsig_wait_4": "30 secondes...", "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", + "migration_description_0001_change_cert_group_to_sslcert": "Change certificates group permissions from 'metronome' to 'ssl-cert'", + "migration_description_0002_migrate_to_tsig_sha256": "Improve security of dyndns TSIG by using SHA512 instead of MD5", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index fcea5ffb3..f0e0315ca 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -950,10 +950,9 @@ def _load_migration(migration_file): class Migration(object): - # forward() and backward() are to be implemented by daughter classes + # Those are to be implemented by daughter classes - def migrate(self): - self.forward() + mode = "auto" def forward(self): raise NotImplementedError() @@ -961,15 +960,27 @@ class Migration(object): def backward(self): pass + def disclaimer(self): + return None + # The followings shouldn't be overriden + def migrate(self): + self.forward() + def __init__(self, id_): self.id = id_ + def description(self): + return m18n.n("migration_description_%s" % self.id) + def infos(self): return { "id": self.id, "number": int(self.id.split("_", 1)[0]), "name": self.id.split("_", 1)[1], + "mode": self.mode, + "description": self.description(), + "disclaimer": self.disclaimer() } From 0702af6054e386fae499cf955bd55a65b2f467f1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 17:08:52 +0100 Subject: [PATCH 0659/1066] Simplify my previous stuff about managing migration infos ? --- src/yunohost/tools.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f0e0315ca..107359469 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -741,7 +741,14 @@ def tools_migrations_list(): migrations = {"migrations": []} for migration in _get_migrations_list(): - migrations["migrations"].append(migration.infos()) + migrations["migrations"].append({ + "id": migration.id, + "number": migration.number, + "name": migration.name, + "mode": migration.mode, + "description": migration.description, + "disclaimer": migration.disclaimer + }) return migrations @@ -960,6 +967,7 @@ class Migration(object): def backward(self): pass + @property def disclaimer(self): return None @@ -970,17 +978,9 @@ class Migration(object): def __init__(self, id_): self.id = id_ + self.number = int(self.id.split("_", 1)[0]) + self.name = self.id.split("_", 1)[1] + @property def description(self): return m18n.n("migration_description_%s" % self.id) - - def infos(self): - - return { - "id": self.id, - "number": int(self.id.split("_", 1)[0]), - "name": self.id.split("_", 1)[1], - "mode": self.mode, - "description": self.description(), - "disclaimer": self.disclaimer() - } From d73197793c37fc8c45d21569be9265b67f01ef5a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 17:44:25 +0100 Subject: [PATCH 0660/1066] Adapt migrations_migrate according to previous changes --- src/yunohost/tools.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 107359469..1e49d1f00 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -770,26 +770,18 @@ def tools_migrations_migrate(target=None, skip=False): last_run_migration_number = state["last_run_migration"]["number"] if state["last_run_migration"] else 0 - migrations = [] - - # loading all migrations - for migration in tools_migrations_list()["migrations"]: - migrations.append({ - "number": migration["number"], - "name": migration["name"], - "module": _get_migration_module(migration), - }) - - migrations = sorted(migrations, key=lambda x: x["number"]) + # load all migrations + migrations = _get_migrations_list() + migrations = sorted(migrations, key=lambda x: x.number) if not migrations: logger.info(m18n.n('migrations_no_migrations_to_run')) return - all_migration_numbers = [x["number"] for x in migrations] + all_migration_numbers = [x.number for x in migrations] if target is None: - target = migrations[-1]["number"] + target = migrations[-1].number # validate input, target must be "0" or a valid number elif target != 0 and target not in all_migration_numbers: @@ -808,14 +800,14 @@ def tools_migrations_migrate(target=None, skip=False): if last_run_migration_number < target: logger.debug(m18n.n('migrations_forward')) # drop all already run migrations - migrations = filter(lambda x: target >= x["number"] > last_run_migration_number, migrations) + migrations = filter(lambda x: target >= x.number > last_run_migration_number, migrations) mode = "forward" # we need to go backward on already run migrations elif last_run_migration_number > target: logger.debug(m18n.n('migrations_backward')) # drop all not already run migrations - migrations = filter(lambda x: target < x["number"] <= last_run_migration_number, migrations) + migrations = filter(lambda x: target < x.number <= last_run_migration_number, migrations) mode = "backward" else: # can't happen, this case is handle before @@ -824,19 +816,24 @@ def tools_migrations_migrate(target=None, skip=False): # effectively run selected migrations for migration in migrations: if not skip: - logger.warn(m18n.n('migrations_show_currently_running_migration', **migration)) + logger.warn(m18n.n('migrations_show_currently_running_migration', + number=migration.number, name=migration.name)) try: if mode == "forward": - migration["module"].MyMigration().migrate() + migration.migrate() elif mode == "backward": - migration["module"].MyMigration().backward() + migration.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) except Exception as e: # migration failed, let's stop here but still update state because # we managed to run the previous ones - logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) + logger.error(m18n.n('migrations_migration_has_failed', + exception=e, + number=migration.number, + name=migration.name), + exc_info=1) break else: # if skip @@ -844,8 +841,8 @@ def tools_migrations_migrate(target=None, skip=False): # update the state to include the latest run migration state["last_run_migration"] = { - "number": migration["number"], - "name": migration["name"], + "number": migration.number, + "name": migration.name } # special case where we want to go back from the start From bafe6efde2f3b93523b4702facaf36b24d841ed3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 17:45:03 +0100 Subject: [PATCH 0661/1066] Typo --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 49376a1e6..4828c6f2a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -225,7 +225,7 @@ "migration_description_0001_change_cert_group_to_sslcert": "Change certificates group permissions from 'metronome' to 'ssl-cert'", "migration_description_0002_migrate_to_tsig_sha256": "Improve security of dyndns TSIG by using SHA512 instead of MD5", "migrations_backward": "Migrating backward.", - "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", + "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", "migrations_current_target": "Migration target is {}", "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {migration_id}", From c40f14e8f0a21e5ec663ba4bd8358899ed5cf921 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 17:49:46 +0100 Subject: [PATCH 0662/1066] Adapt 'manual' call to migration from dyndns according to previous changes --- src/yunohost/dyndns.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ec3bf88c8..60bea90e8 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -232,10 +232,13 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, from yunohost.tools import _get_migration_by_name migration = _get_migration_by_name("migrate_to_tsig_sha256") try: - migration["module"].MyMigration().migrate(dyn_host, domain, key) + migration.migrate(dyn_host, domain, key) except Exception as e: - logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) - + logger.error(m18n.n('migrations_migration_has_failed', + exception=e, + number=migration.number, + name=migration.name), + exc_info=1) return # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' From c568b04459422bea7d0e8a92a2b33bbfedec7ac3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 1 Feb 2018 21:11:12 +0100 Subject: [PATCH 0663/1066] Manage the auto/manual flag in migrations_migrate --- data/actionsmap/yunohost.yml | 3 +++ debian/postinst | 2 +- locales/en.json | 1 + src/yunohost/tools.py | 16 +++++++++++++--- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 77887b41a..d84d308c0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1589,6 +1589,9 @@ tools: help: skip the migration(s), use it only if you know what you are doing full: --skip action: store_true + --auto: + help: automatic mode, won't run manual migrations, use it only if you know what you are doing + action: store_true ### tools_migrations_state() diff --git a/debian/postinst b/debian/postinst index 7e91ffbb3..5b6ed8259 100644 --- a/debian/postinst +++ b/debian/postinst @@ -15,7 +15,7 @@ do_configure() { yunohost service regen-conf --output-as none echo "Launching migrations.." - yunohost tools migrations migrate + yunohost tools migrations migrate --auto # restart yunohost-firewall if it's running service yunohost-firewall status >/dev/null \ diff --git a/locales/en.json b/locales/en.json index 4828c6f2a..7597e17e4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -236,6 +236,7 @@ "migrations_show_currently_running_migration": "Running migration {number} {name}...", "migrations_show_last_migration": "Last ran migration is {}", "migrations_skip_migration": "Skipping migration {number} {name}...", + "migrations_to_be_ran_manually": "Migration {number} {name} has to be ran manually. Please go to Tools > Migrations on the webadmin, or run `yunohost tools migrations migrate`.", "monitor_disabled": "The server monitoring has been disabled", "monitor_enabled": "The server monitoring has been enabled", "monitor_glances_con_failed": "Unable to connect to Glances server", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 1e49d1f00..0e2e1195c 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -753,7 +753,7 @@ def tools_migrations_list(): return migrations -def tools_migrations_migrate(target=None, skip=False): +def tools_migrations_migrate(target=None, skip=False, auto=False): """ Perform migrations """ @@ -816,8 +816,18 @@ def tools_migrations_migrate(target=None, skip=False): # effectively run selected migrations for migration in migrations: if not skip: - logger.warn(m18n.n('migrations_show_currently_running_migration', - number=migration.number, name=migration.name)) + + # If we are migrating in "automatic mode" (i.e. from debian + # configure during an upgrade of the package) but the migration + # is to be ran manually by the user + if auto and migration.mode == "manual": + logger.warn(m18n.n('migrations_to_be_ran_manually', + number=migration.number, name=migration.name)) + break + else: + logger.warn(m18n.n('migrations_show_currently_running_migration', + number=migration.number, name=migration.name)) + try: if mode == "forward": From c266147fd9ce4654c6b0b0766aa45eff21c4a71d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 3 Feb 2018 03:27:13 +0100 Subject: [PATCH 0664/1066] Be able to list only pending or done migrations --- data/actionsmap/yunohost.yml | 7 +++++++ locales/en.json | 1 + src/yunohost/tools.py | 36 ++++++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index d84d308c0..8eba45306 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1575,6 +1575,13 @@ tools: list: action_help: List migrations api: GET /migrations + arguments: + --pending: + help: list only pending migrations + action: store_true + --done: + help: list only migrations already performed + action: store_true ### tools_migrations_migrate() migrate: diff --git a/locales/en.json b/locales/en.json index 7597e17e4..e5034cf81 100644 --- a/locales/en.json +++ b/locales/en.json @@ -230,6 +230,7 @@ "migrations_current_target": "Migration target is {}", "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {migration_id}", "migrations_forward": "Migrating forward", + "migrations_list_conflict_pending_done": "You cannot use both --previous and --done at the same time.", "migrations_loading_migration": "Loading migration {migration_id}...", "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", "migrations_no_migrations_to_run": "No migrations to run", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 0e2e1195c..0978956fd 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -733,24 +733,36 @@ def tools_reboot(force=False): subprocess.check_call(['systemctl', 'reboot']) -def tools_migrations_list(): +def tools_migrations_list(pending=False, done=False): """ List existing migrations """ - migrations = {"migrations": []} + # Check for option conflict + if pending and done: + raise MoulinetteError(errno.EINVAL, m18n.n("migrations_list_conflict_pending_done")) - for migration in _get_migrations_list(): - migrations["migrations"].append({ - "id": migration.id, - "number": migration.number, - "name": migration.name, - "mode": migration.mode, - "description": migration.description, - "disclaimer": migration.disclaimer - }) + # Get all migrations + migrations = _get_migrations_list() - return migrations + # If asked, filter pending or done migrations + if pending or done: + last_migration = tools_migrations_state()["last_run_migration"] + last_migration = last_migration["number"] if last_migration else -1 + if done: + migrations = [m for m in migrations if m.number <= last_migration] + if pending: + migrations = [m for m in migrations if m.number > last_migration] + + # Reduce to dictionnaries + migrations = [{ "id": migration.id, + "number": migration.number, + "name": migration.name, + "mode": migration.mode, + "description": migration.description, + "disclaimer": migration.disclaimer } for migration in migrations ] + + return {"migrations": migrations} def tools_migrations_migrate(target=None, skip=False, auto=False): From c8b1d7e2c3a830d4a242a251304ed7d13f241093 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 5 Feb 2018 15:23:42 +0100 Subject: [PATCH 0665/1066] Forgot to adapt this also.. --- src/yunohost/tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 0978956fd..2fd139318 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -859,7 +859,9 @@ def tools_migrations_migrate(target=None, skip=False, auto=False): break else: # if skip - logger.warn(m18n.n('migrations_skip_migration', **migration)) + logger.warn(m18n.n('migrations_skip_migration', + number=migration.number, + name=migration.name)) # update the state to include the latest run migration state["last_run_migration"] = { From 9009b3f9d35b6d2945926e0b144c553bf4af4850 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 5 Feb 2018 18:13:51 +0100 Subject: [PATCH 0666/1066] Handle disclaimers --- data/actionsmap/yunohost.yml | 4 +++- locales/en.json | 1 + src/yunohost/tools.py | 37 ++++++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8eba45306..715a13504 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1599,7 +1599,9 @@ tools: --auto: help: automatic mode, won't run manual migrations, use it only if you know what you are doing action: store_true - + --accept-disclaimer: + help: accept disclaimers of migration (please read them before using this option) + action: store_true ### tools_migrations_state() state: diff --git a/locales/en.json b/locales/en.json index e5034cf81..b0c2ea3d2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -238,6 +238,7 @@ "migrations_show_last_migration": "Last ran migration is {}", "migrations_skip_migration": "Skipping migration {number} {name}...", "migrations_to_be_ran_manually": "Migration {number} {name} has to be ran manually. Please go to Tools > Migrations on the webadmin, or run `yunohost tools migrations migrate`.", + "migrations_need_to_accept_disclaimer": "To run the migration {number} {name}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option --accept-disclaimer.", "monitor_disabled": "The server monitoring has been disabled", "monitor_enabled": "The server monitoring has been enabled", "monitor_glances_con_failed": "Unable to connect to Glances server", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 2fd139318..9719f9223 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -765,7 +765,7 @@ def tools_migrations_list(pending=False, done=False): return {"migrations": migrations} -def tools_migrations_migrate(target=None, skip=False, auto=False): +def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclaimer=False): """ Perform migrations """ @@ -825,21 +825,34 @@ def tools_migrations_migrate(target=None, skip=False, auto=False): else: # can't happen, this case is handle before raise Exception() + # If we are migrating in "automatic mode" (i.e. from debian + # configure during an upgrade of the package) but we are asked to run + # migrations is to be ran manually by the user + manual_migrations = [m for m in migrations if m.mode == "manual"] + if auto and manual_migrations: + for m in manual_migrations: + logger.warn(m18n.n('migrations_to_be_ran_manually', + number=m.number, + name=m.name)) + return + + # If some migrations have disclaimers, require the --accept-disclaimer + # option + migrations_with_disclaimer = [m for m in migrations if m.disclaimer] + if not accept_disclaimer and migrations_with_disclaimer: + for m in migrations_with_disclaimer: + logger.warn(m18n.n('migrations_need_to_accept_disclaimer', + number=m.number, + name=m.name, + disclaimer=m.disclaimer)) + return + # effectively run selected migrations for migration in migrations: if not skip: - # If we are migrating in "automatic mode" (i.e. from debian - # configure during an upgrade of the package) but the migration - # is to be ran manually by the user - if auto and migration.mode == "manual": - logger.warn(m18n.n('migrations_to_be_ran_manually', - number=migration.number, name=migration.name)) - break - else: - logger.warn(m18n.n('migrations_show_currently_running_migration', - number=migration.number, name=migration.name)) - + logger.warn(m18n.n('migrations_show_currently_running_migration', + number=migration.number, name=migration.name)) try: if mode == "forward": From 44a66b1ff4453d2e88aebe64a38de275a2e02844 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 5 Mar 2018 23:42:01 +0100 Subject: [PATCH 0667/1066] Fix migration skipping during postinstall --- src/yunohost/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 9719f9223..5e6e3f881 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -396,7 +396,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): _install_appslist_fetch_cron() # Init migrations (skip them, no need to run them on a fresh system) - tools_migrations_migrate(skip=True) + tools_migrations_migrate(skip=True, auto=True) os.system('touch /etc/yunohost/installed') @@ -829,7 +829,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai # configure during an upgrade of the package) but we are asked to run # migrations is to be ran manually by the user manual_migrations = [m for m in migrations if m.mode == "manual"] - if auto and manual_migrations: + if not skip and auto and manual_migrations: for m in manual_migrations: logger.warn(m18n.n('migrations_to_be_ran_manually', number=m.number, @@ -839,7 +839,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai # If some migrations have disclaimers, require the --accept-disclaimer # option migrations_with_disclaimer = [m for m in migrations if m.disclaimer] - if not accept_disclaimer and migrations_with_disclaimer: + if not skip and not accept_disclaimer and migrations_with_disclaimer: for m in migrations_with_disclaimer: logger.warn(m18n.n('migrations_need_to_accept_disclaimer', number=m.number, From 79eb70ec61ae35b7984e57096629a5603b0f5707 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 10 Mar 2018 16:09:16 +0100 Subject: [PATCH 0668/1066] Use number/name in i18n string to avoid breaking existing translations... --- locales/en.json | 4 ++-- src/yunohost/tools.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index b0c2ea3d2..9957c25a0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -228,10 +228,10 @@ "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", "migrations_current_target": "Migration target is {}", - "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {migration_id}", + "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", "migrations_forward": "Migrating forward", "migrations_list_conflict_pending_done": "You cannot use both --previous and --done at the same time.", - "migrations_loading_migration": "Loading migration {migration_id}...", + "migrations_loading_migration": "Loading migration {number} {name}...", "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", "migrations_no_migrations_to_run": "No migrations to run", "migrations_show_currently_running_migration": "Running migration {number} {name}...", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5e6e3f881..c92f3bda4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -970,9 +970,10 @@ def _load_migration(migration_file): migration_id = migration_file[:-len(".py")] + number, name = migration_id.split("_", 1) + logger.debug(m18n.n('migrations_loading_migration', - migration_id=migration_id, - )) + number=number, name=name)) try: # this is python builtin method to import a module using a name, we @@ -985,8 +986,7 @@ def _load_migration(migration_file): traceback.print_exc() raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', - migration_id=migration_id, - )) + number=number, name=name)) class Migration(object): From cf78e4065839309d50dc45bfe69292b50941aeb1 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 18 Mar 2018 15:11:49 +0100 Subject: [PATCH 0669/1066] Add wget as dependency --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index c15c5eec5..d1505994a 100644 --- a/debian/control +++ b/debian/control @@ -14,7 +14,7 @@ Depends: ${python:Depends}, ${misc:Depends} , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc , glances - , dnsutils, bind9utils, unzip, git, curl, cron + , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd From 60727e5fab2b949ec28c85e9572dfdf1939f9955 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 20 Mar 2018 22:27:42 +0100 Subject: [PATCH 0670/1066] [fix] typo --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c2c796089..27dee21e7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -498,7 +498,7 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journa=journal) != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journal=journal) != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -617,7 +617,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journa=journal) != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journal=journal) != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) From 91d0965569708ee66bf012f35a6cda34ba59b709 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 30 Mar 2018 23:05:04 +0200 Subject: [PATCH 0671/1066] Add nodejs helpers (#441) * Add nodejs helpers * Typo fix * Add explanation for load_n_path * Remove useless line * Remove another old useless line * Move to a dedicated file * Move to a dedicated file * Fix check n already installed --- data/helpers.d/nodejs | 203 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 data/helpers.d/nodejs diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs new file mode 100644 index 000000000..ff69cacd2 --- /dev/null +++ b/data/helpers.d/nodejs @@ -0,0 +1,203 @@ +n_install_dir="/opt/node_n" +node_version_path="/opt/node_n/n/versions/node" +# N_PREFIX is the directory of n, it needs to be loaded as a environment variable. +export N_PREFIX="$n_install_dir" + +# Install Node version management +# +# [internal] +# +# usage: ynh_install_n +ynh_install_n () { + echo "Installation of N - Node.js version management" >&2 + # Build an app.src for n + mkdir -p "../conf" + echo "SOURCE_URL=https://github.com/tj/n/archive/v2.1.7.tar.gz +SOURCE_SUM=2ba3c9d4dd3c7e38885b37e02337906a1ee91febe6d5c9159d89a9050f2eea8f" > "../conf/n.src" + # Download and extract n + ynh_setup_source "$n_install_dir/git" n + # Install n + (cd "$n_install_dir/git" + PREFIX=$N_PREFIX make install 2>&1) +} + +# Load the version of node for an app, and set variables. +# +# ynh_use_nodejs has to be used in any app scripts before using node for the first time. +# +# 2 variables are available: +# - $nodejs_path: The absolute path of node for the chosen version. +# - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml. +# And 2 alias stored in variables: +# - $nodejs_use_version: An alias to load a node version in the current shell. Especially useful for systemd. +# NB: $PATH will contain the path to node, it has to be propagated to any other shell which needs to use it. +# - $ynh_node_exec: An alias to execute a command with node. +# +# usage: ynh_use_nodejs +ynh_use_nodejs () { + nodejs_version=$(ynh_app_setting_get $app nodejs_version) + + # Add "$n_install_dir/bin" to the PATH variable if it isn't already added. + # And define N_PREFIX in the current shell. + load_n_path="[[ :$PATH: == *\":$n_install_dir/bin:\"* ]] || PATH=\"$n_install_dir/bin:$PATH\"; N_PREFIX="$n_install_dir"" + + nodejs_use_version="$n_install_dir/bin/n -q $nodejs_version" + + # "Load" a version of node + eval $load_n_path; $nodejs_use_version + + # Get the absolute path of this version of node + nodejs_path="$(n bin $nodejs_version)" + + # Make an alias for node use + ynh_node_exec="eval $load_n_path; n use $nodejs_version" +} + +# Install a specific version of nodejs +# +# n (Node version management) uses the PATH variable to store the path of the version of node it is going to use. +# That's how it changes the version +# +# ynh_install_nodejs will install the version of node provided as argument by using n. +# +# usage: ynh_install_nodejs [nodejs_version] +# | arg: nodejs_version - Version of node to install. +# If possible, prefer to use major version number (e.g. 8 instead of 8.10.0). +# The crontab will handle the update of minor versions when needed. +ynh_install_nodejs () { + # Use n, https://github.com/tj/n to manage the nodejs versions + nodejs_version="$1" + + # Create $n_install_dir + mkdir -p "$n_install_dir" + + # Load n path in PATH + CLEAR_PATH="$n_install_dir/bin:$PATH" + # Remove /usr/local/bin in PATH in case of node prior installation + PATH=$(echo $CLEAR_PATH | sed 's@/usr/local/bin:@@') + + # Move an existing node binary, to avoid to block n. + test -x /usr/bin/node && mv /usr/bin/node /usr/bin/node_n + test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n + + # If n is not previously setup, install it + if ! test $(n --version) > /dev/null 2>&1 + then + ynh_install_n + fi + + # Modify the default N_PREFIX in n script + ynh_replace_string "^N_PREFIX=\${N_PREFIX-.*}$" "N_PREFIX=\${N_PREFIX-$N_PREFIX}" "$n_install_dir/bin/n" + + # Restore /usr/local/bin in PATH + PATH=$CLEAR_PATH + + # And replace the old node binary. + test -x /usr/bin/node_n && mv /usr/bin/node_n /usr/bin/node + test -x /usr/bin/npm_n && mv /usr/bin/npm_n /usr/bin/npm + + # Install the requested version of nodejs + n $nodejs_version + + # Find the last "real" version for this major version of node. + real_nodejs_version=$(find $node_version_path/$nodejs_version* -maxdepth 0 | sort --version-sort | tail --lines=1) + real_nodejs_version=$(basename $real_nodejs_version) + + # Create a symbolic link for this major version if the file doesn't already exist + if [ ! -e "$node_version_path/$nodejs_version" ] + then + ln --symbolic --force --no-target-directory $node_version_path/$real_nodejs_version $node_version_path/$nodejs_version + fi + + # Store the ID of this app and the version of node requested for it + echo "$YNH_APP_ID:$nodejs_version" | tee --append "$n_install_dir/ynh_app_version" + + # Store nodejs_version into the config of this app + ynh_app_setting_set $app nodejs_version $nodejs_version + + # Build the update script and set the cronjob + ynh_cron_upgrade_node + + ynh_use_nodejs +} + +# Remove the version of node used by the app. +# +# This helper will check if another app uses the same version of node, +# if not, this version of node will be removed. +# If no other app uses node, n will be also removed. +# +# usage: ynh_remove_nodejs +ynh_remove_nodejs () { + ynh_use_nodejs + + # Remove the line for this app + sed --in-place "/$YNH_APP_ID:$nodejs_version/d" "$n_install_dir/ynh_app_version" + + # If no other app uses this version of nodejs, remove it. + if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version" + then + n rm $nodejs_version + fi + + # If no other app uses n, remove n + if [ ! -s "$n_install_dir/ynh_app_version" ] + then + ynh_secure_remove "$n_install_dir" + ynh_secure_remove "/usr/local/n" + fi +} + +# Set a cron design to update your node versions +# +# [internal] +# +# This cron will check and update all minor node versions used by your apps. +# +# usage: ynh_cron_upgrade_node +ynh_cron_upgrade_node () { + # Build the update script + cat > "$n_install_dir/node_update.sh" << EOF +#!/bin/bash + +version_path="$node_version_path" +n_install_dir="$n_install_dir" + +# Log the date +date + +# List all real installed version of node +all_real_version="\$(find \$version_path/* -maxdepth 0 -type d | sed "s@\$version_path/@@g")" + +# Keep only the major version number of each line +all_real_version=\$(echo "\$all_real_version" | sed 's/\..*\$//') + +# Remove double entries +all_real_version=\$(echo "\$all_real_version" | sort --unique) + +# Read each major version +while read version +do + echo "Update of the version \$version" + sudo \$n_install_dir/bin/n \$version + + # Find the last "real" version for this major version of node. + real_nodejs_version=\$(find \$version_path/\$version* -maxdepth 0 | sort --version-sort | tail --lines=1) + real_nodejs_version=\$(basename \$real_nodejs_version) + + # Update the symbolic link for this version + sudo ln --symbolic --force --no-target-directory \$version_path/\$real_nodejs_version \$version_path/\$version +done <<< "\$(echo "\$all_real_version")" +EOF + + chmod +x "$n_install_dir/node_update.sh" + + # Build the cronjob + cat > "/etc/cron.daily/node_update" << EOF +#!/bin/bash + +$n_install_dir/node_update.sh >> $n_install_dir/node_update.log +EOF + + chmod +x "/etc/cron.daily/node_update" +} From ba01168684c8459701e7ec26d1ec37084062e850 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 3 Apr 2018 20:38:56 +0200 Subject: [PATCH 0672/1066] [enh] Allow to display all operation logs with one command --- data/actionsmap/yunohost.yml | 25 ++++---- locales/en.json | 2 +- src/yunohost/app.py | 8 +-- src/yunohost/{journals.py => log.py} | 93 +++++++++++++++------------- 4 files changed, 69 insertions(+), 59 deletions(-) rename src/yunohost/{journals.py => log.py} (54%) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 3a16bf3bd..b30167b18 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1603,26 +1603,27 @@ hook: help: The directory from where the script will be executed ############################# -# Journals # +# Log # ############################# -journals: - category_help: Manage debug journals +log: + category_help: Manage debug logs actions: - ### journals_list() + ### log_list() list: - action_help: List journals - api: GET /journals + action_help: List logs + api: GET /logs arguments: -l: full: --limit - help: Maximum number of journals per categories + help: Maximum number of logs per categories type: int - ### journals_display() + ### log_display() display: - action_help: Display a journal content - api: GET /journals/ + action_help: Display a log content + api: GET /logs/ arguments: - file_name: - help: Journal file name + file_name_list: + help: Log filenames for which to display the content + nargs: "*" diff --git a/locales/en.json b/locales/en.json index 2859bc888..27946e18d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -205,7 +205,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", - "journal_does_exists": "There is not journal with the name '{journal}', use 'yunohost journal list to see all available journals'", + "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 27dee21e7..2ee96afd1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -433,7 +433,7 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal installed = _is_installed(app) if not installed: @@ -542,7 +542,7 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' @@ -673,7 +673,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal # Fetch or extract sources try: @@ -837,7 +837,7 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - from yunohost.journals import Journal + from yunohost.log import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, diff --git a/src/yunohost/journals.py b/src/yunohost/log.py similarity index 54% rename from src/yunohost/journals.py rename to src/yunohost/log.py index 0f1785697..7c1f3b2a8 100644 --- a/src/yunohost/journals.py +++ b/src/yunohost/log.py @@ -19,9 +19,9 @@ """ -""" yunohost_journals.py +""" yunohost_log.py - Manage debug journals + Manage debug logs """ import os @@ -34,90 +34,98 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -JOURNALS_PATH = '/var/log/yunohost/journals/' +OPERATIONS_PATH = '/var/log/yunohost/operation/' +OPERATION_FILE_EXT = '.yml' -logger = getActionLogger('yunohost.journals') +logger = getActionLogger('yunohost.log') -def journals_list(limit=None): +def log_list(limit=None): """ - List available journals + List available logs Keyword argument: - limit -- Maximum number of journals per categories + limit -- Maximum number of logs per categories """ result = {"categories": []} - if not os.path.exists(JOURNALS_PATH): + if not os.path.exists(OPERATIONS_PATH): return result - for category in sorted(os.listdir(JOURNALS_PATH)): - result["categories"].append({"name": category, "journals": []}) - for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): + for category in sorted(os.listdir(OPERATIONS_PATH)): + result["categories"].append({"name": category, "logs": []}) + for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - file_name = journal + file_name = operation - journal = journal[:-len(".journal")] - journal = journal.split("_") + operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation.split("_") - journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") - result["categories"][-1]["journals"].append({ - "started_at": journal_datetime, - "name": " ".join(journal[:-2]), + result["categories"][-1]["logs"].append({ + "started_at": operation_datetime, + "name": " ".join(operation[:-2]), "file_name": file_name, - "path": os.path.join(JOURNALS_PATH, category, file_name), + "path": os.path.join(OPERATIONS_PATH, category, file_name), }) - result["categories"][-1]["journals"] = list(reversed(sorted(result["categories"][-1]["journals"], key=lambda x: x["started_at"]))) + result["categories"][-1]["logs"] = list(reversed(sorted(result["categories"][-1]["logs"], key=lambda x: x["started_at"]))) if limit is not None: - result["categories"][-1]["journals"] = result["categories"][-1]["journals"][:limit] + result["categories"][-1]["logs"] = result["categories"][-1]["logs"][:limit] return result -def journals_display(file_name): +def log_display(file_name_list): """ - Display a journal content + Display full log or specific logs listed Argument: - file_name + file_name_list """ - if not os.path.exists(JOURNALS_PATH): + if not os.path.exists(OPERATIONS_PATH): raise MoulinetteError(errno.EINVAL, - m18n.n('journal_does_exists', journal=file_name)) + m18n.n('log_does_exists', log=" ".join(file_name_list))) - for category in os.listdir(JOURNALS_PATH): - for journal in filter(lambda x: x.endswith(".journal"), os.listdir(os.path.join(JOURNALS_PATH, category))): - if journal != file_name: + result = {"logs": []} + + for category in os.listdir(OPERATIONS_PATH): + for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + if operation not in file_name_list and file_name_list: continue - with open(os.path.join(JOURNALS_PATH, category, file_name), "r") as content: + file_name = operation + + with open(os.path.join(OPERATIONS_PATH, category, file_name), "r") as content: content = content.read() - journal = journal[:-len(".journal")] - journal = journal.split("_") - journal_datetime = datetime.strptime(" ".join(journal[-2:]), "%Y-%m-%d %H-%M-%S") + operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation.split("_") + operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - return { - "started_at": journal_datetime, - "name": " ".join(journal[:-2]), + result['logs'].append({ + "started_at": operation_datetime, + "name": " ".join(operation[:-2]), "file_name": file_name, - "path": os.path.join(JOURNALS_PATH, category, file_name), + "path": os.path.join(OPERATIONS_PATH, category, file_name), "metadata": infos, "logs": logs, - } + }) - raise MoulinetteError(errno.EINVAL, - m18n.n('journal_does_exists', journal=file_name)) + logger.debug("====> %s", len(file_name_list), exc_info=1) + if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): + logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) + if len(result['logs']) > 0: + return result class Journal(object): def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): @@ -129,7 +137,7 @@ class Journal(object): # this help uniformise file name and avoir threads concurrency errors self.started_at = datetime.now() - self.path = os.path.join(JOURNALS_PATH, category) + self.path = os.path.join(OPERATIONS_PATH, category) self.fd = None @@ -157,7 +165,8 @@ class Journal(object): if not os.path.exists(self.path): os.makedirs(self.path) - file_name = "%s_%s.journal" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name = "%s_%s" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) From d310540a6f52727bf2475d0a9bc59d13628875e8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Apr 2018 15:42:48 +0200 Subject: [PATCH 0673/1066] [enh] Set the date before operation name --- src/yunohost/log.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7c1f3b2a8..6fa7ae489 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -62,11 +62,11 @@ def log_list(limit=None): operation = operation[:-len(OPERATION_FILE_EXT)] operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") result["categories"][-1]["logs"].append({ "started_at": operation_datetime, - "name": " ".join(operation[:-2]), + "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) @@ -105,7 +105,7 @@ def log_display(file_name_list): operation = operation[:-len(OPERATION_FILE_EXT)] operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[-2:]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") infos, logs = content.split("\n---\n", 1) infos = yaml.safe_load(infos) @@ -113,14 +113,13 @@ def log_display(file_name_list): result['logs'].append({ "started_at": operation_datetime, - "name": " ".join(operation[:-2]), + "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), "metadata": infos, "logs": logs, }) - logger.debug("====> %s", len(file_name_list), exc_info=1) if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) @@ -165,7 +164,7 @@ class Journal(object): if not os.path.exists(self.path): os.makedirs(self.path) - file_name = "%s_%s" % (self.name if isinstance(self.name, basestring) else "_".join(self.name), self.started_at.strftime("%F_%X").replace(":", "-")) + file_name = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self.name if isinstance(self.name, basestring) else "_".join(self.name)) file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) From fb27b4d5f213ee309e831152dbcde435c07d59a1 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 10 Apr 2018 16:28:31 +0200 Subject: [PATCH 0674/1066] [enh] Sort logs display by start date --- src/yunohost/log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6fa7ae489..83fcc032d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -124,6 +124,7 @@ def log_display(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) if len(result['logs']) > 0: + result['logs'] = sorted(result['logs'], key=lambda operation: operation['started_at']) return result class Journal(object): From c1bd302973ed4b2c3b147fd93eab09a746ccfc1e Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 09:31:40 +0200 Subject: [PATCH 0675/1066] [enh] Rename some logs into operations --- src/yunohost/log.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 83fcc032d..389f446a6 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -54,7 +54,7 @@ def log_list(limit=None): return result for category in sorted(os.listdir(OPERATIONS_PATH)): - result["categories"].append({"name": category, "logs": []}) + result["categories"].append({"name": category, "operations": []}) for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): file_name = operation @@ -64,17 +64,17 @@ def log_list(limit=None): operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") - result["categories"][-1]["logs"].append({ + result["categories"][-1]["operations"].append({ "started_at": operation_datetime, "name": " ".join(operation[-2:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) - result["categories"][-1]["logs"] = list(reversed(sorted(result["categories"][-1]["logs"], key=lambda x: x["started_at"]))) + result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) if limit is not None: - result["categories"][-1]["logs"] = result["categories"][-1]["logs"][:limit] + result["categories"][-1]["operations"] = result["categories"][-1]["operations"][:limit] return result @@ -91,7 +91,7 @@ def log_display(file_name_list): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=" ".join(file_name_list))) - result = {"logs": []} + result = {"operations": []} for category in os.listdir(OPERATIONS_PATH): for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): @@ -111,7 +111,7 @@ def log_display(file_name_list): infos = yaml.safe_load(infos) logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - result['logs'].append({ + result['operations'].append({ "started_at": operation_datetime, "name": " ".join(operation[-2:]), "file_name": file_name, @@ -120,11 +120,11 @@ def log_display(file_name_list): "logs": logs, }) - if len(file_name_list) > 0 and len(result['logs']) < len(file_name_list): + if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) - if len(result['logs']) > 0: - result['logs'] = sorted(result['logs'], key=lambda operation: operation['started_at']) + if len(result['operations']) > 0: + result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result class Journal(object): From a22672ede84496cfa2f49438e8e3d58663802314 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 10:03:20 +0200 Subject: [PATCH 0676/1066] [fix] Avoid operation log filename with space --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2ee96afd1..0e40470c5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -786,7 +786,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.path.join(extracted_app_folder, 'scripts/remove'), args=[app_instance_name], env=env_dict_remove, user="root", journal = Journal( - ["remove", app_instance_name, "failed install"], + ["remove", app_instance_name, "failed_install"], "app", args=[app_instance_name], env=env_dict_remove, ), ) From 219f1c6262ccbef6829fafdd124691bfb42b522b Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:49:26 +0200 Subject: [PATCH 0677/1066] [enh] Use Handler to write log --- src/yunohost/app.py | 40 ++++++------- src/yunohost/hook.py | 14 +---- src/yunohost/log.py | 128 +++++++++++++++++++++++++--------------- src/yunohost/service.py | 3 +- 4 files changed, 105 insertions(+), 80 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0e40470c5..1ad566bcb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -422,6 +422,7 @@ def app_map(app=None, raw=False, user=None): return result +@is_unit_operation() def app_change_url(auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -433,7 +434,6 @@ def app_change_url(auth, app, domain, path): """ from yunohost.hook import hook_exec, hook_callback - from yunohost.log import Journal installed = _is_installed(app) if not installed: @@ -497,8 +497,7 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) - journal = Journal(["change_url", app], "app", args=args_list, env=env_dict) - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root", journal=journal) != 0: + if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -531,6 +530,7 @@ def app_change_url(auth, app, domain, path): hook_callback('post_app_change_url', args=args_list, env=env_dict) +@is_unit_operation() def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app @@ -616,8 +616,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - journal = Journal(["upgrade", app_instance_name], "app", args=args_list, env=env_dict) - if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root", journal=journal) != 0: + if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) @@ -660,8 +659,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if is_api: return {"log": service_log('yunohost-api', number="100").values()[0]} - -def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): +def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): """ Install apps @@ -673,7 +671,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import Journal + from yunohost.log import UnitOperationHandler + + uo_install = UnitOperationHandler('app_install', 'app', args=kwargs) # Fetch or extract sources try: @@ -762,11 +762,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root", - journal = Journal( - ["install", app_instance_name], - "app", args=args_list, env=env_dict - ), + args=args_list, env=env_dict, user="root" ) except (KeyboardInterrupt, EOFError): install_retcode = -1 @@ -774,6 +770,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: + uo_install.close() if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -782,18 +779,19 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script + uo_remove = UnitOperationHandler('remove_on_failed_install', + 'app', args=env_dict_remove) + remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root", - journal = Journal( - ["remove", app_instance_name, "failed_install"], - "app", args=[app_instance_name], env=env_dict_remove, - ), + args=[app_instance_name], env=env_dict_remove, user="root" ) if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) + uo_remove.close() + # Clean tmp folders shutil.rmtree(app_setting_path) shutil.rmtree(extracted_app_folder) @@ -827,7 +825,10 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): hook_callback('post_app_install', args=args_list, env=env_dict) + uo_install.close() + +@is_unit_operation() def app_remove(auth, app): """ Remove app @@ -863,8 +864,7 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - journal = Journal(["remove", app], "app", args=args_list, env=env_dict) - if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root", journal=journal) == 0: + if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) hook_callback('post_app_remove', args=args_list, env=env_dict) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 95ed2aec4..95025d827 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -297,7 +297,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, journal=None, user="admin"): + chdir=None, env=None, user="admin"): """ Execute hook from a file with arguments @@ -359,18 +359,6 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) - if journal is None: - # Define output callbacks and call command - callbacks = ( - lambda l: logger.info(l.rstrip()), - lambda l: logger.warning(l.rstrip()), - ) - else: - callbacks = journal.as_callbacks_tuple( - stdout=lambda l: logger.info(l.rstrip()), - stderr=lambda l: logger.warning(l.rstrip()), - ) - returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 389f446a6..6d51f62c4 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,8 +27,11 @@ import os import yaml import errno +import logging from datetime import datetime +from logging import StreamHandler, getLogger, Formatter +from sys import exc_info from moulinette import m18n from moulinette.core import MoulinetteError @@ -39,7 +42,6 @@ OPERATION_FILE_EXT = '.yml' logger = getActionLogger('yunohost.log') - def log_list(limit=None): """ List available logs @@ -127,71 +129,105 @@ def log_display(file_name_list): result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result -class Journal(object): - def __init__(self, name, category, on_stdout=None, on_stderr=None, on_write=None, **kwargs): +def is_unit_operation(categorie=None, description_key=None): + def decorate(func): + def func_wrapper(*args, **kwargs): + cat = categorie + desc_key = description_key + + if cat is None: + cat = func.__module__.split('.')[1] + if desc_key is None: + desc_key = func.__name__ + uo = UnitOperationHandler(desc_key, cat, args=kwargs) + try: + result = func(*args, **kwargs) + finally: + uo.close(exc_info()[0]) + return result + return func_wrapper + return decorate + +class UnitOperationHandler(StreamHandler): + def __init__(self, name, category, **kwargs): # TODO add a way to not save password on app installation - self.name = name + self._name = name self.category = category self.first_write = True + self.closed = False # this help uniformise file name and avoir threads concurrency errors self.started_at = datetime.now() self.path = os.path.join(OPERATIONS_PATH, category) - self.fd = None + if not os.path.exists(self.path): + os.makedirs(self.path) - self.on_stdout = [] if on_stdout is None else on_stdout - self.on_stderr = [] if on_stderr is None else on_stderr - self.on_write = [] if on_write is None else on_write + self.filename = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self._name if isinstance(self._name, basestring) else "_".join(self._name)) + self.filename += OPERATION_FILE_EXT self.additional_information = kwargs - def __del__(self): - if self.fd: - self.fd.close() + logging.StreamHandler.__init__(self, self._open()) - def write(self, line): + self.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') + + if self.stream is None: + self.stream = self._open() + + # Listen to the root logger + self.logger = getLogger('yunohost') + self.logger.addHandler(self) + + + def _open(self): + stream = open(os.path.join(self.path, self.filename), "w") + return stream + + def close(self, error=None): + """ + Closes the stream. + """ + if self.closed: + return + self.acquire() + #self.ended_at = datetime.now() + #self.error = error + #self.stream.seek(0) + #context = { + # 'ended_at': datetime.now() + #} + #if error is not None: + # context['error'] = error + #self.stream.write(yaml.safe_dump(context)) + self.logger.removeHandler(self) + try: + if self.stream: + try: + self.flush() + finally: + stream = self.stream + self.stream = None + if hasattr(stream, "close"): + stream.close() + finally: + self.release() + self.closed = True + + def __del__(self): + self.close() + + def emit(self, record): if self.first_write: self._do_first_write() self.first_write = False - self.fd.write("%s: " % datetime.now().strftime("%F %X")) - self.fd.write(line.rstrip()) - self.fd.write("\n") - self.fd.flush() + StreamHandler.emit(self, record) def _do_first_write(self): - if not os.path.exists(self.path): - os.makedirs(self.path) - - file_name = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self.name if isinstance(self.name, basestring) else "_".join(self.name)) - file_name += OPERATION_FILE_EXT serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) - self.fd = open(os.path.join(self.path, file_name), "w") - - self.fd.write(serialized_additional_information) - self.fd.write("\n---\n") - - def stdout(self, line): - for i in self.on_stdout: - i(line) - - self.write(line) - - def stderr(self, line): - for i in self.on_stderr: - i(line) - - self.write(line) - - def as_callbacks_tuple(self, stdout=None, stderr=None): - if stdout: - self.on_stdout.append(stdout) - - if stderr: - self.on_stderr.append(stderr) - - return (self.stdout, self.stderr) + self.stream.write(serialized_additional_information) + self.stream.write("\n---\n") diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f0948c961..4e264e310 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -38,6 +38,7 @@ from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem from yunohost.hook import hook_callback +from yunohost.log import is_unit_operation BASE_CONF_PATH = '/home/yunohost.conf' BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup') @@ -141,7 +142,7 @@ def service_stop(names): m18n.n('service_stop_failed', service=name)) logger.info(m18n.n('service_already_stopped', service=name)) - +@is_unit_operation() def service_enable(names): """ Enable one or more services From fac50997dceefbe0e4a8243765bb8f766a0e0d65 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:54:46 +0200 Subject: [PATCH 0678/1066] [fix] Some missing --- src/yunohost/app.py | 1 - src/yunohost/hook.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1ad566bcb..1e6ac3a82 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -542,7 +542,6 @@ def app_upgrade(auth, app=[], url=None, file=None): """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import Journal # Retrieve interface is_api = msettings.get('interface') == 'api' diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 95025d827..89defe55e 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -359,6 +359,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, else: logger.info(m18n.n('executing_script', script=path)) + # Define output callbacks and call command + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From 6c063112bfcb74c67c130f876e9c2834297f3028 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 22:58:26 +0200 Subject: [PATCH 0679/1066] [fix] Some missing --- src/yunohost/app.py | 1 + src/yunohost/hook.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1e6ac3a82..1024b8b28 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -658,6 +658,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if is_api: return {"log": service_log('yunohost-api', number="100").values()[0]} + def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): """ Install apps diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 89defe55e..1f971edb6 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -364,7 +364,6 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, lambda l: logger.info(l.rstrip()), lambda l: logger.warning(l.rstrip()), ) - returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From b81d89f93ab56795700b9db9fe99aac11a50f77c Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 11 Apr 2018 23:01:24 +0200 Subject: [PATCH 0680/1066] [fix] Remove old ref to Journal --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1024b8b28..2bfca443e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,6 +44,7 @@ from moulinette.utils.log import getActionLogger from yunohost.service import service_log, _run_service_command from yunohost.utils import packages +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.app') @@ -838,7 +839,6 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - from yunohost.log import Journal if not _is_installed(app): raise MoulinetteError(errno.EINVAL, From 79ee0396d0e301c4e41493b392f723190d1419aa Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 12 Apr 2018 21:22:17 +0200 Subject: [PATCH 0681/1066] [enh] Add related info --- locales/en.json | 1 + src/yunohost/app.py | 66 ++++++++---- src/yunohost/certificate.py | 2 +- src/yunohost/domain.py | 3 + src/yunohost/dyndns.py | 14 ++- src/yunohost/log.py | 207 ++++++++++++++++++++---------------- src/yunohost/tools.py | 24 ++++- src/yunohost/user.py | 4 + 8 files changed, 200 insertions(+), 121 deletions(-) diff --git a/locales/en.json b/locales/en.json index 27946e18d..9f310c3b9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -206,6 +206,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", + "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2bfca443e..f83a11222 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -188,6 +188,7 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) +@is_unit_operation() def app_removelist(name): """ Remove list from the repositories @@ -423,8 +424,8 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation() -def app_change_url(auth, app, domain, path): +@is_unit_operation(lazy=True) +def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -481,6 +482,9 @@ def app_change_url(auth, app, domain, path): env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + uo.extra.update({'env': env_dict}) + uo.start() + if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) @@ -499,13 +503,14 @@ def app_change_url(auth, app, domain, path): os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: - logger.error("Failed to change '%s' url." % app) + msg = "Failed to change '%s' url." % app + logger.error(msg) + uo.error(msg) # restore values modified by app_checkurl # see begining of the function app_setting(app, "domain", value=old_domain) app_setting(app, "path", value=old_path) - return # this should idealy be done in the change_url script but let's avoid common mistakes @@ -531,7 +536,6 @@ def app_change_url(auth, app, domain, path): hook_callback('post_app_change_url', args=args_list, env=env_dict) -@is_unit_operation() def app_upgrade(auth, app=[], url=None, file=None): """ Upgrade app @@ -614,10 +618,15 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + # Start register change on system + uo = UnitOperation('app_upgrade', 'app', app_instance_name, env=env_dict) + # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: - logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) + msg = m18n.n('app_upgrade_failed', app=app_instance_name) + logger.error(msg) + uo.error(msg) else: now = int(time.time()) # TODO: Move install_time away from app_setting @@ -646,7 +655,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('app_upgraded', app=app_instance_name)) hook_callback('post_app_upgrade', args=args_list, env=env_dict) - + uo.success() if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -660,7 +669,8 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, **kwargs): +@is_unit_operation(lazy=True) +def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -672,9 +682,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import UnitOperationHandler + from yunohost.log import UnitOperation - uo_install = UnitOperationHandler('app_install', 'app', args=kwargs) # Fetch or extract sources try: @@ -732,6 +741,10 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) + # Start register change on system + uo.extra.update({'env':env_dict}) + uo.start() + # Create app directory app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) if os.path.exists(app_setting_path): @@ -771,7 +784,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - uo_install.close() + uo.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -780,18 +793,21 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script - uo_remove = UnitOperationHandler('remove_on_failed_install', - 'app', args=env_dict_remove) + uo_remove = UnitOperation('remove_on_failed_install', + 'app', app_instance_name, + env=env_dict_remove) remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), args=[app_instance_name], env=env_dict_remove, user="root" ) if remove_retcode != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) - - uo_remove.close() + msg = m18n.n('app_not_properly_removed', + app=app_instance_name) + logger.warning(msg) + uo_remove.error(msg) + else: + uo_remove.success() # Clean tmp folders shutil.rmtree(app_setting_path) @@ -826,11 +842,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False, ** hook_callback('post_app_install', args=args_list, env=env_dict) - uo_install.close() - -@is_unit_operation() -def app_remove(auth, app): +@is_unit_operation(lazy=True) +def app_remove(uo, auth, app): """ Remove app @@ -839,11 +853,12 @@ def app_remove(auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - if not _is_installed(app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) + uo.start() + app_setting_path = APPS_SETTING_PATH + app # TODO: display fail messages from script @@ -863,6 +878,8 @@ def app_remove(auth, app): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + uo.extra.update({'env': env_dict}) + uo.flush() if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) @@ -1034,7 +1051,8 @@ def app_debug(app): } -def app_makedefault(auth, app, domain=None): +@is_unit_operation(lazy=True) +def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app @@ -1051,9 +1069,11 @@ def app_makedefault(auth, app, domain=None): if domain is None: domain = app_domain + uo.related_to['domain']=[domain] elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + uo.start() if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, m18n.n('app_make_default_location_already_used', diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 310c5d131..a242f51b0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -49,7 +49,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf - +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.certmanager') diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 026c4da36..774ab928e 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -38,6 +38,7 @@ import yunohost.certificate from yunohost.service import service_regen_conf from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.domain') @@ -62,6 +63,7 @@ def domain_list(auth): return {'domains': result_list} +@is_unit_operation() def domain_add(auth, domain, dyndns=False): """ Create a custom domain @@ -127,6 +129,7 @@ def domain_add(auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) +@is_unit_operation() def domain_remove(auth, domain, force=False): """ Delete domains diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ec3bf88c8..739f2da9e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -41,6 +41,7 @@ from moulinette.utils.network import download_json from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.dyndns') @@ -113,7 +114,8 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None): +@is_unit_operation('domain', lazy=True) +def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -126,6 +128,10 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None if domain is None: domain = _get_maindomain() + uo.on = [domain] + uo.related_to['domain'] = [domain] + uo.start() + # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): raise MoulinetteError(errno.ENOENT, @@ -170,7 +176,8 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None dyndns_installcron() -def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, +@is_unit_operation('domain',lazy=True) +def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ Update IP on DynDNS platform @@ -212,6 +219,9 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") + uo.on = [domain] + uo.related_to['domain'] = [domain] + uo.start() # If domain is not given, try to guess it from keys available... if domain is None: diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6d51f62c4..fbf2fc232 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,10 +27,9 @@ import os import yaml import errno -import logging from datetime import datetime -from logging import StreamHandler, getLogger, Formatter +from logging import FileHandler, getLogger, Formatter from sys import exc_info from moulinette import m18n @@ -38,7 +37,9 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger OPERATIONS_PATH = '/var/log/yunohost/operation/' -OPERATION_FILE_EXT = '.yml' +METADATA_FILE_EXT = '.yml' +LOG_FILE_EXT = '.log' +RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') @@ -57,11 +58,11 @@ def log_list(limit=None): for category in sorted(os.listdir(OPERATIONS_PATH)): result["categories"].append({"name": category, "operations": []}) - for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): file_name = operation - operation = operation[:-len(OPERATION_FILE_EXT)] + operation = operation[:-len(METADATA_FILE_EXT)] operation = operation.split("_") operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") @@ -96,31 +97,27 @@ def log_display(file_name_list): result = {"operations": []} for category in os.listdir(OPERATIONS_PATH): - for operation in filter(lambda x: x.endswith(OPERATION_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): if operation not in file_name_list and file_name_list: continue - file_name = operation + file_name = operation[:-len(METADATA_FILE_EXT)] + operation = file_name.split("_") - with open(os.path.join(OPERATIONS_PATH, category, file_name), "r") as content: - content = content.read() + with open(os.path.join(OPERATIONS_PATH, category, file_name + METADATA_FILE_EXT), "r") as md_file: + try: + infos = yaml.safe_load(md_file) + except yaml.YAMLError as exc: + print(exc) - operation = operation[:-len(OPERATION_FILE_EXT)] - operation = operation.split("_") - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") - - infos, logs = content.split("\n---\n", 1) - infos = yaml.safe_load(infos) + with open(os.path.join(OPERATIONS_PATH, category, file_name + LOG_FILE_EXT), "r") as content: + logs = content.read() logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - - result['operations'].append({ - "started_at": operation_datetime, - "name": " ".join(operation[-2:]), - "file_name": file_name, - "path": os.path.join(OPERATIONS_PATH, category, file_name), - "metadata": infos, - "logs": logs, - }) + infos['logs'] = logs + infos['name'] = " ".join(operation[-2:]) + infos['file_name'] = file_name + METADATA_FILE_EXT + infos['path'] = os.path.join(OPERATIONS_PATH, category, file_name) + result['operations'].append(infos) if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) @@ -129,105 +126,133 @@ def log_display(file_name_list): result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) return result -def is_unit_operation(categorie=None, description_key=None): +def is_unit_operation(categorie=None, operation_key=None, lazy=False): def decorate(func): def func_wrapper(*args, **kwargs): cat = categorie - desc_key = description_key + op_key = operation_key + on = None + related_to = {} + inject = lazy + to_start = not lazy if cat is None: cat = func.__module__.split('.')[1] - if desc_key is None: - desc_key = func.__name__ - uo = UnitOperationHandler(desc_key, cat, args=kwargs) + if op_key is None: + op_key = func.__name__ + if cat in kwargs: + on = kwargs[cat] + for r_category in RELATED_CATEGORIES: + if r_category in kwargs and kwargs[r_category] is not None: + if r_category not in related_to: + related_to[r_category] = [] + if isinstance(kwargs[r_category], basestring): + related_to[r_category] += [kwargs[r_category]] + else: + related_to[r_category] += kwargs[r_category] + context = kwargs.copy() + if 'auth' in context: + context.pop('auth', None) + uo = UnitOperation(op_key, cat, on, related_to, args=context) + if to_start: + uo.start() try: + if inject: + args = (uo,) + args result = func(*args, **kwargs) finally: - uo.close(exc_info()[0]) + if uo.started_at is not None: + uo.close(exc_info()[0]) return result return func_wrapper return decorate -class UnitOperationHandler(StreamHandler): - def __init__(self, name, category, **kwargs): +class UnitOperation(object): + def __init__(self, operation, category, on=None, related_to=None, **kwargs): # TODO add a way to not save password on app installation - self._name = name + self.operation = operation self.category = category - self.first_write = True - self.closed = False + self.on = on + if isinstance(self.on, basestring): + self.on = [self.on] - # this help uniformise file name and avoir threads concurrency errors - self.started_at = datetime.now() + self.related_to = related_to + if related_to is None: + if self.category in RELATED_CATEGORIES: + self.related_to = {self.category: self.on} + self.extra = kwargs + self.started_at = None + self.ended_at = None + self.logger = None self.path = os.path.join(OPERATIONS_PATH, category) if not os.path.exists(self.path): os.makedirs(self.path) - self.filename = "%s_%s" % (self.started_at.strftime("%F_%X").replace(":", "-"), self._name if isinstance(self._name, basestring) else "_".join(self._name)) - self.filename += OPERATION_FILE_EXT + def start(self): + if self.started_at is None: + self.started_at = datetime.now() + self.flush() + self._register_log() - self.additional_information = kwargs - - logging.StreamHandler.__init__(self, self._open()) - - self.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') - - if self.stream is None: - self.stream = self._open() + def _register_log(self): + # TODO add a way to not save password on app installation + filename = os.path.join(self.path, self.name + LOG_FILE_EXT) + self.file_handler = FileHandler(filename) + self.file_handler.formatter = Formatter('%(asctime)s: %(levelname)s - %(message)s') # Listen to the root logger self.logger = getLogger('yunohost') - self.logger.addHandler(self) + self.logger.addHandler(self.file_handler) + def flush(self): + filename = os.path.join(self.path, self.name + METADATA_FILE_EXT) + with open(filename, 'w') as outfile: + yaml.safe_dump(self.metadata, outfile, default_flow_style=False) - def _open(self): - stream = open(os.path.join(self.path, self.filename), "w") - return stream + @property + def name(self): + name = [self.started_at.strftime("%F_%X").replace(":", "-")] + name += [self.operation] + if self.on is not None: + name += self.on + return '_'.join(name) - def close(self, error=None): - """ - Closes the stream. - """ - if self.closed: - return - self.acquire() - #self.ended_at = datetime.now() - #self.error = error - #self.stream.seek(0) - #context = { - # 'ended_at': datetime.now() - #} - #if error is not None: - # context['error'] = error - #self.stream.write(yaml.safe_dump(context)) - self.logger.removeHandler(self) - try: - if self.stream: - try: - self.flush() - finally: - stream = self.stream - self.stream = None - if hasattr(stream, "close"): - stream.close() - finally: - self.release() - self.closed = True + @property + def metadata(self): + data = { + 'started_at': self.started_at, + 'operation': self.operation, + 'related_to': self.related_to + } + if self.on is not None: + data['on'] = self.on + if self.ended_at is not None: + data['ended_at'] = self.ended_at + data['success'] = self._success + if self.error is not None: + data['error'] = self._error + # TODO: detect if 'extra' erase some key of 'data' + data.update(self.extra) + return data - def __del__(self): + def success(self): self.close() - def emit(self, record): - if self.first_write: - self._do_first_write() - self.first_write = False + def error(self, error): + self.close(error) - StreamHandler.emit(self, record) + def close(self, error=None): + if self.ended_at is not None or self.started_at is None: + return + self.ended_at = datetime.now() + self._error = error + self._success = error is None + if self.logger is not None: + self.logger.removeHandler(self.file_handler) + self.flush() - def _do_first_write(self): + def __del__(self): + self.error(m18n.n('log_operation_unit_unclosed_properly')) - serialized_additional_information = yaml.safe_dump(self.additional_information, default_flow_style=False) - - self.stream.write(serialized_additional_information) - self.stream.write("\n---\n") diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 381cd07e0..ba942d6e4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,6 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip +from yunohost.log import is_unit_operation # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -138,7 +139,8 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -def tools_maindomain(auth, new_domain=None): +@is_unit_operation('domain', lazy=True) +def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -155,6 +157,10 @@ def tools_maindomain(auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + uo.on = [new_domain] + uo.related_to['domain'] = [new_domain] + uo.start() + # Apply changes to ssl certs ssl_key = "/etc/ssl/private/yunohost_key.pem" ssl_crt = "/etc/ssl/private/yunohost_crt.pem" @@ -244,6 +250,7 @@ def _is_inside_container(): return out.split()[1] != "(1," +@is_unit_operation() def tools_postinstall(domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -464,7 +471,8 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): +@is_unit_operation(lazy=True) +def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -505,6 +513,7 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) + uo.start() try: # Apply APT changes # TODO: Logs output for the API @@ -514,11 +523,14 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): failure = True logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) + uo.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) + uo.success() else: logger.info(m18n.n('packages_no_upgrade')) + if not ignore_apps: try: app_upgrade(auth) @@ -699,7 +711,8 @@ def tools_port_available(port): return False -def tools_shutdown(force=False): +@is_unit_operation(lazy=True) +def tools_shutdown(uo, force=False): shutdown = force if not shutdown: try: @@ -712,11 +725,13 @@ def tools_shutdown(force=False): shutdown = True if shutdown: + uo.start() logger.warn(m18n.n('server_shutdown')) subprocess.check_call(['systemctl', 'poweroff']) -def tools_reboot(force=False): +@is_unit_operation(lazy=True) +def tools_reboot(uo, force=False): reboot = force if not reboot: try: @@ -728,6 +743,7 @@ def tools_reboot(force=False): if i.lower() == 'y' or i.lower() == 'yes': reboot = True if reboot: + uo.start() logger.warn(m18n.n('server_reboot')) subprocess.check_call(['systemctl', 'reboot']) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 11f61d807..08946633e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -36,6 +36,7 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.service import service_status +from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.user') @@ -89,6 +90,7 @@ def user_list(auth, fields=None): return {'users': users} +@is_unit_operation() def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -210,6 +212,7 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) +@is_unit_operation() def user_delete(auth, username, purge=False): """ Delete user @@ -245,6 +248,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) +@is_unit_operation() def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From a9d937db211871e26415125c5c75f86b9e55334b Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 13 Apr 2018 10:11:44 +0200 Subject: [PATCH 0682/1066] [enh] Display nice operation name --- locales/en.json | 18 ++++++++++++++++++ src/yunohost/log.py | 10 +++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index 9f310c3b9..47b3eefee 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,6 +207,24 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_app_removelist": "Remove an application list", + "log_app_change_url": "Change the url of '{}' application", + "log_app_install": "Install '{}' application", + "log_app_remove": "Remove '{}' application", + "log_app_upgrade": "Upgrade '{}' application", + "log_app_makedefault": "Make '{}' as default application", + "log_domain_add": "Add '{}' domain into system configuration", + "log_domain_remove": "Remove '{}' domain from system configuration", + "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", + "log_dyndns_update": "Update the ip associated with your YunoHost subdomain '{}'", + "log_user_create": "Add '{}' user", + "log_user_delete": "Delete '{}' user", + "log_user_update": "Update information of '{}' user", + "log_tools_maindomain": "Make '{}' as main domain", + "log_tools_postinstall": "Postinstall your YunoHost server", + "log_tools_upgrade": "Upgrade debian packages", + "log_tools_shutdown": "Shutdown your server", + "log_tools_reboot": "Reboot your server", "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", "ldap_initialized": "LDAP has been initialized", "license_undefined": "undefined", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index fbf2fc232..da6543c86 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -63,13 +63,13 @@ def log_list(limit=None): file_name = operation operation = operation[:-len(METADATA_FILE_EXT)] - operation = operation.split("_") + operation = operation.split("-") - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y-%m-%d %H-%M-%S") + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") result["categories"][-1]["operations"].append({ "started_at": operation_datetime, - "name": " ".join(operation[-2:]), + "name": m18n.n("log_" + operation[2], *operation[3:]), "file_name": file_name, "path": os.path.join(OPERATIONS_PATH, category, file_name), }) @@ -213,11 +213,11 @@ class UnitOperation(object): @property def name(self): - name = [self.started_at.strftime("%F_%X").replace(":", "-")] + name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.on is not None: name += self.on - return '_'.join(name) + return '-'.join(name) @property def metadata(self): From 73828306df0fa143a912690099fdf3a7f9e9dc50 Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 13 Apr 2018 11:27:31 +0200 Subject: [PATCH 0683/1066] [fix] Path log file with no extension --- src/yunohost/log.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index da6543c86..51fa85fa1 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -60,18 +60,19 @@ def log_list(limit=None): result["categories"].append({"name": category, "operations": []}) for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - file_name = operation + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, category, md_filename) - operation = operation[:-len(METADATA_FILE_EXT)] - operation = operation.split("-") + operation = base_filename.split("-") operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") result["categories"][-1]["operations"].append({ "started_at": operation_datetime, - "name": m18n.n("log_" + operation[2], *operation[3:]), - "file_name": file_name, - "path": os.path.join(OPERATIONS_PATH, category, file_name), + "description": m18n.n("log_" + operation[2], *operation[3:]), + "name": base_filename, + "path": md_path, }) result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) @@ -101,22 +102,26 @@ def log_display(file_name_list): if operation not in file_name_list and file_name_list: continue - file_name = operation[:-len(METADATA_FILE_EXT)] - operation = file_name.split("_") + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, category, md_filename) + log_filename = base_filename + LOG_FILE_EXT + log_path = os.path.join(OPERATIONS_PATH, category, log_filename) + operation = base_filename.split("-") - with open(os.path.join(OPERATIONS_PATH, category, file_name + METADATA_FILE_EXT), "r") as md_file: + with open(md_path, "r") as md_file: try: infos = yaml.safe_load(md_file) except yaml.YAMLError as exc: print(exc) - with open(os.path.join(OPERATIONS_PATH, category, file_name + LOG_FILE_EXT), "r") as content: + with open(log_path, "r") as content: logs = content.read() logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] infos['logs'] = logs - infos['name'] = " ".join(operation[-2:]) - infos['file_name'] = file_name + METADATA_FILE_EXT - infos['path'] = os.path.join(OPERATIONS_PATH, category, file_name) + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename + infos['log_path'] = log_path result['operations'].append(infos) if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): From 6c7fb0cef2e25cb6d2044a8f3e078604ef182e8e Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 13 Apr 2018 14:28:58 +0200 Subject: [PATCH 0684/1066] Fix n usage (#446) * Fix n usage * Fix ynh_remove_nodejs as well --- data/helpers.d/nodejs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index ff69cacd2..156507c3c 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -1,5 +1,5 @@ n_install_dir="/opt/node_n" -node_version_path="/opt/node_n/n/versions/node" +node_version_path="$n_install_dir/n/versions/node" # N_PREFIX is the directory of n, it needs to be loaded as a environment variable. export N_PREFIX="$n_install_dir" @@ -29,28 +29,21 @@ SOURCE_SUM=2ba3c9d4dd3c7e38885b37e02337906a1ee91febe6d5c9159d89a9050f2eea8f" > " # - $nodejs_path: The absolute path of node for the chosen version. # - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml. # And 2 alias stored in variables: -# - $nodejs_use_version: An alias to load a node version in the current shell. Especially useful for systemd. +# - $nodejs_use_version: An old variable, not used anymore. Keep here to not break old apps # NB: $PATH will contain the path to node, it has to be propagated to any other shell which needs to use it. -# - $ynh_node_exec: An alias to execute a command with node. +# That's means it has to be added to any systemd script. # # usage: ynh_use_nodejs ynh_use_nodejs () { nodejs_version=$(ynh_app_setting_get $app nodejs_version) - # Add "$n_install_dir/bin" to the PATH variable if it isn't already added. - # And define N_PREFIX in the current shell. - load_n_path="[[ :$PATH: == *\":$n_install_dir/bin:\"* ]] || PATH=\"$n_install_dir/bin:$PATH\"; N_PREFIX="$n_install_dir"" - - nodejs_use_version="$n_install_dir/bin/n -q $nodejs_version" - - # "Load" a version of node - eval $load_n_path; $nodejs_use_version + nodejs_use_version="echo \"Deprecated command, should be removed\"" # Get the absolute path of this version of node - nodejs_path="$(n bin $nodejs_version)" + nodejs_path="$node_version_path/$nodejs_version/bin" - # Make an alias for node use - ynh_node_exec="eval $load_n_path; n use $nodejs_version" + # Load the path of this version of node in $PATH + [[ :$PATH: == *":$nodejs_path"* ]] || PATH="$nodejs_path:$PATH" } # Install a specific version of nodejs @@ -81,7 +74,7 @@ ynh_install_nodejs () { test -x /usr/bin/npm && mv /usr/bin/npm /usr/bin/npm_n # If n is not previously setup, install it - if ! test $(n --version) > /dev/null 2>&1 + if ! test $(n --version > /dev/null 2>&1) then ynh_install_n fi @@ -129,7 +122,7 @@ ynh_install_nodejs () { # # usage: ynh_remove_nodejs ynh_remove_nodejs () { - ynh_use_nodejs + nodejs_version=$(ynh_app_setting_get $app nodejs_version) # Remove the line for this app sed --in-place "/$YNH_APP_ID:$nodejs_version/d" "$n_install_dir/ynh_app_version" @@ -137,7 +130,7 @@ ynh_remove_nodejs () { # If no other app uses this version of nodejs, remove it. if ! grep --quiet "$nodejs_version" "$n_install_dir/ynh_app_version" then - n rm $nodejs_version + $n_install_dir/bin/n rm $nodejs_version fi # If no other app uses n, remove n From ab2b7db0cfd452fc15d2a3634bd55eae3e6e6b39 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Fri, 13 Apr 2018 14:31:36 +0200 Subject: [PATCH 0685/1066] Trigger error if app dependency install fails (Redmine 1006) + allow for 'or' in dependencies (#381) * Solve issue https://dev.yunohost.org/issues/1006 I purpose this change to improve the helper 'ynh_install_app_dependencies'. Before this change if the dependences are not installable the install didn't fail. By these change the helper generate an error and the install stop. * Get the error if apt fail * Remove old change * Add dependence choice --- data/helpers.d/package | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 4d147488c..a1d29651e 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -109,7 +109,7 @@ ynh_package_install_from_equivs () { && equivs-build ./control 1>/dev/null \ && sudo dpkg --force-depends \ -i "./${pkgname}_${pkgversion}_all.deb" 2>&1 \ - && ynh_package_install -f) + && ynh_package_install -f) || ynh_die "Unable to install dependencies" [[ -n "$TMPDIR" ]] && rm -rf $TMPDIR # Remove the temp dir. # check if the package is actually installed @@ -121,8 +121,13 @@ ynh_package_install_from_equivs () { # # usage: ynh_install_app_dependencies dep [dep [...]] # | arg: dep - the package name to install in dependence +# You can give a choice between some package with this syntax : "dep1|dep2" +# Example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5" +# This mean in the dependence tree : dep1 & dep2 & (dep3 | dep4 | dep5) ynh_install_app_dependencies () { local dependencies=$@ + local dependencies=${dependencies// /, } + local dependencies=${dependencies//|/ | } local manifest_path="../manifest.json" if [ ! -e "$manifest_path" ]; then manifest_path="../settings/manifest.json" # Into the restore script, the manifest is not at the same place @@ -139,7 +144,7 @@ Section: misc Priority: optional Package: ${dep_app}-ynh-deps Version: ${version} -Depends: ${dependencies// /, } +Depends: ${dependencies} Architecture: all Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. @@ -158,4 +163,4 @@ EOF ynh_remove_app_dependencies () { local dep_app=${app//_/-} # Replace all '_' by '-' ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. -} +} \ No newline at end of file From 650b768229e62a0c5b1bada0e6cb7b0584b5ff33 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:04:09 +0200 Subject: [PATCH 0686/1066] [enh] Replace category by related_to fields --- data/actionsmap/yunohost.yml | 10 +- src/yunohost/app.py | 17 ++-- src/yunohost/dyndns.py | 27 +++--- src/yunohost/log.py | 175 ++++++++++++++++------------------- src/yunohost/tools.py | 11 +-- src/yunohost/user.py | 6 +- 6 files changed, 118 insertions(+), 128 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index b30167b18..e57d44716 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,16 +1614,18 @@ log: action_help: List logs api: GET /logs arguments: - -l: + -l: full: --limit - help: Maximum number of logs per categories + help: Maximum number of logs type: int + --full: + help: Show more details + action: store_true ### log_display() display: action_help: Display a log content api: GET /logs/ arguments: - file_name_list: + file_name: help: Log filenames for which to display the content - nargs: "*" diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f83a11222..df36ccdf7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -424,7 +424,7 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -482,6 +482,8 @@ def app_change_url(uo, auth, app, domain, path): env_dict["YNH_APP_NEW_DOMAIN"] = domain env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") + if domain != old_domain: + uo.related_to.append(('domain', old_domain)) uo.extra.update({'env': env_dict}) uo.start() @@ -619,7 +621,8 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) # Start register change on system - uo = UnitOperation('app_upgrade', 'app', app_instance_name, env=env_dict) + related_to = [('app', app_instance_name)] + uo = UnitOperation('app_upgrade', related_to, env=env_dict) # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) @@ -669,7 +672,7 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -794,7 +797,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False # Execute remove script uo_remove = UnitOperation('remove_on_failed_install', - 'app', app_instance_name, + [('app', app_instance_name)], env=env_dict_remove) remove_retcode = hook_exec( @@ -843,7 +846,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False hook_callback('post_app_install', args=args_list, env=env_dict) -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_remove(uo, auth, app): """ Remove app @@ -1051,7 +1054,7 @@ def app_debug(app): } -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app @@ -1069,7 +1072,7 @@ def app_makedefault(uo, auth, app, domain=None): if domain is None: domain = app_domain - uo.related_to['domain']=[domain] + uo.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 739f2da9e..438116fb1 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -114,7 +114,7 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -@is_unit_operation('domain', lazy=True) +@is_unit_operation(auto=False) def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -127,9 +127,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= """ if domain is None: domain = _get_maindomain() - - uo.on = [domain] - uo.related_to['domain'] = [domain] + uo.related_to.append(('domain', domain)) uo.start() # Verify if domain is provided by subscribe_host @@ -176,7 +174,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= dyndns_installcron() -@is_unit_operation('domain',lazy=True) +@is_unit_operation(auto=False) def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ @@ -219,21 +217,24 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") - uo.on = [domain] - uo.related_to['domain'] = [domain] - uo.start() + if domain is not None: + uo.related_to.append(('domain', domain)) # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) + uo.related_to.append(('domain', domain)) + uo.start() # If key is not given, pick the first file we find with the domain given - elif key is None: - keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) + else: + uo.start() + if key is None: + keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) - if not keys: - raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) + if not keys: + raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) - key = keys[0] + key = keys[0] # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 51fa85fa1..2b9c17a37 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -43,154 +43,140 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(limit=None): +def log_list(limit=None, full=False): """ List available logs Keyword argument: - limit -- Maximum number of logs per categories + limit -- Maximum number of logs """ - result = {"categories": []} + result = {"operations": []} if not os.path.exists(OPERATIONS_PATH): return result - for category in sorted(os.listdir(OPERATIONS_PATH)): - result["categories"].append({"name": category, "operations": []}) - for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): + operations = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(OPERATIONS_PATH)) + operations = reversed(sorted(operations)) - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, category, md_filename) + if limit is not None: + operations = operations[:limit] - operation = base_filename.split("-") + for operation in operations: - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") + base_filename = operation[:-len(METADATA_FILE_EXT)] + md_filename = operation + md_path = os.path.join(OPERATIONS_PATH, md_filename) - result["categories"][-1]["operations"].append({ - "started_at": operation_datetime, - "description": m18n.n("log_" + operation[2], *operation[3:]), - "name": base_filename, - "path": md_path, - }) + operation = base_filename.split("-") - result["categories"][-1]["operations"] = list(reversed(sorted(result["categories"][-1]["operations"], key=lambda x: x["started_at"]))) + operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") - if limit is not None: - result["categories"][-1]["operations"] = result["categories"][-1]["operations"][:limit] + result["operations"].append({ + "started_at": operation_datetime, + "description": m18n.n("log_" + operation[2], *operation[3:]), + "name": base_filename, + "path": md_path, + }) return result -def log_display(file_name_list): +def log_display(file_name): """ Display full log or specific logs listed Argument: - file_name_list + file_name """ - if not os.path.exists(OPERATIONS_PATH): + if file_name.endswith(METADATA_FILE_EXT): + base_filename = file_name[:-len(METADATA_FILE_EXT)] + elif file_name.endswith(LOG_FILE_EXT): + base_filename = file_name[:-len(LOG_FILE_EXT)] + else: + base_filename = file_name + md_filename = base_filename + METADATA_FILE_EXT + md_path = os.path.join(OPERATIONS_PATH, md_filename) + log_filename = base_filename + LOG_FILE_EXT + log_path = os.path.join(OPERATIONS_PATH, log_filename) + operation = base_filename.split("-") + + if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, - m18n.n('log_does_exists', log=" ".join(file_name_list))) + m18n.n('log_does_exists', log=file_name)) + infos = {} + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename - result = {"operations": []} + if os.path.exists(md_path): + with open(md_path, "r") as md_file: + try: + metadata = yaml.safe_load(md_file) + infos['metadata_path'] = md_path + infos['metadata'] = metadata + except yaml.YAMLError as exc: + print(exc) - for category in os.listdir(OPERATIONS_PATH): - for operation in filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(os.path.join(OPERATIONS_PATH, category))): - if operation not in file_name_list and file_name_list: - continue - - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, category, md_filename) - log_filename = base_filename + LOG_FILE_EXT - log_path = os.path.join(OPERATIONS_PATH, category, log_filename) - operation = base_filename.split("-") - - with open(md_path, "r") as md_file: - try: - infos = yaml.safe_load(md_file) - except yaml.YAMLError as exc: - print(exc) - - with open(log_path, "r") as content: - logs = content.read() - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - infos['logs'] = logs - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), - infos['name'] = base_filename + if os.path.exists(log_path): + with open(log_path, "r") as content: + logs = content.read() + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] infos['log_path'] = log_path - result['operations'].append(infos) + infos['logs'] = logs - if len(file_name_list) > 0 and len(result['operations']) < len(file_name_list): - logger.error(m18n.n('log_does_exists', log="', '".join(file_name_list))) + return infos - if len(result['operations']) > 0: - result['operations'] = sorted(result['operations'], key=lambda operation: operation['started_at']) - return result - -def is_unit_operation(categorie=None, operation_key=None, lazy=False): +def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): def decorate(func): def func_wrapper(*args, **kwargs): - cat = categorie + entities_list = entities.split(',') + exclude_list = exclude.split(',') op_key = operation_key - on = None - related_to = {} - inject = lazy - to_start = not lazy + related_to = [] - if cat is None: - cat = func.__module__.split('.')[1] if op_key is None: op_key = func.__name__ - if cat in kwargs: - on = kwargs[cat] - for r_category in RELATED_CATEGORIES: - if r_category in kwargs and kwargs[r_category] is not None: - if r_category not in related_to: - related_to[r_category] = [] - if isinstance(kwargs[r_category], basestring): - related_to[r_category] += [kwargs[r_category]] + + for entity in entities_list: + entity = entity.split(':') + entity_type = entity[-1] + entity = entity[0] + if entity in kwargs and kwargs[entity] is not None: + if isinstance(kwargs[entity], basestring): + related_to.append({entity_type: kwargs[entity]}) else: - related_to[r_category] += kwargs[r_category] + for x in kwargs[entity]: + related_to.append({entity_type: kwargs[x]}) + context = kwargs.copy() - if 'auth' in context: - context.pop('auth', None) - uo = UnitOperation(op_key, cat, on, related_to, args=context) - if to_start: + for field in exclude_list: + if field in context: + context.pop(field, None) + uo = UnitOperation(op_key, related_to, args=context) + if auto: uo.start() try: - if inject: + if not auto: args = (uo,) + args result = func(*args, **kwargs) finally: - if uo.started_at is not None: - uo.close(exc_info()[0]) + uo.close(exc_info()[0]) return result return func_wrapper return decorate class UnitOperation(object): - def __init__(self, operation, category, on=None, related_to=None, **kwargs): + def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation self.operation = operation - self.category = category - self.on = on - if isinstance(self.on, basestring): - self.on = [self.on] - self.related_to = related_to - if related_to is None: - if self.category in RELATED_CATEGORIES: - self.related_to = {self.category: self.on} self.extra = kwargs self.started_at = None self.ended_at = None self.logger = None - self.path = os.path.join(OPERATIONS_PATH, category) + self.path = OPERATIONS_PATH if not os.path.exists(self.path): os.makedirs(self.path) @@ -220,8 +206,8 @@ class UnitOperation(object): def name(self): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] - if self.on is not None: - name += self.on + if self.related_to: + name += self.related_to[0].values() return '-'.join(name) @property @@ -229,10 +215,9 @@ class UnitOperation(object): data = { 'started_at': self.started_at, 'operation': self.operation, - 'related_to': self.related_to } - if self.on is not None: - data['on'] = self.on + if self.related_to is not None: + data['related_to'] = self.related_to if self.ended_at is not None: data['ended_at'] = self.ended_at data['success'] = self._success diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ba942d6e4..eefe2d28d 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -139,7 +139,7 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -@is_unit_operation('domain', lazy=True) +@is_unit_operation(auto=False) def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -157,8 +157,7 @@ def tools_maindomain(uo, auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.on = [new_domain] - uo.related_to['domain'] = [new_domain] + uo.related_to.append(('domain', new_domain)) uo.start() # Apply changes to ssl certs @@ -471,7 +470,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -711,7 +710,7 @@ def tools_port_available(port): return False -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_shutdown(uo, force=False): shutdown = force if not shutdown: @@ -730,7 +729,7 @@ def tools_shutdown(uo, force=False): subprocess.check_call(['systemctl', 'poweroff']) -@is_unit_operation(lazy=True) +@is_unit_operation(auto=False) def tools_reboot(uo, force=False): reboot = force if not reboot: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 08946633e..2b9a70463 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -90,7 +90,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation() +@is_unit_operation('username:user') def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -212,7 +212,7 @@ def user_create(auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) -@is_unit_operation() +@is_unit_operation('username:user') def user_delete(auth, username, purge=False): """ Delete user @@ -248,7 +248,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation() +@is_unit_operation('username:user', exclude='auth,change_password') def user_update(auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From 2c30bb89659c38a7bb0a5772c6064cb42285737e Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:14:14 +0200 Subject: [PATCH 0687/1066] [enh] Configure unit operations on cert management --- locales/en.json | 3 +++ src/yunohost/certificate.py | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index 47b3eefee..930722a9e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,6 +217,9 @@ "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", "log_dyndns_update": "Update the ip associated with your YunoHost subdomain '{}'", + "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", + "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", + "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", "log_user_update": "Update information of '{}' user", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a242f51b0..69043bdbd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -49,7 +49,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation logger = getActionLogger('yunohost.certmanager') @@ -162,6 +162,10 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: + uo = UnitOperation('selfsigned_cert_install', [{'domain', domain}], + args={'force': force}) + uo.start() + # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % ( @@ -240,9 +244,11 @@ def _certificate_install_selfsigned(domain_list, force=False): if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + uo.success() else: - logger.error( - "Installation of self-signed certificate installation for %s failed !", domain) + msg = "Installation of self-signed certificate installation for %s failed !" % (domain) + logger.error(msg) + uo.error(msg) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -281,6 +287,11 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: + uo = UnitOperation('letsencrypt_cert_install', [{'domain', domain}], + args={'force': force, 'no_checks': no_checks, + 'staging': staging}) + uo.start() + logger.info( "Now attempting install of certificate for domain %s!", domain) @@ -295,9 +306,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.success( m18n.n("certmanager_cert_install_success", domain=domain)) - except Exception as e: - logger.error("Certificate installation for %s failed !\nException: %s", domain, e) + uo.success() + except Exception as e: + msg = "Certificate installation for %s failed !\nException: %s" % (domain, e) + logger.error(msg) + uo.error(msg) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ @@ -373,6 +387,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: + + uo = UnitOperation('letsencrypt_cert_renew', [{'domain', domain}], + args={'force': force, 'no_checks': no_checks, + 'staging': staging, 'email': email}) + uo.start() + logger.info( "Now attempting renewing of certificate for domain %s !", domain) @@ -385,12 +405,16 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.success( m18n.n("certmanager_cert_renew_success", domain=domain)) + uo.success() + except Exception as e: import traceback from StringIO import StringIO stack = StringIO() traceback.print_exc(file=stack) - logger.error("Certificate renewing for %s failed !", domain) + msg = "Certificate renewing for %s failed !" % (domain) + logger.error(msg) + uo.error(msg) logger.error(stack.getvalue()) logger.error(str(e)) @@ -398,7 +422,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e, stack.getvalue()) - ############################################################################### # Back-end stuff # ############################################################################### From bb03e90d162e32ad0b1f3a6665f80385ff983ad0 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:29:41 +0200 Subject: [PATCH 0688/1066] [enh] Support other log path in metadata --- src/yunohost/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 2b9c17a37..736f9e795 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -115,6 +115,8 @@ def log_display(file_name): metadata = yaml.safe_load(md_file) infos['metadata_path'] = md_path infos['metadata'] = metadata + if 'log_path' in metadata: + log_path = metadata['log_path'] except yaml.YAMLError as exc: print(exc) From d17193b404fbb6bb4577a7d21e52358dd5eeae5d Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 04:55:20 +0200 Subject: [PATCH 0689/1066] [enh] Limit number of line per log --- src/yunohost/log.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 736f9e795..bb0be6d0d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -82,12 +82,13 @@ def log_list(limit=None, full=False): return result -def log_display(file_name): +def log_display(file_name, number=50): """ Display full log or specific logs listed Argument: file_name + number """ if file_name.endswith(METADATA_FILE_EXT): @@ -96,6 +97,7 @@ def log_display(file_name): base_filename = file_name[:-len(LOG_FILE_EXT)] else: base_filename = file_name + md_filename = base_filename + METADATA_FILE_EXT md_path = os.path.join(OPERATIONS_PATH, md_filename) log_filename = base_filename + LOG_FILE_EXT @@ -121,11 +123,11 @@ def log_display(file_name): print(exc) if os.path.exists(log_path): - with open(log_path, "r") as content: - logs = content.read() - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs.split("\n") if x] - infos['log_path'] = log_path - infos['logs'] = logs + from yunohost.service import _tail + logs = _tail(log_path, int(number)) + logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] + infos['log_path'] = log_path + infos['logs'] = logs return infos From 1b62e9425d276691605ab7ba620b3d2be923dded Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 18:11:50 +0200 Subject: [PATCH 0690/1066] [enh] Allow to display part of log specified with path --- data/actionsmap/yunohost.yml | 9 +++++++-- src/yunohost/log.py | 17 ++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index e57d44716..2150aa7d3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1625,7 +1625,12 @@ log: ### log_display() display: action_help: Display a log content - api: GET /logs/ + api: GET /logs/ arguments: file_name: - help: Log filenames for which to display the content + help: Log filename which to display the content + -n: + full: --number + help: Number of lines to display + default: 50 + type: int diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bb0be6d0d..86e2f9267 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -98,18 +98,21 @@ def log_display(file_name, number=50): else: base_filename = file_name - md_filename = base_filename + METADATA_FILE_EXT - md_path = os.path.join(OPERATIONS_PATH, md_filename) - log_filename = base_filename + LOG_FILE_EXT - log_path = os.path.join(OPERATIONS_PATH, log_filename) + base_path = base_filename + if not base_filename.startswith('/'): + base_path = os.path.join(OPERATIONS_PATH, base_filename) + + md_path = base_path + METADATA_FILE_EXT + log_path = base_path + LOG_FILE_EXT operation = base_filename.split("-") if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) infos = {} - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), - infos['name'] = base_filename + if not base_filename.startswith('/'): + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + infos['name'] = base_filename if os.path.exists(md_path): with open(md_path, "r") as md_file: @@ -125,7 +128,7 @@ def log_display(file_name, number=50): if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) - logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] + #logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] infos['log_path'] = log_path infos['logs'] = logs From b0a60b89d2dd8b0b03b274beb970bbe54e17118a Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 14 Apr 2018 18:16:56 +0200 Subject: [PATCH 0691/1066] [fix] Display operation name if exist --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 86e2f9267..c73269ba0 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -110,7 +110,7 @@ def log_display(file_name, number=50): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) infos = {} - if not base_filename.startswith('/'): + if not base_path.startswith(OPERATIONS_PATH): infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename From fa152adee6e3bb67cf5e599c37e097a58508c70a Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 17 Apr 2018 17:23:04 +0200 Subject: [PATCH 0692/1066] [enh] Display debug information on cert-install/renew failure (#447) * [mod] split function to add _get_dns_ip * [enh] display debug information on cert-install/renew failure --- src/yunohost/certificate.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d4ddeff36..2ea9b682a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -296,6 +296,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: + _display_debug_information(domain) logger.error("Certificate installation for %s failed !\nException: %s", domain, e) @@ -564,6 +565,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): 'certmanager_hit_rate_limit', domain=domain)) else: logger.error(str(e)) + _display_debug_information(domain) raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) @@ -823,7 +825,7 @@ def _check_domain_is_ready_for_ACME(domain): 'certmanager_domain_http_not_working', domain=domain)) -def _dns_ip_match_public_ip(public_ip, domain): +def _get_dns_ip(domain): try: resolver = dns.resolver.Resolver() resolver.nameservers = DNS_RESOLVERS @@ -832,9 +834,11 @@ def _dns_ip_match_public_ip(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_error_no_A_record', domain=domain)) - dns_ip = str(answers[0]) + return str(answers[0]) - return dns_ip == public_ip + +def _dns_ip_match_public_ip(public_ip, domain): + return _get_dns_ip(domain) == public_ip def _domain_is_accessible_through_HTTP(ip, domain): @@ -850,6 +854,30 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True +def _get_local_dns_ip(domain): + try: + resolver = dns.resolver.Resolver() + answers = resolver.query(domain, "A") + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): + logger.warning("Failed to resolved domain '%s' locally", domain) + return None + + return str(answers[0]) + + +def _display_debug_information(domain): + dns_ip = _get_dns_ip(domain) + public_ip = get_public_ip() + local_dns_ip = _get_local_dns_ip(domain) + + logger.warning("""\ +Debug information: + - domain ip from DNS %s + - domain ip from local DNS %s + - public ip of the server %s +""", dns_ip, local_dns_ip, public_ip) + + # FIXME / TODO : ideally this should not be needed. There should be a proper # mechanism to regularly check the value of the public IP and trigger # corresponding hooks (e.g. dyndns update and dnsmasq regen-conf) From de305ce18e84adf1835531336f38f081173a4e29 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 18 Apr 2018 04:29:11 +0200 Subject: [PATCH 0693/1066] [mod] more explicite error message for dyndns subscribe server failure --- src/yunohost/dyndns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ec3bf88c8..f6048b06e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -161,7 +161,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None try: error = json.loads(r.text)['error'] except: - error = "Server error" + error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text) raise MoulinetteError(errno.EPERM, m18n.n('dyndns_registration_failed', error=error)) From c20fa217a710a57a7d7ae002278d30d9a5ee747f Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 24 Apr 2018 00:02:07 +0200 Subject: [PATCH 0694/1066] fix bad line --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 81a76fdf3..e58f0aaab 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -100,7 +100,7 @@ ynh_backup_before_upgrade () { ynh_die "Backup failed, the upgrade process was aborted." fi else - echo "\$NO_BACKUP_UPGRADE is set, backup will be avoided. Be careful, this upgrade is going to be operated without securi$ + echo "\$NO_BACKUP_UPGRADE is set, backup will be avoided. Be careful, this upgrade is going to be operated without a security backup" fi } From b9330d7501d519fb7d28187d883f203b941a83ab Mon Sep 17 00:00:00 2001 From: pitchum Date: Thu, 26 Apr 2018 16:49:38 +0200 Subject: [PATCH 0695/1066] [enh] Lazy-load some module for perf improvements (#451) * Lazy load some python imports (perfs improved a lot). These commands became way faster: - yunohost app setting ... - yunohost app list - yunohost domain list - yunohost domain dns-conf - yunohost dyndns installcron/removecron - ... and maybe others * [fix] Timeout wat not defined anymore --- src/yunohost/app.py | 2 +- src/yunohost/certificate.py | 12 +++++++----- src/yunohost/domain.py | 1 - src/yunohost/dyndns.py | 2 +- src/yunohost/tools.py | 1 - 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ac70833f6..6ddf08f52 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -32,7 +32,6 @@ import re import urlparse import errno import subprocess -import requests import glob import pwd import grp @@ -129,6 +128,7 @@ def app_fetchlist(url=None, name=None): else: appslists_to_be_fetched = appslists.keys() + import requests # lazy loading this module for performance reasons # Fetch all appslists to be fetched for name in appslists_to_be_fetched: diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 2ea9b682a..775e726e9 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -29,14 +29,11 @@ import shutil import pwd import grp import smtplib -import requests import subprocess import dns.resolver import glob -from OpenSSL import crypto from datetime import datetime -from requests.exceptions import Timeout from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate @@ -575,9 +572,10 @@ def _fetch_and_enable_new_certificate(domain, staging=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) + import requests # lazy loading this module for performance reasons try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text - except Timeout as e: + except requests.exceptions.Timeout as e: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) # Now save the key and signed certificate @@ -626,6 +624,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): def _prepare_certificate_signing_request(domain, key_file, output_folder): + from OpenSSL import crypto # lazy loading this module for performance reasons # Init a request csr = crypto.X509Req() @@ -657,6 +656,7 @@ def _get_status(domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_no_cert_file', domain=domain, file=cert_file)) + from OpenSSL import crypto # lazy loading this module for performance reasons try: cert = crypto.load_certificate( crypto.FILETYPE_PEM, open(cert_file).read()) @@ -759,6 +759,7 @@ def _generate_account_key(): def _generate_key(destination_path): + from OpenSSL import crypto # lazy loading this module for performance reasons k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, KEY_SIZE) @@ -842,9 +843,10 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): + import requests # lazy loading this module for performance reasons try: requests.head("http://" + ip, headers={"Host": domain}, timeout=10) - except Timeout as e: + except requests.exceptions.Timeout as e: logger.warning(m18n.n('certmanager_http_check_timeout', domain=domain, ip=ip)) return False except Exception as e: diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 026c4da36..354df2887 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -28,7 +28,6 @@ import re import json import yaml import errno -import requests from moulinette import m18n, msettings from moulinette.core import MoulinetteError diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index f6048b06e..0aa6cf36c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -30,7 +30,6 @@ import glob import time import base64 import errno -import requests import subprocess from moulinette import m18n @@ -152,6 +151,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None with open(key_file) as f: key = f.readline().strip().split(' ', 6)[-1] + import requests # lazy loading this module for performance reasons # Send subscription try: r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f98d48fc5..0b6de942e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -26,7 +26,6 @@ import re import os import yaml -import requests import json import errno import logging From 0154f15d95634347dd295012ebc39ed6f0f61e28 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 20:42:57 +0200 Subject: [PATCH 0696/1066] [enh] ECDH Curves --- data/templates/nginx/server.tpl.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index ac2ff8486..5e69aeca8 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -24,6 +24,7 @@ server { ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From 8c3c9e697aa584e4c5c775b7431d4e7d79e7753c Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 20:44:44 +0200 Subject: [PATCH 0697/1066] [enh] ECDH Curves --- data/templates/nginx/plain/yunohost_admin.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 156d61bd6..e293327aa 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -19,6 +19,7 @@ server { ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From d73789c546383652309392103a9289097d69506f Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:21:25 +0200 Subject: [PATCH 0698/1066] Update server.tpl.conf --- data/templates/nginx/server.tpl.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 5e69aeca8..eb3c4a1d5 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -24,6 +24,7 @@ server { ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From 15a331ec5d6286c1abcbde50ada8fa7b0451426c Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:21:44 +0200 Subject: [PATCH 0699/1066] Update yunohost_admin.conf --- data/templates/nginx/plain/yunohost_admin.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index e293327aa..e6f7d16f7 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -19,6 +19,7 @@ server { ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From f59eed7b7ed410c1b60c32377d6012c132b02623 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:33:49 +0200 Subject: [PATCH 0700/1066] [enh] add X25519 curve --- data/templates/nginx/server.tpl.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index eb3c4a1d5..19e7d8ca6 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -24,8 +24,8 @@ server { ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; - # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS - ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 + ssl_ecdh_curve X25519:sect571r1:secp521r1:secp384r1; ssl_prefer_server_ciphers on; From 1bb65cfdf8ac09c1dc2dda9a314e1bed4e0c9396 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:34:23 +0200 Subject: [PATCH 0701/1066] [enh] add X25519 curve --- data/templates/nginx/plain/yunohost_admin.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index e6f7d16f7..76525aab6 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -19,8 +19,8 @@ server { ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; - # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS - ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 + ssl_ecdh_curve X25519:sect571r1:secp521r1:secp384r1; ssl_prefer_server_ciphers on; From 3a4ac25721cffd66847d74d35bebe393030a4029 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:40:13 +0200 Subject: [PATCH 0702/1066] Update yunohost_admin.conf --- data/templates/nginx/plain/yunohost_admin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 76525aab6..11f5d11d2 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -20,7 +20,7 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve X25519:sect571r1:secp521r1:secp384r1; + ssl_ecdh_curve X25519:secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From d497fd216a722154848bec10cd2d882f6095b810 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 28 Apr 2018 21:40:39 +0200 Subject: [PATCH 0703/1066] Update server.tpl.conf --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 19e7d8ca6..421cef712 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -25,7 +25,7 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve X25519:sect571r1:secp521r1:secp384r1; + ssl_ecdh_curve X25519:secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From 57155c94d85ade8cdbea11490b0ec792aa8573e9 Mon Sep 17 00:00:00 2001 From: frju365 Date: Tue, 1 May 2018 17:15:55 +0200 Subject: [PATCH 0704/1066] [Fix] ECDH curve not compatible --- data/templates/nginx/plain/yunohost_admin.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 11f5d11d2..b1fb0d2ef 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -20,7 +20,7 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve X25519:secp521r1:secp384r1:prime256v1; + ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From 6c07296b9f2ae617741c68eacd3e834a4bffe81c Mon Sep 17 00:00:00 2001 From: frju365 Date: Tue, 1 May 2018 17:16:27 +0200 Subject: [PATCH 0705/1066] =?UTF-8?q?[Fix]=C2=A0ECdH=20curve=20not=20compa?= =?UTF-8?q?tible=20with=20Jessie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 421cef712..88296c755 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -25,7 +25,7 @@ server { ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve X25519:secp521r1:secp384r1:prime256v1; + ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; From 90e093a482dc2bd6e0f7f0e6b08275a0c963dfad Mon Sep 17 00:00:00 2001 From: frju365 Date: Tue, 1 May 2018 23:44:23 +0200 Subject: [PATCH 0706/1066] Tweak the CSP config in nginx template for domains (#456) * Little correction for template conf for apps * Move the default-src CSP thing to report-only for now --- data/templates/nginx/server.tpl.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index ac2ff8486..495a15bdc 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -46,7 +46,8 @@ server { # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'"; + add_header Content-Security-Policy "upgrade-insecure-requests;" + add_header Content-Security-Policy-Report-Only "default-src https: data: 'unsafe-inline' 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Download-Options noopen; From 062ca32eac775802186bfa5776b58f8790c7accb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 1 May 2018 23:45:12 +0200 Subject: [PATCH 0707/1066] Move ssh commands into a subcategory of 'user' + fix a few bugs (#445) * Move all ssh commands to a subcategory in user... * [fix] Actionmap didn't match functions input ? * [fix] Invalidate nscd cache to propagate new loginShell * Consistency of list-keys even if there's no key.. --- data/actionsmap/yunohost.yml | 164 +++++++++++++++-------------------- src/yunohost/ssh.py | 113 ++++++++++++++++++++++-- src/yunohost/user.py | 95 ++++---------------- 3 files changed, 197 insertions(+), 175 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 77887b41a..31fb0e6cf 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -203,30 +203,6 @@ user: extra: pattern: *pattern_mailbox_quota - ### ssh_user_enable_ssh() - allow-ssh: - action_help: Allow the user to uses ssh - api: POST /ssh/user/enable-ssh - configuration: - authenticate: all - arguments: - username: - help: Username of the user - extra: - pattern: *pattern_username - - ### ssh_user_disable_ssh() - disallow-ssh: - action_help: Disallow the user to uses ssh - api: POST /ssh/user/disable-ssh - configuration: - authenticate: all - arguments: - username: - help: Username of the user - extra: - pattern: *pattern_username - ### user_info() info: action_help: Get user information @@ -238,6 +214,78 @@ user: username: help: Username or email to get information + subcategories: + + ssh: + subcategory_help: Manage ssh access + actions: + ### user_ssh_enable() + allow: + action_help: Allow the user to uses ssh + api: POST /users/ssh/enable + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + + ### user_ssh_disable() + disallow: + action_help: Disallow the user to uses ssh + api: POST /users/ssh/disable + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + + ### user_ssh_keys_list() + list-keys: + action_help: Show user's authorized ssh keys + api: GET /users/ssh/keys + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + + ### user_ssh_keys_add() + add-key: + action_help: Add a new authorized ssh key for this user + api: POST /users/ssh/key + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + key: + help: The key to be added + -c: + full: --comment + help: Optionnal comment about the key + + ### user_ssh_keys_remove() + remove-key: + action_help: Remove an authorized ssh key for this user + api: DELETE /users/ssh/key + configuration: + authenticate: all + arguments: + username: + help: Username of the user + extra: + pattern: *pattern_username + key: + help: The key to be removed + ############################# # Domain # @@ -1349,74 +1397,6 @@ dyndns: api: DELETE /dyndns/cron -############################# -# SSH # -############################# -ssh: - category_help: Manage ssh keys and access - actions: {} - subcategories: - authorized-keys: - subcategory_help: Manage user's authorized ssh keys - - actions: - ### ssh_authorized_keys_list() - list: - action_help: Show user's authorized ssh keys - api: GET /ssh/authorized-keys - configuration: - authenticate: all - arguments: - username: - help: Username of the user - extra: - pattern: *pattern_username - - ### ssh_authorized_keys_add() - add: - action_help: Add a new authorized ssh key for this user - api: POST /ssh/authorized-keys - configuration: - authenticate: all - arguments: - username: - help: Username of the user - extra: - pattern: *pattern_username - -u: - full: --public - help: Public key - extra: - required: True - -i: - full: --private - help: Private key - extra: - required: True - -n: - full: --name - help: Key name - extra: - required: True - - ### ssh_authorized_keys_remove() - remove: - action_help: Remove an authorized ssh key for this user - api: DELETE /ssh/authorized-keys - configuration: - authenticate: all - arguments: - username: - help: Username of the user - extra: - pattern: *pattern_username - -k: - full: --key - help: Key as a string - extra: - required: True - - ############################# # Tools # ############################# diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index 5f1f33b55..5ddebfc2f 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -1,13 +1,57 @@ # encoding: utf-8 +import re import os +import errno +import pwd +import subprocess +from moulinette import m18n +from moulinette.core import MoulinetteError from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir -from yunohost.user import _get_user_for_ssh +SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" -def ssh_authorized_keys_list(auth, username): +def user_ssh_allow(auth, username): + """ + Allow YunoHost user connect as ssh. + + Keyword argument: + username -- User username + """ + # TODO it would be good to support different kind of shells + + if not _get_user_for_ssh(auth, username): + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + + auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'}) + + # Somehow this is needed otherwise the PAM thing doesn't forget about the + # old loginShell value ? + subprocess.call(['nscd', '-i', 'passwd']) + + +def user_ssh_disallow(auth, username): + """ + Disallow YunoHost user connect as ssh. + + Keyword argument: + username -- User username + """ + # TODO it would be good to support different kind of shells + + if not _get_user_for_ssh(auth, username): + raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + + auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'}) + + # Somehow this is needed otherwise the PAM thing doesn't forget about the + # old loginShell value ? + subprocess.call(['nscd', '-i', 'passwd']) + + +def user_ssh_list_keys(auth, username): user = _get_user_for_ssh(auth, username, ["homeDirectory"]) if not user: raise Exception("User with username '%s' doesn't exists" % username) @@ -15,7 +59,7 @@ def ssh_authorized_keys_list(auth, username): authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys") if not os.path.exists(authorized_keys_file): - return [] + return {"keys": []} keys = [] last_comment = "" @@ -40,7 +84,7 @@ def ssh_authorized_keys_list(auth, username): return {"keys": keys} -def ssh_authorized_keys_add(auth, username, key, comment): +def user_ssh_add_key(auth, username, key, comment): user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"]) if not user: raise Exception("User with username '%s' doesn't exists" % username) @@ -74,8 +118,8 @@ def ssh_authorized_keys_add(auth, username, key, comment): write_to_file(authorized_keys_file, authorized_keys_content) -def ssh_authorized_keys_remove(auth, username, key): - user = _get_user(auth, username, ["homeDirectory", "uid"]) +def user_ssh_remove_key(auth, username, key): + user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"]) if not user: raise Exception("User with username '%s' doesn't exists" % username) @@ -100,3 +144,60 @@ def ssh_authorized_keys_remove(auth, username, key): authorized_keys_content = authorized_keys_content.replace(key, "") write_to_file(authorized_keys_file, authorized_keys_content) + +# +# Helpers +# + + +def _get_user_for_ssh(auth, username, attrs=None): + def ssh_root_login_status(auth): + # XXX temporary placed here for when the ssh_root commands are integrated + # extracted from https://github.com/YunoHost/yunohost/pull/345 + # XXX should we support all the options? + # this is the content of "man sshd_config" + # PermitRootLogin + # Specifies whether root can log in using ssh(1). The argument must be + # “yes”, “without-password”, “forced-commands-only”, or “no”. The + # default is “yes”. + sshd_config_content = read_file(SSHD_CONFIG_PATH) + + if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$", + sshd_config_content, re.MULTILINE): + return {"PermitRootLogin": False} + + return {"PermitRootLogin": True} + + if username == "root": + root_unix = pwd.getpwnam("root") + return { + 'username': 'root', + 'fullname': '', + 'mail': '', + 'ssh_allowed': ssh_root_login_status(auth)["PermitRootLogin"], + 'shell': root_unix.pw_shell, + 'home_path': root_unix.pw_dir, + } + + if username == "admin": + admin_unix = pwd.getpwnam("admin") + return { + 'username': 'admin', + 'fullname': '', + 'mail': '', + 'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false", + 'shell': admin_unix.pw_shell, + 'home_path': admin_unix.pw_dir, + } + + # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html + user = auth.search('ou=users,dc=yunohost,dc=org', + '(&(objectclass=person)(uid=%s))' % username, + attrs) + + assert len(user) in (0, 1) + + if not user: + return None + + return user[0] diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 793ccaf7a..bed5fb8c8 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -41,9 +41,6 @@ from yunohost.service import service_status logger = getActionLogger('yunohost.user') -SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" - - def user_list(auth, fields=None): """ List users @@ -446,36 +443,30 @@ def user_info(auth, username): else: raise MoulinetteError(167, m18n.n('user_info_failed')) +# +# SSH subcategory +# +# +import yunohost.ssh -def user_allow_ssh(auth, username): - """ - Allow YunoHost user connect as ssh. +def user_ssh_allow(auth, username): + return yunohost.ssh.user_ssh_allow(auth, username) - Keyword argument: - username -- User username - """ - # TODO it would be good to support different kind of shells +def user_ssh_disallow(auth, username): + return yunohost.ssh.user_ssh_disallow(auth, username) - if not _get_user_for_ssh(auth, username): - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) +def user_ssh_list_keys(auth, username): + return yunohost.ssh.user_ssh_list_keys(auth, username) - auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'}) +def user_ssh_add_key(auth, username, key, comment): + return yunohost.ssh.user_ssh_add_key(auth, username, key, comment) +def user_ssh_remove_key(auth, username, key): + return yunohost.ssh.user_ssh_remove_key(auth, username, key) -def user_disallow_ssh(auth, username): - """ - Disallow YunoHost user connect as ssh. - - Keyword argument: - username -- User username - """ - # TODO it would be good to support different kind of shells - - if not _get_user_for_ssh(auth, username) : - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) - - auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'}) - +# +# End SSH subcategory +# def _convertSize(num, suffix=''): for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: @@ -514,54 +505,4 @@ def _hash_user_password(password): return '{CRYPT}' + crypt.crypt(str(password), salt) -def _get_user_for_ssh(auth, username, attrs=None): - def ssh_root_login_status(auth): - # XXX temporary placed here for when the ssh_root commands are integrated - # extracted from https://github.com/YunoHost/yunohost/pull/345 - # XXX should we support all the options? - # this is the content of "man sshd_config" - # PermitRootLogin - # Specifies whether root can log in using ssh(1). The argument must be - # “yes”, “without-password”, “forced-commands-only”, or “no”. The - # default is “yes”. - sshd_config_content = read_file(SSHD_CONFIG_PATH) - if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$", - sshd_config_content, re.MULTILINE): - return {"PermitRootLogin": False} - - return {"PermitRootLogin": True} - - if username == "root": - root_unix = pwd.getpwnam("root") - return { - 'username': 'root', - 'fullname': '', - 'mail': '', - 'ssh_allowed': ssh_root_login_status(auth)["PermitRootLogin"], - 'shell': root_unix.pw_shell, - 'home_path': root_unix.pw_dir, - } - - if username == "admin": - admin_unix = pwd.getpwnam("admin") - return { - 'username': 'admin', - 'fullname': '', - 'mail': '', - 'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false", - 'shell': admin_unix.pw_shell, - 'home_path': admin_unix.pw_dir, - } - - # TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html - user = auth.search('ou=users,dc=yunohost,dc=org', - '(&(objectclass=person)(uid=%s))' % username, - attrs) - - assert len(user) in (0, 1) - - if not user: - return None - - return user[0] From 9d249e6eb95ee5509046ddf37909b902753f37af Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 7 Feb 2018 23:18:22 +0000 Subject: [PATCH 0708/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 99.7% (366 of 367 strings) --- locales/ar.json | 366 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/locales/ar.json b/locales/ar.json index 2c63c0851..81b73f5ec 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,2 +1,368 @@ { + "action_invalid": "Invalid action '{action:s}'", + "admin_password": "كلمة السر الإدارية", + "admin_password_change_failed": "Unable to change password", + "admin_password_changed": "The administration password has been changed", + "app_already_installed": "{app:s} is already installed", + "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", + "app_already_up_to_date": "{app:s} is already up to date", + "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", + "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", + "app_argument_required": "Argument '{name:s}' is required", + "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", + "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", + "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", + "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", + "app_extraction_failed": "Unable to extract installation files", + "app_id_invalid": "Invalid app id", + "app_incompatible": "The app {app} is incompatible with your YunoHost version", + "app_install_files_invalid": "Invalid installation files", + "app_location_already_used": "The app '{app}' is already installed on that location ({path})", + "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", + "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", + "app_location_unavailable": "This url is not available or conflicts with an already installed app", + "app_manifest_invalid": "Invalid app manifest: {error}", + "app_no_upgrade": "البرمجيات لا تحتاج إلى تحديث", + "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", + "app_not_installed": "{app:s} is not installed", + "app_not_properly_removed": "{app:s} has not been properly removed", + "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", + "app_removed": "{app:s} has been removed", + "app_requirements_checking": "Checking required packages for {app}...", + "app_requirements_failed": "Unable to meet requirements for {app}: {error}", + "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", + "app_sources_fetch_failed": "Unable to fetch sources files", + "app_unknown": "Unknown app", + "app_unsupported_remote_type": "Unsupported remote type used for the app", + "app_upgrade_app_name": "Upgrading app {app}...", + "app_upgrade_failed": "Unable to upgrade {app:s}", + "app_upgrade_some_app_failed": "Unable to upgrade some applications", + "app_upgraded": "{app:s} has been upgraded", + "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", + "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", + "appslist_fetched": "The application list {appslist:s} has been fetched", + "appslist_migrating": "Migrating application list {appslist:s} ...", + "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", + "appslist_removed": "The application list {appslist:s} has been removed", + "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", + "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", + "appslist_unknown": "Application list {appslist:s} unknown.", + "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", + "ask_current_admin_password": "كلمة السر الإدارية الحالية", + "ask_email": "عنوان البريد الإلكتروني", + "ask_firstname": "First name", + "ask_lastname": "Last name", + "ask_list_to_remove": "List to remove", + "ask_main_domain": "النطاق الرئيسي", + "ask_new_admin_password": "كلمة السر الإدارية الجديدة", + "ask_password": "كلمة السر", + "ask_path": "المسار", + "backup_abstract_method": "This backup method hasn't yet been implemented", + "backup_action_required": "You must specify something to save", + "backup_app_failed": "Unable to back up the app '{app:s}'", + "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", + "backup_applying_method_copy": "Copying all files to backup...", + "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", + "backup_applying_method_tar": "Creating the backup tar archive...", + "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", + "backup_archive_broken_link": "Unable to access backup archive (broken link to {path:s})", + "backup_archive_mount_failed": "Mounting the backup archive failed", + "backup_archive_name_exists": "The backup's archive name already exists", + "backup_archive_name_unknown": "Unknown local backup archive named '{name:s}'", + "backup_archive_open_failed": "Unable to open the backup archive", + "backup_archive_system_part_not_available": "System part '{part:s}' not available in this backup", + "backup_archive_writing_error": "Unable to add files to backup into the compressed archive", + "backup_ask_for_copying_if_needed": "Some files couldn't be prepared to be backuped using the method that avoid to temporarily waste space on the system. To perform the backup, {size:s}MB should be used temporarily. Do you agree?", + "backup_borg_not_implemented": "Borg backup method is not yet implemented", + "backup_cant_mount_uncompress_archive": "Unable to mount in readonly mode the uncompress archive directory", + "backup_cleaning_failed": "Unable to clean-up the temporary backup directory", + "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", + "backup_couldnt_bind": "Couldn't bind {src:s} to {dest:s}.", + "backup_created": "تم إنشاء النسخة الإحتياطية", + "backup_creating_archive": "Creating the backup archive...", + "backup_creation_failed": "Backup creation failed", + "backup_csv_addition_failed": "Unable to add files to backup into the CSV file", + "backup_csv_creation_failed": "Unable to create the CSV file needed for future restore operations", + "backup_custom_backup_error": "Custom backup method failure on 'backup' step", + "backup_custom_mount_error": "Custom backup method failure on 'mount' step", + "backup_custom_need_mount_error": "Custom backup method failure on 'need_mount' step", + "backup_delete_error": "Unable to delete '{path:s}'", + "backup_deleted": "The backup has been deleted", + "backup_extracting_archive": "Extracting the backup archive...", + "backup_hook_unknown": "Backup hook '{hook:s}' unknown", + "backup_invalid_archive": "Invalid backup archive", + "backup_method_borg_finished": "Backup into borg finished", + "backup_method_copy_finished": "Backup copy finished", + "backup_method_custom_finished": "Custom backup method '{method:s}' finished", + "backup_method_tar_finished": "Backup tar archive created", + "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", + "backup_nothings_done": "There is nothing to save", + "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", + "backup_output_directory_not_empty": "The output directory is not empty", + "backup_output_directory_required": "You must provide an output directory for the backup", + "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", + "backup_running_app_script": "Running backup script of app '{app:s}'...", + "backup_running_hooks": "Running backup hooks...", + "backup_system_part_failed": "Unable to backup the '{part:s}' system part", + "backup_unable_to_organize_files": "Unable to organize files in the archive with the quick method", + "backup_with_no_backup_script_for_app": "App {app:s} has no backup script. Ignoring.", + "backup_with_no_restore_script_for_app": "App {app:s} has no restore script, you won't be able to automatically restore the backup of this app.", + "certmanager_acme_not_configured_for_domain": "Certificate for domain {domain:s} does not appear to be correctly installed. Please run cert-install for this domain first.", + "certmanager_attempt_to_renew_nonLE_cert": "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically!", + "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", + "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", + "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", + "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_signing_failed": "Signing the new certificate failed", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", + "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", + "certmanager_domain_cert_not_selfsigned": "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it? (Use --force)", + "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", + "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", + "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", + "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", + "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", + "custom_appslist_name_required": "You must provide a name for your custom app list", + "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", + "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}", + "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", + "diagnosis_monitor_network_error": "Can't monitor network: {error}", + "diagnosis_monitor_system_error": "Can't monitor system: {error}", + "diagnosis_no_apps": "No installed application", + "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", + "domain_cert_gen_failed": "Unable to generate certificate", + "domain_created": "The domain has been created", + "domain_creation_failed": "Unable to create domain", + "domain_deleted": "The domain has been deleted", + "domain_deletion_failed": "Unable to delete domain", + "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", + "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", + "domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}", + "domain_dyndns_invalid": "Invalid domain to use with DynDNS", + "domain_dyndns_root_unknown": "Unknown DynDNS root domain", + "domain_exists": "Domain already exists", + "domain_hostname_failed": "Failed to set new hostname", + "domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal", + "domain_unknown": "النطاق مجهول", + "domain_zone_exists": "DNS zone file already exists", + "domain_zone_not_found": "DNS zone file not found for domain {:s}", + "domains_available": "النطاقات المتوفرة :", + "done": "تم", + "downloading": "عملية التنزيل جارية …", + "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.", + "dyndns_cron_installed": "The DynDNS cron job has been installed", + "dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job", + "dyndns_cron_removed": "The DynDNS cron job has been removed", + "dyndns_ip_update_failed": "Unable to update IP address on DynDNS", + "dyndns_ip_updated": "Your IP address has been updated on DynDNS", + "dyndns_key_generating": "DNS key is being generated, it may take a while...", + "dyndns_key_not_found": "DNS key not found for the domain", + "dyndns_no_domain_registered": "No domain has been registered with DynDNS", + "dyndns_registered": "The DynDNS domain has been registered", + "dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}", + "dyndns_domain_not_provided": "Dyndns provider {provider:s} cannot provide domain {domain:s}.", + "dyndns_unavailable": "Domain {domain:s} is not available.", + "executing_command": "Executing command '{command:s}'...", + "executing_script": "Executing script '{script:s}'...", + "extracting": "عملية فك الضغط جارية …", + "field_invalid": "Invalid field '{:s}'", + "firewall_reload_failed": "Unable to reload the firewall", + "firewall_reloaded": "The firewall has been reloaded", + "firewall_rules_cmd_failed": "Some firewall rules commands have failed. For more information, see the log.", + "format_datetime_short": "%m/%d/%Y %I:%M %p", + "global_settings_bad_choice_for_enum": "Bad value for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_bad_type_for_setting": "Bad type for setting {setting:s}, received {received_type:s}, except {expected_type:s}", + "global_settings_cant_open_settings": "Failed to open settings file, reason: {reason:s}", + "global_settings_cant_serialize_settings": "Failed to serialize settings data, reason: {reason:s}", + "global_settings_cant_write_settings": "Failed to write settings file, reason: {reason:s}", + "global_settings_key_doesnt_exists": "The key '{settings_key:s}' doesn't exists in the global settings, you can see all the available keys by doing 'yunohost settings list'", + "global_settings_reset_success": "Success. Your previous settings have been backuped in {path:s}", + "global_settings_setting_example_bool": "Example boolean option", + "global_settings_setting_example_enum": "Example enum option", + "global_settings_setting_example_int": "Example int option", + "global_settings_setting_example_string": "Example string option", + "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", + "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "hook_exec_failed": "Script execution failed: {path:s}", + "hook_exec_not_terminated": "Script execution hasn’t terminated: {path:s}", + "hook_list_by_invalid": "Invalid property to list hook by", + "hook_name_unknown": "Unknown hook name '{name:s}'", + "installation_complete": "إكتملت عملية التنصيب", + "installation_failed": "Installation failed", + "invalid_url_format": "Invalid URL format", + "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", + "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "ldap_init_failed_to_create_admin": "LDAP initialization failed to create admin user", + "ldap_initialized": "LDAP has been initialized", + "license_undefined": "undefined", + "mail_alias_remove_failed": "Unable to remove mail alias '{mail:s}'", + "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", + "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", + "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", + "maindomain_change_failed": "Unable to change the main domain", + "maindomain_changed": "The main domain has been changed", + "migrate_tsig_end": "Migration to hmac-sha512 finished", + "migrate_tsig_failed": "Migrating the dyndns domain {domain} to hmac-sha512 failed, rolling back. Error: {error_code} - {error}", + "migrate_tsig_start": "Not secure enough key algorithm detected for TSIG signature of domain '{domain}', initiating migration to the more secure one hmac-sha512", + "migrate_tsig_wait": "Let's wait 3min for the dyndns server to take the new key into account...", + "migrate_tsig_wait_2": "دقيقتين …", + "migrate_tsig_wait_3": "دقيقة واحدة …", + "migrate_tsig_wait_4": "30 ثانية …", + "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", + "migrations_backward": "Migrating backward.", + "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", + "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", + "migrations_current_target": "Migration target is {}", + "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", + "migrations_forward": "Migrating forward", + "migrations_loading_migration": "Loading migration {number} {name}...", + "migrations_migration_has_failed": "Migration {number} {name} has failed with exception {exception}, aborting", + "migrations_no_migrations_to_run": "No migrations to run", + "migrations_show_currently_running_migration": "Running migration {number} {name}...", + "migrations_show_last_migration": "Last ran migration is {}", + "migrations_skip_migration": "Skipping migration {number} {name}...", + "monitor_disabled": "The server monitoring has been disabled", + "monitor_enabled": "The server monitoring has been enabled", + "monitor_glances_con_failed": "Unable to connect to Glances server", + "monitor_not_enabled": "Server monitoring is not enabled", + "monitor_period_invalid": "Invalid time period", + "monitor_stats_file_not_found": "Statistics file not found", + "monitor_stats_no_update": "No monitoring statistics to update", + "monitor_stats_period_unavailable": "No available statistics for the period", + "mountpoint_unknown": "Unknown mountpoint", + "mysql_db_creation_failed": "MySQL database creation failed", + "mysql_db_init_failed": "MySQL database init failed", + "mysql_db_initialized": "The MySQL database has been initialized", + "network_check_mx_ko": "DNS MX record is not set", + "network_check_smtp_ko": "Outbound mail (SMTP port 25) seems to be blocked by your network", + "network_check_smtp_ok": "Outbound mail (SMTP port 25) is not blocked", + "new_domain_required": "You must provide the new main domain", + "no_appslist_found": "No app list found", + "no_internet_connection": "Server is not connected to the Internet", + "no_ipv6_connectivity": "IPv6 connectivity is not available", + "no_restore_script": "No restore script found for the app '{app:s}'", + "not_enough_disk_space": "Not enough free disk space on '{path:s}'", + "package_not_installed": "Package '{pkgname}' is not installed", + "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'", + "package_unknown": "Unknown package '{pkgname}'", + "packages_no_upgrade": "There is no package to upgrade", + "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", + "packages_upgrade_failed": "Unable to upgrade all of the packages", + "path_removal_failed": "Unable to remove path {:s}", + "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", + "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", + "pattern_email": "Must be a valid email address (e.g. someone@domain.org)", + "pattern_firstname": "Must be a valid first name", + "pattern_lastname": "Must be a valid last name", + "pattern_listname": "Must be alphanumeric and underscore characters only", + "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota", + "pattern_password": "Must be at least 3 characters long", + "pattern_port": "Must be a valid port number (i.e. 0-65535)", + "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", + "pattern_positive_number": "Must be a positive number", + "pattern_username": "Must be lower-case alphanumeric and underscore characters only", + "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections", + "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", + "port_available": "Port {port:d} is available", + "port_unavailable": "Port {port:d} is not available", + "restore_action_required": "You must specify something to restore", + "restore_already_installed_app": "An app is already installed with the id '{app:s}'", + "restore_app_failed": "Unable to restore the app '{app:s}'", + "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", + "restore_complete": "Restore complete", + "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", + "restore_extracting": "Extracting needed files from the archive...", + "restore_failed": "Unable to restore the system", + "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", + "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_mounting_archive": "Mounting archive into '{path:s}'", + "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", + "restore_nothings_done": "Nothing has been restored", + "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", + "restore_running_app_script": "Running restore script of app '{app:s}'...", + "restore_running_hooks": "Running restoration hooks...", + "restore_system_part_failed": "Unable to restore the '{part:s}' system part", + "server_shutdown": "The server will shutdown", + "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", + "server_reboot": "The server will reboot", + "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", + "service_add_failed": "Unable to add service '{service:s}'", + "service_added": "The service '{service:s}' has been added", + "service_already_started": "Service '{service:s}' has already been started", + "service_already_stopped": "Service '{service:s}' has already been stopped", + "service_cmd_exec_failed": "Unable to execute command '{command:s}'", + "service_conf_file_backed_up": "The configuration file '{conf}' has been backed up to '{backup}'", + "service_conf_file_copy_failed": "Unable to copy the new configuration file '{new}' to '{conf}'", + "service_conf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by service {service} but has been kept back.", + "service_conf_file_manually_modified": "The configuration file '{conf}' has been manually modified and will not be updated", + "service_conf_file_manually_removed": "The configuration file '{conf}' has been manually removed and will not be created", + "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", + "service_conf_file_removed": "The configuration file '{conf}' has been removed", + "service_conf_file_updated": "The configuration file '{conf}' has been updated", + "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", + "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", + "service_conf_updated": "The configuration has been updated for service '{service}'", + "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", + "service_disable_failed": "Unable to disable service '{service:s}'", + "service_disabled": "The service '{service:s}' has been disabled", + "service_enable_failed": "Unable to enable service '{service:s}'", + "service_enabled": "The service '{service:s}' has been enabled", + "service_no_log": "No log to display for service '{service:s}'", + "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", + "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}", + "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", + "service_remove_failed": "Unable to remove service '{service:s}'", + "service_removed": "The service '{service:s}' has been removed", + "service_start_failed": "Unable to start service '{service:s}'", + "service_started": "The service '{service:s}' has been started", + "service_status_failed": "Unable to determine status of service '{service:s}'", + "service_stop_failed": "Unable to stop service '{service:s}'", + "service_stopped": "The service '{service:s}' has been stopped", + "service_unknown": "Unknown service '{service:s}'", + "ssowat_conf_generated": "The SSOwat configuration has been generated", + "ssowat_conf_updated": "The SSOwat configuration has been updated", + "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", + "system_upgraded": "The system has been upgraded", + "system_username_exists": "Username already exists in the system users", + "unbackup_app": "App '{app:s}' will not be saved", + "unexpected_error": "An unexpected error occured", + "unit_unknown": "Unknown unit '{unit:s}'", + "unlimit": "No quota", + "unrestore_app": "App '{app:s}' will not be restored", + "update_cache_failed": "Unable to update APT cache", + "updating_apt_cache": "Updating the list of available packages...", + "upgrade_complete": "إكتملت عملية الترقية و التحديث", + "upgrading_packages": "Upgrading packages...", + "upnp_dev_not_found": "No UPnP device found", + "upnp_disabled": "UPnP has been disabled", + "upnp_enabled": "UPnP has been enabled", + "upnp_port_open_failed": "Unable to open UPnP ports", + "user_created": "تم إنشاء المستخدم", + "user_creation_failed": "Unable to create user", + "user_deleted": "The user has been deleted", + "user_deletion_failed": "Unable to delete user", + "user_home_creation_failed": "Unable to create user home folder", + "user_info_failed": "Unable to retrieve user information", + "user_unknown": "Unknown user: {user:s}", + "user_update_failed": "Unable to update user", + "user_updated": "The user has been updated", + "yunohost_already_installed": "YunoHost is already installed", + "yunohost_ca_creation_failed": "Unable to create certificate authority", + "yunohost_ca_creation_success": "The local certification authority has been created.", + "yunohost_configured": "YunoHost has been configured", + "yunohost_installing": "عملية تنصيب يونوهوست جارية …", + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" } From 165e3e1ca781e084db5a96b904e0531f3f57dd53 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 8 Feb 2018 21:28:33 +0000 Subject: [PATCH 0709/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 99.7% (366 of 367 strings) --- locales/ar.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 81b73f5ec..45a78883d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -269,13 +269,13 @@ "pattern_listname": "Must be alphanumeric and underscore characters only", "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota", "pattern_password": "Must be at least 3 characters long", - "pattern_port": "Must be a valid port number (i.e. 0-65535)", + "pattern_port": "يجب أن يكون رقم منفذ صالح (مثال 0-65535)", "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", - "pattern_positive_number": "Must be a positive number", + "pattern_positive_number": "يجب أن يكون عددا إيجابيا", "pattern_username": "Must be lower-case alphanumeric and underscore characters only", "port_already_closed": "Port {port:d} is already closed for {ip_version:s} connections", "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", - "port_available": "Port {port:d} is available", + "port_available": "المنفذ {port:d} متوفر", "port_unavailable": "Port {port:d} is not available", "restore_action_required": "You must specify something to restore", "restore_already_installed_app": "An app is already installed with the id '{app:s}'", @@ -335,33 +335,33 @@ "ssowat_conf_updated": "The SSOwat configuration has been updated", "ssowat_persistent_conf_read_error": "Error while reading SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", "ssowat_persistent_conf_write_error": "Error while saving SSOwat persistent configuration: {error:s}. Edit /etc/ssowat/conf.json.persistent file to fix the JSON syntax", - "system_upgraded": "The system has been upgraded", + "system_upgraded": "تمت عملية ترقية النظام", "system_username_exists": "Username already exists in the system users", "unbackup_app": "App '{app:s}' will not be saved", "unexpected_error": "An unexpected error occured", "unit_unknown": "Unknown unit '{unit:s}'", - "unlimit": "No quota", + "unlimit": "دون تحديد الحصة", "unrestore_app": "App '{app:s}' will not be restored", "update_cache_failed": "Unable to update APT cache", - "updating_apt_cache": "Updating the list of available packages...", + "updating_apt_cache": "جارٍ تحديث قائمة الحُزم المتوفرة …", "upgrade_complete": "إكتملت عملية الترقية و التحديث", - "upgrading_packages": "Upgrading packages...", + "upgrading_packages": "عملية ترقية الحُزم جارية …", "upnp_dev_not_found": "No UPnP device found", "upnp_disabled": "UPnP has been disabled", "upnp_enabled": "UPnP has been enabled", "upnp_port_open_failed": "Unable to open UPnP ports", "user_created": "تم إنشاء المستخدم", "user_creation_failed": "Unable to create user", - "user_deleted": "The user has been deleted", - "user_deletion_failed": "Unable to delete user", + "user_deleted": "تم حذف المستخدم", + "user_deletion_failed": "لا يمكن حذف المستخدم", "user_home_creation_failed": "Unable to create user home folder", "user_info_failed": "Unable to retrieve user information", - "user_unknown": "Unknown user: {user:s}", - "user_update_failed": "Unable to update user", - "user_updated": "The user has been updated", + "user_unknown": "المستخدم {user:s} مجهول", + "user_update_failed": "لا يمكن تحديث المستخدم", + "user_updated": "تم تحديث المستخدم", "yunohost_already_installed": "YunoHost is already installed", - "yunohost_ca_creation_failed": "Unable to create certificate authority", - "yunohost_ca_creation_success": "The local certification authority has been created.", + "yunohost_ca_creation_failed": "تعذرت عملية إنشاء هيئة الشهادات", + "yunohost_ca_creation_success": "تم إنشاء هيئة الشهادات المحلية.", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "عملية تنصيب يونوهوست جارية …", "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" From 9a6d5a4a8bdfc0b2281569d7ad621bd6761168c8 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 11 Feb 2018 00:42:53 +0000 Subject: [PATCH 0710/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 99.7% (366 of 367 strings) --- locales/ar.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 45a78883d..a70ee3975 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -1,9 +1,9 @@ { - "action_invalid": "Invalid action '{action:s}'", + "action_invalid": "إجراء غير صالح '{action:s}'", "admin_password": "كلمة السر الإدارية", - "admin_password_change_failed": "Unable to change password", - "admin_password_changed": "The administration password has been changed", - "app_already_installed": "{app:s} is already installed", + "admin_password_change_failed": "تعذرت عملية تعديل كلمة السر", + "admin_password_changed": "تم تعديل الكلمة السرية الإدارية", + "app_already_installed": "{app:s} تم تنصيبه مِن قبل", "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", "app_already_up_to_date": "{app:s} is already up to date", "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", @@ -32,28 +32,28 @@ "app_requirements_checking": "Checking required packages for {app}...", "app_requirements_failed": "Unable to meet requirements for {app}: {error}", "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", - "app_sources_fetch_failed": "Unable to fetch sources files", - "app_unknown": "Unknown app", + "app_sources_fetch_failed": "تعذرت عملية جلب مصادر الملفات", + "app_unknown": "برنامج مجهول", "app_unsupported_remote_type": "Unsupported remote type used for the app", - "app_upgrade_app_name": "Upgrading app {app}...", - "app_upgrade_failed": "Unable to upgrade {app:s}", - "app_upgrade_some_app_failed": "Unable to upgrade some applications", + "app_upgrade_app_name": "جارٍ تحديث برنامج {app}...", + "app_upgrade_failed": "تعذرت عملية ترقية {app:s}", + "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض البرمجيات", "app_upgraded": "{app:s} has been upgraded", "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", "appslist_fetched": "The application list {appslist:s} has been fetched", "appslist_migrating": "Migrating application list {appslist:s} ...", "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", - "appslist_removed": "The application list {appslist:s} has been removed", + "appslist_removed": "تم حذف قائمة البرمجيات {appslist:s}", "appslist_retrieve_bad_format": "Retrieved file for application list {appslist:s} is not valid", "appslist_retrieve_error": "Unable to retrieve the remote application list {appslist:s}: {error:s}", - "appslist_unknown": "Application list {appslist:s} unknown.", + "appslist_unknown": "قائمة البرمجيات {appslist:s} مجهولة.", "appslist_url_already_tracked": "There is already a registered application list with url {url:s}.", "ask_current_admin_password": "كلمة السر الإدارية الحالية", "ask_email": "عنوان البريد الإلكتروني", - "ask_firstname": "First name", - "ask_lastname": "Last name", - "ask_list_to_remove": "List to remove", + "ask_firstname": "الإسم", + "ask_lastname": "اللقب", + "ask_list_to_remove": "القائمة المختارة للحذف", "ask_main_domain": "النطاق الرئيسي", "ask_new_admin_password": "كلمة السر الإدارية الجديدة", "ask_password": "كلمة السر", @@ -62,7 +62,7 @@ "backup_action_required": "You must specify something to save", "backup_app_failed": "Unable to back up the app '{app:s}'", "backup_applying_method_borg": "Sending all files to backup into borg-backup repository...", - "backup_applying_method_copy": "Copying all files to backup...", + "backup_applying_method_copy": "جارٍ نسخ كافة الملفات إلى النسخة الإحتياطية …", "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", "backup_applying_method_tar": "Creating the backup tar archive...", "backup_archive_app_not_found": "App '{app:s}' not found in the backup archive", @@ -113,7 +113,7 @@ "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", - "certmanager_cert_install_success": "Successfully installed Let's Encrypt certificate for domain {domain:s}!", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain:s}!", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", "certmanager_cert_signing_failed": "Signing the new certificate failed", @@ -124,17 +124,17 @@ "certmanager_domain_dns_ip_differs_from_public_ip": "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay", "certmanager_domain_not_resolved_locally": "The domain {domain:s} cannot be resolved from inside your Yunohost server. This might happen if you recently modified your DNS record. If so, please wait a few hours for it to propagate. If the issue persists, consider adding {domain:s} to /etc/hosts. (If you know what you are doing, use --no-checks to disable those checks.)", - "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_domain_unknown": "النطاق مجهول {domain:s}", "certmanager_error_no_A_record": "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", - "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", + "certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain:s} (الملف : {file:s})", "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", - "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", + "diagnosis_debian_version_error": "لم نتمكن من العثور على إصدار ديبيان : {error}", "diagnosis_kernel_version_error": "Can't retrieve kernel version: {error}", "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", "diagnosis_monitor_network_error": "Can't monitor network: {error}", From d1cf7abc420bcc98242d638ebba863ecabcf028d Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 11 Feb 2018 20:32:58 +0000 Subject: [PATCH 0711/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 97.8% (359 of 367 strings) --- locales/ar.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index a70ee3975..9d2cfe54a 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -4,13 +4,13 @@ "admin_password_change_failed": "تعذرت عملية تعديل كلمة السر", "admin_password_changed": "تم تعديل الكلمة السرية الإدارية", "app_already_installed": "{app:s} تم تنصيبه مِن قبل", - "app_already_installed_cant_change_url": "This app is already installed. The url cannot be changed just by this function. Look into `app changeurl` if it's available.", - "app_already_up_to_date": "{app:s} is already up to date", - "app_argument_choice_invalid": "Invalid choice for argument '{name:s}', it must be one of {choices:s}", - "app_argument_invalid": "Invalid value for argument '{name:s}': {error:s}", - "app_argument_required": "Argument '{name:s}' is required", - "app_change_no_change_url_script": "The application {app_name:s} doesn't support changing it's URL yet, you might need to upgrade it.", - "app_change_url_failed_nginx_reload": "Failed to reload nginx. Here is the output of 'nginx -t':\n{nginx_errors:s}", + "app_already_installed_cant_change_url": "", + "app_already_up_to_date": "", + "app_argument_choice_invalid": "", + "app_argument_invalid": "", + "app_argument_required": "", + "app_change_no_change_url_script": "", + "app_change_url_failed_nginx_reload": "", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", From 0e2e349d31dccb9d971a183af28860ccfe941047 Mon Sep 17 00:00:00 2001 From: eric g Date: Mon, 26 Feb 2018 07:58:06 +0000 Subject: [PATCH 0712/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (367 of 367 strings) --- locales/fr.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index dd9c40b34..84601dcb5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -135,7 +135,7 @@ "mountpoint_unknown": "Point de montage inconnu", "mysql_db_creation_failed": "Impossible de créer la base de données MySQL", "mysql_db_init_failed": "Impossible d'initialiser la base de données MySQL", - "mysql_db_initialized": "La base de donnée MySQL a été initialisée", + "mysql_db_initialized": "La base de données MySQL a été initialisée", "network_check_mx_ko": "L'enregistrement DNS MX n'est pas précisé", "network_check_smtp_ko": "Le trafic courriel sortant (port 25 SMTP) semble bloqué par votre réseau", "network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n'est pas bloqué", @@ -320,7 +320,7 @@ "backup_archive_system_part_not_available": "La partie « {part:s} » du système n’est pas disponible dans cette sauvegarde", "backup_archive_mount_failed": "Le montage de l’archive de sauvegarde a échoué", "backup_archive_writing_error": "Impossible d’ajouter les fichiers à la sauvegarde dans l’archive compressée", - "backup_ask_for_copying_if_needed": "Certains fichiers n’ont pas pu être préparés pour être sauvegardée en utilisant la méthode qui évite de temporairement gaspiller de l’espace sur le système. Pour mener la sauvegarde, {size:s} Mio doivent être temporairement utilisés. Acceptez-vous ?", + "backup_ask_for_copying_if_needed": "Certains fichiers n’ont pas pu être préparés pour être sauvegardés en utilisant la méthode qui évite temporairement de gaspiller de l’espace sur le système. Pour mener la sauvegarde, {size:s} Mo doivent être temporairement utilisés. Acceptez-vous ?", "backup_borg_not_implemented": "La méthode de sauvegarde Bord n’est pas encore implémentée", "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de l’archive décompressée", "backup_copying_to_organize_the_archive": "Copie de {size:s} Mio pour organiser l’archive", @@ -378,5 +378,6 @@ "migrate_tsig_wait_2": "2 minutes…", "migrate_tsig_wait_3": "1 minute…", "migrate_tsig_wait_4": "30 secondes…", - "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine dyndns, donc aucune migration n’est nécessaire !" + "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine dyndns, donc aucune migration n’est nécessaire !", + "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' est obsolète ! Utilisez 'app register-url' en remplacement !" } From 3094711c8a8660620b34ca95d3d01fbdf01f650c Mon Sep 17 00:00:00 2001 From: Cedric Date: Sun, 15 Apr 2018 15:11:17 +0000 Subject: [PATCH 0713/1066] [i18n] Translated using Weblate (German) Currently translated at 78.7% (289 of 367 strings) --- locales/de.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locales/de.json b/locales/de.json index 14a9cb4b9..882f5287c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -10,8 +10,8 @@ "app_extraction_failed": "Installationsdateien konnten nicht entpackt werden", "app_id_invalid": "Falsche App-ID", "app_install_files_invalid": "Ungültige Installationsdateien", - "app_location_already_used": "Eine andere App ist bereits an diesem Ort installiert", - "app_location_install_failed": "Die App kann nicht an diesem Ort installiert werden", + "app_location_already_used": "Eine andere App ({app}) ist bereits an diesem Ort ({path}) installiert", + "app_location_install_failed": "Die App kann nicht an diesem Ort installiert werden, da es mit der App {other_app} die bereits in diesem Pfad ({other_path}) installiert ist Probleme geben würde", "app_manifest_invalid": "Ungültiges App-Manifest", "app_no_upgrade": "Keine Aktualisierungen für Apps verfügbar", "app_not_installed": "{app:s} ist nicht installiert", @@ -219,11 +219,11 @@ "pattern_positive_number": "Muss eine positive Zahl sein", "diagnosis_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", - "app_incompatible": "Die Anwendung ist nicht mit deiner YunoHost-Version kompatibel", + "app_incompatible": "Die Anwendung {app} ist nicht mit deiner YunoHost-Version kompatibel", "app_not_correctly_installed": "{app:s} scheint nicht richtig installiert worden zu sein", - "app_requirements_checking": "Überprüfe notwendige Pakete...", - "app_requirements_failed": "Anforderungen werden nicht erfüllt: {error}", - "app_requirements_unmeet": "Anforderungen werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", + "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...", + "app_requirements_failed": "Anforderungen für {app} werden nicht erfüllt: {error}", + "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", "diagnosis_debian_version_error": "Debian Version konnte nicht abgerufen werden: {error}", @@ -272,7 +272,7 @@ "certmanager_self_ca_conf_file_not_found": "Die Konfigurationsdatei der Zertifizierungsstelle für selbstsignierte Zertifikate wurde nicht gefunden (Datei {file:s})", "certmanager_acme_not_configured_for_domain": "Das Zertifikat für die Domain {domain:s} scheint nicht richtig installiert zu sein. Bitte führe den Befehl cert-install für diese Domain nochmals aus.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht analysiert werden (Datei: {file:s})", - "app_package_need_update": "Es ist notwendig das Paket zu aktualisieren, um Aktualisierungen für YunoHost zu erhalten", + "app_package_need_update": "Es ist notwendig das Paket {app} zu aktualisieren, um Aktualisierungen für YunoHost zu erhalten", "service_regenconf_dry_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server {service} notwendig sind...", "service_regenconf_pending_applying": "Überprüfe ausstehende Konfigurationen, die für den Server '{service}' notwendig sind...", "certmanager_http_check_timeout": "Eine Zeitüberschreitung ist aufgetreten als der Server versuchte sich selbst über HTTP mit der öffentlichen IP (Domain {domain:s} mit der IP {ip:s}) zu erreichen. Möglicherweise ist dafür hairpinning oder eine falsch konfigurierte Firewall/Router deines Servers dafür verantwortlich.", From 4592b5994d26866060c0f2b0fa9b053f5dd8f3be Mon Sep 17 00:00:00 2001 From: Jeroen Keerl Date: Sun, 15 Apr 2018 15:12:26 +0000 Subject: [PATCH 0714/1066] [i18n] Translated using Weblate (German) Currently translated at 78.7% (289 of 367 strings) --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 882f5287c..f93c2eb79 100644 --- a/locales/de.json +++ b/locales/de.json @@ -2,7 +2,7 @@ "action_invalid": "Ungültige Aktion '{action:s}'", "admin_password": "Administrator-Passwort", "admin_password_change_failed": "Passwort kann nicht geändert werden", - "admin_password_changed": "Das Administrator-Passwort wurde erfolgreich geändert", + "admin_password_changed": "Das Administrator-Kennwort wurde erfolgreich geändert", "app_already_installed": "{app:s} ist schon installiert", "app_argument_choice_invalid": "Ungültige Auswahl für Argument '{name:s}'. Es muss einer der folgenden Werte sein {choices:s}", "app_argument_invalid": "Das Argument '{name:s}' hat einen falschen Wert: {error:s}", From 6ae7173073d38fdb4984e325b28f062e0245090c Mon Sep 17 00:00:00 2001 From: Cedric Date: Sun, 15 Apr 2018 15:14:15 +0000 Subject: [PATCH 0715/1066] [i18n] Translated using Weblate (German) Currently translated at 78.7% (289 of 367 strings) --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index f93c2eb79..d2fc58466 100644 --- a/locales/de.json +++ b/locales/de.json @@ -220,7 +220,7 @@ "diagnosis_kernel_version_error": "Kann Kernelversion nicht abrufen: {error}", "package_unexpected_error": "Ein unerwarteter Fehler trat bei der Verarbeitung des Pakets '{pkgname}' auf", "app_incompatible": "Die Anwendung {app} ist nicht mit deiner YunoHost-Version kompatibel", - "app_not_correctly_installed": "{app:s} scheint nicht richtig installiert worden zu sein", + "app_not_correctly_installed": "{app:s} scheint nicht korrekt installiert zu sein", "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...", "app_requirements_failed": "Anforderungen für {app} werden nicht erfüllt: {error}", "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", From 1628b248d240ecabb0d57948fae6936211e6f39e Mon Sep 17 00:00:00 2001 From: beyercenter Date: Sun, 15 Apr 2018 15:14:33 +0000 Subject: [PATCH 0716/1066] [i18n] Translated using Weblate (German) Currently translated at 79.0% (290 of 367 strings) --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index d2fc58466..67cf0c1b2 100644 --- a/locales/de.json +++ b/locales/de.json @@ -299,5 +299,6 @@ "backup_archive_system_part_not_available": "Der System-Teil '{part:s}' ist in diesem Backup nicht enthalten", "backup_archive_mount_failed": "Das Einbinden des Backup-Archives ist fehlgeschlagen", "backup_archive_writing_error": "Die Dateien konnten nicht in der komprimierte Archiv-Backup hinzugefügt werden", - "app_change_url_success": "Erfolgreiche Änderung der URL von {app:s} zu {domain:s}{path:s}" + "app_change_url_success": "Erfolgreiche Änderung der URL von {app:s} zu {domain:s}{path:s}", + "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup" } From 0da8973352812c832d783f7d72d9939a927f66c8 Mon Sep 17 00:00:00 2001 From: Cedric Date: Sun, 15 Apr 2018 15:14:57 +0000 Subject: [PATCH 0717/1066] [i18n] Translated using Weblate (German) Currently translated at 79.0% (290 of 367 strings) --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 67cf0c1b2..965650422 100644 --- a/locales/de.json +++ b/locales/de.json @@ -300,5 +300,5 @@ "backup_archive_mount_failed": "Das Einbinden des Backup-Archives ist fehlgeschlagen", "backup_archive_writing_error": "Die Dateien konnten nicht in der komprimierte Archiv-Backup hinzugefügt werden", "app_change_url_success": "Erfolgreiche Änderung der URL von {app:s} zu {domain:s}{path:s}", - "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup" + "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository..." } From f21b2749fc3e43a65cafc4b29e77a6b1b614b3ca Mon Sep 17 00:00:00 2001 From: Philip Gatzka Date: Sun, 15 Apr 2018 15:15:20 +0000 Subject: [PATCH 0718/1066] [i18n] Translated using Weblate (German) Currently translated at 79.0% (290 of 367 strings) --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 965650422..f043df595 100644 --- a/locales/de.json +++ b/locales/de.json @@ -62,7 +62,7 @@ "domain_creation_failed": "Konnte Domain nicht erzeugen", "domain_deleted": "Die Domain wurde gelöscht", "domain_deletion_failed": "Konnte Domain nicht löschen", - "domain_dyndns_already_subscribed": "Du hast bereits eine DynDNS-Domain abonniert", + "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domain angemeldet", "domain_dyndns_invalid": "Domain nicht mittels DynDNS nutzbar", "domain_dyndns_root_unknown": "Unbekannte DynDNS Hauptdomain", "domain_exists": "Die Domain existiert bereits", From f59d6531f7d960ae19131d31dd63fc8e2b0199fd Mon Sep 17 00:00:00 2001 From: beyercenter Date: Sun, 15 Apr 2018 15:15:25 +0000 Subject: [PATCH 0719/1066] [i18n] Translated using Weblate (German) Currently translated at 79.2% (291 of 367 strings) --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index f043df595..8174e258e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -300,5 +300,6 @@ "backup_archive_mount_failed": "Das Einbinden des Backup-Archives ist fehlgeschlagen", "backup_archive_writing_error": "Die Dateien konnten nicht in der komprimierte Archiv-Backup hinzugefügt werden", "app_change_url_success": "Erfolgreiche Änderung der URL von {app:s} zu {domain:s}{path:s}", - "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository..." + "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository...", + "invalid_url_format": "ungültiges URL Format" } From ddc35770bbd47433014fcfe35805fb5b7d62366b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Fri, 27 Apr 2018 13:22:51 +0000 Subject: [PATCH 0720/1066] Added translation using Weblate (Occitan) --- locales/oc.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/oc.json diff --git a/locales/oc.json b/locales/oc.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/oc.json @@ -0,0 +1 @@ +{} From dc26b1de7c05076111579af32c659ef8a4d026ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Fri, 27 Apr 2018 13:49:32 +0000 Subject: [PATCH 0721/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 1.3% (5 of 367 strings) --- locales/oc.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 0967ef424..35a9e8342 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -1 +1,7 @@ -{} +{ + "admin_password": "Senhal d'administracion", + "admin_password_change_failed": "Impossible de cambiar lo senhal", + "admin_password_changed": "Lo senhal d'administracion es ben estat cambiat", + "app_already_installed": "{app:s} es ja installat", + "app_already_up_to_date": "{app:s} es ja a jorn" +} From 995319d5023d220ffe0dd1d44482a0cbbec39c72 Mon Sep 17 00:00:00 2001 From: bjarkan Date: Sat, 28 Apr 2018 19:54:24 +0000 Subject: [PATCH 0722/1066] [i18n] Translated using Weblate (Spanish) Currently translated at 82.0% (301 of 367 strings) --- locales/es.json | 50 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/locales/es.json b/locales/es.json index eba43fac2..264641065 100644 --- a/locales/es.json +++ b/locales/es.json @@ -9,21 +9,21 @@ "app_argument_required": "Se requiere el argumento '{name:s} 7'", "app_extraction_failed": "No se pudieron extraer los archivos de instalación", "app_id_invalid": "Id de la aplicación no válida", - "app_incompatible": "La aplicación no es compatible con su versión de YunoHost", + "app_incompatible": "La aplicación {app} no es compatible con su versión de YunoHost", "app_install_files_invalid": "Los archivos de instalación no son válidos", - "app_location_already_used": "Una aplicación ya está instalada en esta localización", - "app_location_install_failed": "No se puede instalar la aplicación en esta localización", + "app_location_already_used": "La aplicación {app} ya está instalada en esta localización ({path})", + "app_location_install_failed": "No se puede instalar la aplicación en esta localización porque entra en conflicto con la aplicación '{other_app}' ya instalada en '{other_path}'", "app_manifest_invalid": "El manifiesto de la aplicación no es válido: {error}", "app_no_upgrade": "No hay aplicaciones para actualizar", "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", "app_not_installed": "{app:s} 9 no está instalada", "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente", - "app_package_need_update": "Es necesario actualizar el paquete de la aplicación debido a los cambios en YunoHost", + "app_package_need_update": "El paquete de la aplicación {app} necesita ser actualizada debido a los cambios en YunoHost", "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", "app_removed": "{app:s} ha sido eliminada", - "app_requirements_checking": "Comprobando los paquetes requeridos...", - "app_requirements_failed": "No se cumplen los requisitos: {error}", - "app_requirements_unmeet": "No se cumplen los requisitos, el paquete {pkgname} ({version}) debe ser {spec}", + "app_requirements_checking": "Comprobando los paquetes requeridos por {app}...", + "app_requirements_failed": "No se cumplen los requisitos para {app}: {error}", + "app_requirements_unmeet": "No se cumplen los requisitos para {app}, el paquete {pkgname} ({version}) debe ser {spec}", "app_sources_fetch_failed": "No se pudieron descargar los archivos del código fuente", "app_unknown": "Aplicación desconocida", "app_unsupported_remote_type": "Tipo remoto no soportado por la aplicación", @@ -31,8 +31,8 @@ "app_upgraded": "{app:s} ha sido actualizada", "appslist_fetched": "La lista de aplicaciones {appslist:s} ha sido descargada", "appslist_removed": "La lista de aplicaciones {appslist:s} ha sido eliminada", - "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones {appslist:s} : {error}", - "appslist_unknown": "Lista de aplicaciones desconocida", + "appslist_retrieve_error": "No se pudo recuperar la lista remota de aplicaciones {appslist:s} : {error:s}", + "appslist_unknown": "Lista de aplicaciones {appslist:s} desconocida.", "ask_current_admin_password": "Contraseña administrativa actual", "ask_email": "Dirección de correo electrónico", "ask_firstname": "Nombre", @@ -151,7 +151,7 @@ "packages_upgrade_critical_later": "Los paquetes críticos ({packages:s}) serán actualizados más tarde", "packages_upgrade_failed": "No se pudieron actualizar todos los paquetes", "path_removal_failed": "No se pudo eliminar la ruta {:s}", - "pattern_backup_archive_name": "Debe ser un nombre de archivo válido, solo se admiten caracteres alfanuméricos, los guiones -_ y el punto.", + "pattern_backup_archive_name": "Debe ser un nombre de archivo válido con un máximo de 30 caracteres, solo se admiten caracteres alfanuméricos, los guiones -_ y el punto", "pattern_domain": "El nombre de dominio debe ser válido (por ejemplo mi-dominio.org)", "pattern_email": "Debe ser una dirección de correo electrónico válida (por ejemplo, alguien@dominio.org)", "pattern_firstname": "Debe ser un nombre válido", @@ -277,17 +277,35 @@ "yunohost_ca_creation_success": "Se ha creado la autoridad de certificación local.", "app_already_installed_cant_change_url": "Esta aplicación ya está instalada. No se puede cambiar el URL únicamente mediante esta función. Compruebe si está disponible la opción 'app changeurl'.", "app_change_no_change_url_script": "La aplicacion {app_name:s} aún no permite cambiar su URL, es posible que deba actualizarla.", - "app_change_url_failed_nginx_reload": "No se pudo recargar nginx. Compruebe la salida de 'nginx -t':\n{nginx_error:s}", - "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain: s} {path: s}'), no se realizarán cambios.", + "app_change_url_failed_nginx_reload": "No se pudo recargar nginx. Compruebe la salida de 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain:s} {path:s}'), no se realizarán cambios.", "app_change_url_no_script": "Esta aplicación '{app_name:s}' aún no permite modificar su URL. Quizás debería actualizar la aplicación.", "app_change_url_success": "El URL de la aplicación {app:s} ha sido cambiado correctamente a {domain:s} {path:s}", - "app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada.", + "app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada", "app_already_up_to_date": "La aplicación {app:s} ya está actualizada", "appslist_name_already_tracked": "Ya existe una lista de aplicaciones registrada con el nombre {name:s}.", "appslist_url_already_tracked": "Ya existe una lista de aplicaciones registrada con el URL {url:s}.", - "appslist_migrating": "Migrando la lista de aplicaciones {applist:s} ...", + "appslist_migrating": "Migrando la lista de aplicaciones {appslist:s} ...", "appslist_could_not_migrate": "No se pudo migrar la lista de aplicaciones {appslist:s}! No se pudo analizar el URL ... El antiguo cronjob se ha mantenido en {bkp_file:s}.", - "appslist_corrupted_json": "No se pudieron cargar las listas de aplicaciones. Parece que {filename: s} está dañado.", + "appslist_corrupted_json": "No se pudieron cargar las listas de aplicaciones. Parece que {filename:s} está dañado.", "invalid_url_format": "Formato de URL no válido", - "app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones" + "app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones", + "app_make_default_location_already_used": "No puede hacer la aplicación '{app}' por defecto en el dominio {domain} dado que está siendo usado por otra aplicación '{other_app}'", + "app_upgrade_app_name": "Actualizando la aplicación {app}...", + "ask_path": "Camino", + "backup_abstract_method": "Este método de backup no ha sido implementado aún", + "backup_applying_method_borg": "Enviando todos los ficheros al backup en el repositorio borg-backup...", + "backup_applying_method_copy": "Copiado todos los ficheros al backup...", + "backup_applying_method_custom": "Llamando el método de backup {method:s} ...", + "backup_applying_method_tar": "Creando el archivo tar de backup...", + "backup_archive_mount_failed": "Fallo en el montado del archivo de backup", + "backup_archive_system_part_not_available": "La parte del sistema {part:s} no está disponible en este backup", + "backup_archive_writing_error": "No se pueden añadir archivos de backup en el archivo comprimido", + "backup_ask_for_copying_if_needed": "Algunos ficheros no pudieron ser preparados para hacer backup usando el método que evita el gasto de espacio temporal en el sistema. Para hacer el backup, {size:s} MB deberían ser usados temporalmente. ¿Está de acuerdo?", + "backup_borg_not_implemented": "Método de backup Borg no está implementado aún", + "backup_cant_mount_uncompress_archive": "No se puede montar en modo solo lectura el directorio del archivo descomprimido", + "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo", + "backup_couldnt_bind": "No puede enlazar {src:s} con {dest:s}", + "backup_csv_addition_failed": "No puede añadir archivos al backup en el archivo CSV", + "backup_csv_creation_failed": "No se puede crear el archivo CSV necesario para futuras operaciones de restauración" } From 1f4a23aec3998f2b8752cb3c5cd47d2025fb7dcc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 1 May 2018 22:45:46 +0000 Subject: [PATCH 0723/1066] Update changelog for 2.7.11 release --- debian/changelog | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/debian/changelog b/debian/changelog index 989419109..9bf2eb579 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,42 @@ +yunohost (2.7.11) testing; urgency=low + + Important changes / fixes + ------------------------- + + * [enh] Add commands to manage user ssh accesses and keys (#403, #445) + * [fix] Fix Lets Encrypt install when an app is installed at root (#428) + * [enh] Improve performances by lazy-loading some modules (#451) + * [enh] Use Mozilla's recommended headers in nginx conf (#399, #456) + * [fix] Fix path traversal issues in yunohost admin nginx conf (#420) + * [helpers] Add nodejs helpers (#441, #446) + + Other changes + ------------- + + * [enh] Enable gzip compression for common text mimetypes in nginx (#356) + * [enh] Add 'post' hooks on app management operations (#360) + * [fix] Fix an issue with custom backup methods and crons (#421) + * [mod] Simplify the way we fetch and test global ip (#424) + * [enh] Manage etckeeper.conf to make etckeeper quiet (#426) + * [fix] Be able to access conf folder in change_url scripts (#427) + * [enh] Verbosify backup/restores that are performed during app upgrades (#432) + * [enh] Display debug information on cert-install/renew failure (#447) + * [fix] Add mailutils and wget as a dependencies + * [mod] Misc tweaks to display more info when some commands fail + * [helpers] More explicit depreciation warning for 'app checkurl' + * [helpers] Fix an issue in ynh_restore_file if destination already exists (#384) + * [helpers] Update php-fpm helpers to handle stretch/php7 and a smooth migration (#373) + * [helpers] Add helper 'ynh_get_debian_release' (#373) + * [helpers] Trigger an error when failing to install dependencies (#381) + * [helpers] Allow for 'or' in dependencies (#381) + * [helpers] Tweak the usage of BACKUP_CORE_ONLY (#398) + * [helpers] Tweak systemd config helpers (optional service name and template name) (#425) + * [i18n] Improve translations for Arabic, French, German, Occitan, Spanish + + Thanks for all contributors (ariasuni, ljf, JimboeJoe, Maniack, Josue, Aleks, Bram, jibec) and the several translators (ButterflyOfFire, J-B Lescher, Eric G., Cedric, J. Keerl, beyercenter, P. Gatzka, Quenti, bjarkan) <3 ! + + -- Alexandre Aubin Tue, 01 May 2018 22:04:40 +0000 + yunohost (2.7.10) stable; urgency=low * [fix] Fail2ban conf/filter was not matching failed login attempts... From ec56b551456926f2fc08ef2222cbc75df670a9ed Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 May 2018 01:27:16 +0200 Subject: [PATCH 0724/1066] Fix a few mistakes in the changelog credits :s --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 9bf2eb579..f2dcee401 100644 --- a/debian/changelog +++ b/debian/changelog @@ -33,7 +33,7 @@ yunohost (2.7.11) testing; urgency=low * [helpers] Tweak systemd config helpers (optional service name and template name) (#425) * [i18n] Improve translations for Arabic, French, German, Occitan, Spanish - Thanks for all contributors (ariasuni, ljf, JimboeJoe, Maniack, Josue, Aleks, Bram, jibec) and the several translators (ButterflyOfFire, J-B Lescher, Eric G., Cedric, J. Keerl, beyercenter, P. Gatzka, Quenti, bjarkan) <3 ! + Thanks to all contributors (ariasuni, ljf, JimboJoe, frju365, Maniack, J-B Lescher, Josue, Aleks, Bram, jibec) and the several translators (ButterflyOfFire, Eric G., Cedric, J. Keerl, beyercenter, P. Gatzka, Quenti, bjarkan) <3 ! -- Alexandre Aubin Tue, 01 May 2018 22:04:40 +0000 From 5013965c0e625e317b96561e251846c902912fc7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 May 2018 02:10:46 +0200 Subject: [PATCH 0725/1066] [enh] [wip] Stretch migration (#433) * Add migration skeleton * Clumsy attempt to adapt the upgrade script to python * At the end of the migration, force the regen conf of specific services * Implement the apt clean/autoremove at the end of migration * Attempt to fix the upgrade of yunohost packages * Dumb mistake :| * Adding strings * Add test of free space for /var/ * Fix sources.list patching * Stupid mistake :| * Check system is up to date * Working on disclaimer draft * Add a function to list installed 'unstable' apps * Get actual list of problemtic apps + improve disclaimer message building * Use helper to run the apt update * More simplifications of disclaimer building * Add helper function to get manually modified files * Fetch actuall list of manually modified files to build disclaimer * Internationalize disclaimer * Don't skip stretch migration when running postinstall on jessie * Add a done message at the very end of the migration * Also patch jessie/updates and backports in sources.list * Backup and restore conf files modified during the upgrade to not mess regen-conf * Also check for yunohost being in 2.x at the beginning of upgrade * Fix the check for upgradable packages.. * Try to be more robust if folder already exists (when running multiple times) * I probably meant fail2ban here o.O * Try to improve robustness when running multiple time * Add a check after the main upgrade that we're effectively on stretch * Hold apps equivs packages during the upgrade * Show dist-upgrade logs in the yunohost admin, using call_async_output * Misc fixes because I broke things /o\ * Touch /etc/yunohost/installed at the end, because for some weird reason it get deleted sometimes :| * Removing this unecessary message, especially because it 'hide' the previous one when running from the webadmin * Install php-zip for nextcloud and kanboard * Don't crash if there's no [app]-ynh-deps * Revert previous commit that added this, should be fixed in the stretch branch now * [fix] Unhold metronome for migration (#452) * Let's use forge.yunohost.org as repo now --- locales/en.json | 13 + src/yunohost/app.py | 17 + .../0003_migrate_to_stretch.py | 291 ++++++++++++++++++ src/yunohost/service.py | 22 ++ src/yunohost/tools.py | 2 +- src/yunohost/utils/filesystem.py | 25 ++ 6 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/data_migrations/0003_migrate_to_stretch.py create mode 100644 src/yunohost/utils/filesystem.py diff --git a/locales/en.json b/locales/en.json index 9957c25a0..0f45c7a8b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -224,6 +224,19 @@ "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", "migration_description_0001_change_cert_group_to_sslcert": "Change certificates group permissions from 'metronome' to 'ssl-cert'", "migration_description_0002_migrate_to_tsig_sha256": "Improve security of dyndns TSIG by using SHA512 instead of MD5", + "migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0", + "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", + "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", + "migration_0003_patching_sources_list": "Patching the sources.lists ...", + "migration_0003_main_upgrade": "Starting main upgrade ...", + "migration_0003_fail2ban_upgrade": "Starting the fail2ban upgrade ...", + "migration_0003_yunohost_upgrade": "Starting the yunohost package upgrade ... The migration will end, but the actual upgrade will happen right after. After the operation is complete, you might have to re-log on the webadmin.", + "migration_0003_not_jessie": "The current debian distribution is not Jessie !", + "migration_0003_system_not_fully_up_to_date": "Your system is not fully up to date. Please perform a regular upgrade before running the migration to stretch.", + "migration_0003_still_on_jessie_after_main_upgrade": "Something wrong happened during the main upgrade : system is still on Jessie !? To investigate the issue, please look at {log} :s ...", + "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.", + "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an applist or are not flagged as 'working'. Consequently, we cannot guarantee that they will still work after the upgrade : {problematic_apps}", + "migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten at the end of the upgrade : {manually_modified_files}", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 6ddf08f52..a4ab8db7b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2164,3 +2164,20 @@ def normalize_url_path(url_path): return '/' + url_path.strip("/").strip() + '/' return "/" + + +def unstable_apps(): + + raw_app_installed = app_list(installed=True, raw=True) + output = [] + + for app, infos in raw_app_installed.items(): + + repo = infos.get("repository", None) + state = infos.get("state", None) + + if repo is None or state in ["inprogress", "notworking"]: + output.append(app) + + return output + diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py new file mode 100644 index 000000000..5f65dbb89 --- /dev/null +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -0,0 +1,291 @@ +import glob +import os +import requests +import base64 +import time +import json +import errno +import platform +from shutil import copy2 + +from moulinette import m18n, msettings +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from moulinette.utils.process import check_output, call_async_output +from moulinette.utils.filesystem import read_file + +from yunohost.tools import Migration +from yunohost.app import unstable_apps +from yunohost.service import _run_service_command, service_regen_conf, manually_modified_files +from yunohost.utils.filesystem import free_space_in_directory +from yunohost.utils.packages import get_installed_version + +logger = getActionLogger('yunohost.migration') + +YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat" ] + +class MyMigration(Migration): + "Upgrade the system to Debian Stretch and Yunohost 3.0" + + mode = "manual" + + def backward(self): + + raise MoulinetteError(m18n.n("migration_0003_backward_impossible")) + + def migrate(self): + + self.logfile = "/tmp/{}.log".format(self.name) + + self.check_assertions() + + logger.warning(m18n.n("migration_0003_start", logfile=self.logfile)) + + # Preparing the upgrade + logger.warning(m18n.n("migration_0003_patching_sources_list")) + self.patch_apt_sources_list() + self.backup_files_to_keep() + self.apt_update() + apps_packages = self.get_apps_equivs_packages() + self.unhold(["metronome"]) + self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban"]) + + # Main dist-upgrade + logger.warning(m18n.n("migration_0003_main_upgrade")) + _run_service_command("stop", "mysql") + self.apt_dist_upgrade(conf_flags=["old", "def"]) + _run_service_command("start", "mysql") + if self.debian_major_version() == 8: + raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) + + # Specific upgrade for fail2ban... + logger.warning(m18n.n("migration_0003_fail2ban_upgrade")) + self.unhold(["fail2ban"]) + # Don't move this if folder already exists. If it does, we probably are + # running this script a 2nd, 3rd, ... time but /etc/fail2ban will + # be re-created only for the first dist-upgrade of fail2ban + if not os.path.exists("/etc/fail2ban.old"): + os.system("mv /etc/fail2ban /etc/fail2ban.old") + self.apt_dist_upgrade(conf_flags=["new", "miss", "def"]) + _run_service_command("restart", "fail2ban") + + # Clean the mess + os.system("apt autoremove --assume-yes") + os.system("apt clean --assume-yes") + + # Upgrade yunohost packages + logger.warning(m18n.n("migration_0003_yunohost_upgrade")) + self.restore_files_to_keep() + self.unhold(YUNOHOST_PACKAGES + apps_packages) + self.upgrade_yunohost_packages() + + def debian_major_version(self): + return int(platform.dist()[1][0]) + + def yunohost_major_version(self): + return int(get_installed_version("yunohost").split('.')[0]) + + def check_assertions(self): + + # Be on jessie (8.x) and yunohost 2.x + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be >= 9.x but yunohost package + # would still be in 2.x... + if not self.debian_major_version() == 8 \ + and not self.yunohost_major_version() == 2: + raise MoulinetteError(m18n.n("migration_0003_not_jessie")) + + # Have > 1 Go free space on /var/ ? + if free_space_in_directory("/var/") / (1024**3) < 1.0: + raise MoulinetteError(m18n.n("migration_0003_not_enough_free_space")) + + # Check system is up to date + # (but we don't if 'stretch' is already in the sources.list ... + # which means maybe a previous upgrade crashed and we're re-running it) + if not " stretch " in read_file("/etc/apt/sources.list"): + self.apt_update() + apt_list_upgradable = check_output("apt list --upgradable -a") + if "upgradable" in apt_list_upgradable: + raise MoulinetteError(m18n.n("migration_0003_system_not_fully_up_to_date")) + + @property + def disclaimer(self): + + # Avoid having a super long disclaimer + uncessary check if we ain't + # on jessie / yunohost 2.x anymore + # NB : we do both check to cover situations where the upgrade crashed + # in the middle and debian version could be >= 9.x but yunohost package + # would still be in 2.x... + if not self.debian_major_version() == 8 \ + and not self.yunohost_major_version() == 2: + return None + + # Get list of problematic apps ? I.e. not official or community+working + problematic_apps = unstable_apps() + problematic_apps = "".join(["\n - "+app for app in problematic_apps ]) + + # Manually modified files ? (c.f. yunohost service regen-conf) + modified_files = manually_modified_files() + modified_files = "".join(["\n - "+f for f in modified_files ]) + + message = m18n.n("migration_0003_general_warning") + + if problematic_apps: + message += "\n\n" + m18n.n("migration_0003_problematic_apps_warning", problematic_apps=problematic_apps) + + if modified_files: + message += "\n\n" + m18n.n("migration_0003_modified_files", manually_modified_files=modified_files) + + return message + + def patch_apt_sources_list(self): + + sources_list = glob.glob("/etc/apt/sources.list.d/*.list") + sources_list.append("/etc/apt/sources.list") + + # This : + # - replace single 'jessie' occurence by 'stretch' + # - comments lines containing "backports" + # - replace 'jessie/updates' by 'strech/updates' + # - switch yunohost's repo to forge + for f in sources_list: + command = "sed -i -e 's@ jessie @ stretch @g' " \ + "-e '/backports/ s@^#*@#@' " \ + "-e 's@ jessie/updates @ stretch/updates @g' " \ + "-e 's@repo.yunohost@forge.yunohost@g' " \ + "{}".format(f) + os.system(command) + + def get_apps_equivs_packages(self): + + command = "dpkg --get-selections" \ + " | grep -v deinstall" \ + " | awk '{print $1}'" \ + " | { grep 'ynh-deps$' || true; }" + + output = check_output(command).strip() + + return output.split('\n') if output else [] + + def hold(self, packages): + for package in packages: + os.system("apt-mark hold {}".format(package)) + + def unhold(self, packages): + for package in packages: + os.system("apt-mark unhold {}".format(package)) + + def apt_update(self): + + command = "apt-get update" + logger.debug("Running apt command :\n{}".format(command)) + command += " 2>&1 | tee -a {}".format(self.logfile) + + os.system(command) + + def upgrade_yunohost_packages(self): + + # + # Here we use a dirty hack to run a command after the current + # "yunohost tools migrations migrate", because the upgrade of + # yunohost will also trigger another "yunohost tools migrations migrate" + # (also the upgrade of the package, if executed from the webadmin, is + # likely to kill/restart the api which is in turn likely to kill this + # command before it ends...) + # + + MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" + + upgrade_command = "" + upgrade_command += " DEBIAN_FRONTEND=noninteractive" + upgrade_command += " APT_LISTCHANGES_FRONTEND=none" + upgrade_command += " apt-get install" + upgrade_command += " --assume-yes " + upgrade_command += " ".join(YUNOHOST_PACKAGES) + # We also install php-zip to fix an issue with nextcloud and kanboard + # that need it when on stretch. + upgrade_command += " php-zip" + upgrade_command += " 2>&1 | tee -a {}".format(self.logfile) + + wait_until_end_of_yunohost_command = "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) + + command = "({} && {}; echo 'Done!') &".format(wait_until_end_of_yunohost_command, + upgrade_command) + + logger.debug("Running command :\n{}".format(command)) + + os.system(command) + + + def apt_dist_upgrade(self, conf_flags): + + # Make apt-get happy + os.system("echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections") + + command = "" + command += " DEBIAN_FRONTEND=noninteractive" + command += " APT_LISTCHANGES_FRONTEND=none" + command += " apt-get" + command += " --fix-broken --show-upgraded --assume-yes" + for conf_flag in conf_flags: + command += ' -o Dpkg::Options::="--force-conf{}"'.format(conf_flag) + command += " dist-upgrade" + + logger.debug("Running apt command :\n{}".format(command)) + + command += " 2>&1 | tee -a {}".format(self.logfile) + + is_api = msettings.get('interface') == 'api' + if is_api: + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + call_async_output(command, callbacks, shell=True) + else: + # We do this when running from the cli to have the output of the + # command showing in the terminal, since 'info' channel is only + # enabled if the user explicitly add --verbose ... + os.system(command) + + + # Those are files that should be kept and restored before the final switch + # to yunohost 3.x... They end up being modified by the various dist-upgrades + # (or need to be taken out momentarily), which then blocks the regen-conf + # as they are flagged as "manually modified"... + files_to_keep = [ + "/etc/mysql/my.cnf", + "/etc/nslcd.conf", + "/etc/postfix/master.cf", + "/etc/fail2ban/filter.d/yunohost.conf" + ] + + def backup_files_to_keep(self): + + logger.debug("Backuping specific files to keep ...") + + # Create tmp directory if it does not exists + tmp_dir = os.path.join("/tmp/", self.name) + if not os.path.exists(tmp_dir): + os.mkdir(tmp_dir, 0700) + + for f in self.files_to_keep: + dest_file = f.strip('/').replace("/", "_") + + # If the file is already there, we might be re-running the migration + # because it previously crashed. Hence we keep the existing file. + if os.path.exists(os.path.join(tmp_dir, dest_file)): + continue + + copy2(f, os.path.join(tmp_dir, dest_file)) + + def restore_files_to_keep(self): + + logger.debug("Restoring specific files to keep ...") + + tmp_dir = os.path.join("/tmp/", self.name) + + for f in self.files_to_keep: + dest_file = f.strip('/').replace("/", "_") + copy2(os.path.join(tmp_dir, dest_file), f) + diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f0948c961..1852259ad 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -26,6 +26,7 @@ import os import time import yaml +import json import glob import subprocess import errno @@ -771,3 +772,24 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): exc_info=1) return False return True + +def manually_modified_files(): + + # We do this to have --quiet, i.e. don't throw a whole bunch of logs + # just to fetch this... + # Might be able to optimize this by looking at what service_regenconf does + # and only do the part that checks file hashes... + cmd = "yunohost service regen-conf --dry-run --output-as json --quiet" + j = json.loads(subprocess.check_output(cmd.split())) + + # j is something like : + # {"postfix": {"applied": {}, "pending": {"/etc/postfix/main.cf": {"status": "modified"}}} + + output = [] + for app, actions in j.items(): + for action, files in actions.items(): + for filename, infos in files.items(): + if infos["status"] == "modified": + output.append(filename) + + return output diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index a60ffa80a..80f7bcaa5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -395,7 +395,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): _install_appslist_fetch_cron() # Init migrations (skip them, no need to run them on a fresh system) - tools_migrations_migrate(skip=True, auto=True) + tools_migrations_migrate(target=2, skip=True, auto=True) os.system('touch /etc/yunohost/installed') diff --git a/src/yunohost/utils/filesystem.py b/src/yunohost/utils/filesystem.py new file mode 100644 index 000000000..9b39f5daa --- /dev/null +++ b/src/yunohost/utils/filesystem.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import os + +def free_space_in_directory(dirpath): + stat = os.statvfs(dirpath) + return stat.f_frsize * stat.f_bavail From 9ebb082e32922ffb8a199ab81bca2255a42bc5e9 Mon Sep 17 00:00:00 2001 From: frju365 Date: Wed, 2 May 2018 14:10:13 +0200 Subject: [PATCH 0726/1066] [fix] Nginx Regression typo (#459) --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 861c4b476..df722b526 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -48,7 +48,7 @@ server { # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header Content-Security-Policy "upgrade-insecure-requests;" + add_header Content-Security-Policy "upgrade-insecure-requests"; add_header Content-Security-Policy-Report-Only "default-src https: data: 'unsafe-inline' 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; From e13915bffa9fdcdf69e728484f4ae475bce7e5b9 Mon Sep 17 00:00:00 2001 From: frju365 Date: Wed, 2 May 2018 14:10:13 +0200 Subject: [PATCH 0727/1066] [fix] Nginx Regression typo (#459) --- data/templates/nginx/server.tpl.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 495a15bdc..47fd05505 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -46,7 +46,7 @@ server { # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header Content-Security-Policy "upgrade-insecure-requests;" + add_header Content-Security-Policy "upgrade-insecure-requests"; add_header Content-Security-Policy-Report-Only "default-src https: data: 'unsafe-inline' 'unsafe-eval'"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; From 1b7bd8124cd8b1cd4d86065ea65e88998b9bc99c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 May 2018 12:13:10 +0000 Subject: [PATCH 0728/1066] Update changelog for 2.7.11.1 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9bf2eb579..42f50bae8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.7.11.1) testing; urgency=low + + * [fix] Nginx Regression typo (#459) + + -- Alexandre Aubin Wed, 02 May 2018 12:12:45 +0000 + yunohost (2.7.11) testing; urgency=low Important changes / fixes From 4c79f525a175544bb3fecb06f878dcd8d59b20eb Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 23 Jan 2017 11:49:09 +0100 Subject: [PATCH 0729/1066] [enh] Compatibility with debian stretch This reverts commit 3be7aff0655d38d4ceba2f9aa54666c1071769a4. --- debian/control | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/debian/control b/debian/control index d1505994a..61b886d72 100644 --- a/debian/control +++ b/debian/control @@ -16,12 +16,12 @@ Depends: ${python:Depends}, ${misc:Depends} , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute - , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd + , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd | php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban - , nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl + , nginx-extras (>=1.6.2), php5-fpm | php-fpm, php5-ldap | php-ldap, php5-intl | php-intl , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , metronome , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools @@ -29,7 +29,7 @@ Depends: ${python:Depends}, ${misc:Depends} Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper - , php5-gd, php5-curl, php-gettext, php5-mcrypt + , php5-gd | php-gd, php5-curl | php-curl, php5-gettext | php-gettext, php5-mcrypt | php-mcrypt , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl From 331db9edb65b22442b47928422c377dc04a8774d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Jul 2017 13:36:49 +0000 Subject: [PATCH 0730/1066] Removing dependency to rmilter, obsolete since rspamd >= 1.6 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 61b886d72..c13095e80 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,7 @@ Depends: ${python:Depends}, ${misc:Depends} , nginx-extras (>=1.6.2), php5-fpm | php-fpm, php5-ldap | php-ldap, php5-intl | php-intl , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , metronome - , rspamd (>= 1.2.0), rmilter (>=1.7.0), redis-server, opendkim-tools + , rspamd (>= 1.6.0), redis-server, opendkim-tools , haveged Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping From 786e1d8728d10dad2349b1816090642add5312ad Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 13:57:08 +0000 Subject: [PATCH 0731/1066] Removing php5 dependencies (generic php deps instead) --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index c13095e80..8faa2dc1e 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Depends: ${python:Depends}, ${misc:Depends} , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute - , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd | php-mysql | php-mysqlnd + , mariadb-server | mysql-server, php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved @@ -29,7 +29,7 @@ Depends: ${python:Depends}, ${misc:Depends} Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper - , php5-gd | php-gd, php5-curl | php-curl, php5-gettext | php-gettext, php5-mcrypt | php-mcrypt + , php-gd, php-curl, php-gettext, php-mcrypt , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl From 6fc3f08cdae5d7b6a9a43926700cbd1080135de6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 14:42:24 +0000 Subject: [PATCH 0732/1066] Regenerate nslcd config before creating admin in ldap --- src/yunohost/tools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 80f7bcaa5..75f7223d3 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -295,6 +295,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.info(m18n.n('yunohost_installing')) + service_regen_conf(['nslcd'], force=True) + # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook auth = tools_ldapinit() From ddebd9f0917eedfe9deefa1574f96396c4781e9a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 14:51:54 +0000 Subject: [PATCH 0733/1066] We also need to regen nsswitch before ldapinit otherwise sudo doesnt work --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 75f7223d3..5571f7c03 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -295,7 +295,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.info(m18n.n('yunohost_installing')) - service_regen_conf(['nslcd'], force=True) + service_regen_conf(['nslcd', 'nsswitch'], force=True) # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook From da973e69a776a7470d7021a19209af5a8557c467 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 15:28:08 +0000 Subject: [PATCH 0734/1066] Getting rid of systematic error message during install --- data/hooks/conf_regen/06-slapd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index aef47c347..d0a1fad63 100755 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -46,7 +46,7 @@ do_pre_regen() { sudo rm -f "$tmp_backup_dir_file" # retrieve current and new backends - curr_backend=$(grep '^database' /etc/ldap/slapd.conf | awk '{print $2}') + curr_backend=$(grep '^database' /etc/ldap/slapd.conf 2>/dev/null | awk '{print $2}') new_backend=$(grep '^database' slapd.conf | awk '{print $2}') # save current database before any conf changes From a72e7753f8e24f4664e56ce2bf1b7698c926e1e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 15:28:48 +0000 Subject: [PATCH 0735/1066] Remove install flag in /etc/yunohost when uninstall yunohost --- debian/postrm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/debian/postrm b/debian/postrm index 2bbdd496b..cf2b6afe7 100644 --- a/debian/postrm +++ b/debian/postrm @@ -6,6 +6,8 @@ if [ "$1" = "purge" ]; then update-rc.d yunohost-firewall remove >/dev/null fi +rm -f /etc/yunohost/installed + #DEBHELPER# exit 0 From 627edbdf0b09e0698543e606f0d8c0ace3b4145c Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 23 Jan 2017 11:49:09 +0100 Subject: [PATCH 0736/1066] [enh] Compatibility with debian stretch This reverts commit 3be7aff0655d38d4ceba2f9aa54666c1071769a4. --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 8faa2dc1e..46c95fb8b 100644 --- a/debian/control +++ b/debian/control @@ -21,7 +21,7 @@ Depends: ${python:Depends}, ${misc:Depends} , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban - , nginx-extras (>=1.6.2), php5-fpm | php-fpm, php5-ldap | php-ldap, php5-intl | php-intl + , nginx-extras (>=1.6.2), php-fpm, php-ldap, php-intl , dnsmasq, openssl, avahi-daemon, libnss-mdns, resolvconf, libnss-myhostname , metronome , rspamd (>= 1.6.0), redis-server, opendkim-tools From 03dac66bcbdbd159ff1ea1a305095348d94a3de3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 14:42:24 +0000 Subject: [PATCH 0737/1066] Regenerate nslcd config before creating admin in ldap --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5571f7c03..75f7223d3 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -295,7 +295,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.info(m18n.n('yunohost_installing')) - service_regen_conf(['nslcd', 'nsswitch'], force=True) + service_regen_conf(['nslcd'], force=True) # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook From 59c8c0d100c9f6b044b2faea7040133328707945 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 14:51:54 +0000 Subject: [PATCH 0738/1066] We also need to regen nsswitch before ldapinit otherwise sudo doesnt work --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 75f7223d3..5571f7c03 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -295,7 +295,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): logger.info(m18n.n('yunohost_installing')) - service_regen_conf(['nslcd'], force=True) + service_regen_conf(['nslcd', 'nsswitch'], force=True) # Initialize LDAP for YunoHost # TODO: Improve this part by integrate ldapinit into conf_regen hook From 078dbaf2d36bedeb430f88756dccc68b2aeb7f4d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 23:50:33 +0000 Subject: [PATCH 0739/1066] Fully execute the regen conf as root --- src/yunohost/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 1852259ad..4a4574d65 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -322,7 +322,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, def _pre_call(name, priority, path, args): # create the pending conf directory for the service service_pending_path = os.path.join(PENDING_CONF_DIR, name) - filesystem.mkdir(service_pending_path, 0755, True, uid='admin') + filesystem.mkdir(service_pending_path, 0755, True, uid='root') # return the arguments to pass to the script return pre_args + [service_pending_path, ] pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call) From bde8b02fef54d0d8a9c622b2132cb0b6f981674e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Jul 2017 23:56:02 +0000 Subject: [PATCH 0740/1066] Removing some rmilter-related conf --- data/hooks/conf_regen/28-rmilter | 80 ---------------------------- data/templates/rmilter/rmilter.conf | 28 ---------- data/templates/yunohost/services.yml | 4 +- 3 files changed, 1 insertion(+), 111 deletions(-) delete mode 100755 data/hooks/conf_regen/28-rmilter delete mode 100644 data/templates/rmilter/rmilter.conf diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter deleted file mode 100755 index f505b6d99..000000000 --- a/data/hooks/conf_regen/28-rmilter +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash - -set -e - -do_pre_regen() { - pending_dir=$1 - - cd /usr/share/yunohost/templates/rmilter - - # Install main configuration - install -D -m 644 rmilter.conf \ - "${pending_dir}/etc/rmilter.conf" - - # Install DKIM specific configuration - install -D -m 644 ynh_dkim.conf \ - "${pending_dir}/etc/rmilter.conf.d/ynh_dkim.conf" - - # Remove old socket file (we stopped using it, since rspamd 1.3.1) - # Regen-conf system need an empty file to delete it - install -D -m 644 /dev/null \ - "${pending_dir}/etc/systemd/system/rmilter.socket" -} - -do_post_regen() { - regen_conf_files=$1 - - # retrieve variables - domain_list=$(sudo yunohost domain list --output-as plain --quiet) - - # create DKIM directory with proper permission - sudo mkdir -p /etc/dkim - sudo chown _rmilter /etc/dkim - - # create DKIM key for domains - for domain in $domain_list; do - domain_key="/etc/dkim/${domain}.mail.key" - [ ! -f $domain_key ] && { - sudo opendkim-genkey --domain="$domain" \ - --selector=mail --directory=/etc/dkim - sudo mv /etc/dkim/mail.private "$domain_key" - sudo mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" - } - done - - # fix DKIM keys permissions - sudo chown _rmilter /etc/dkim/*.mail.key - sudo chmod 400 /etc/dkim/*.mail.key - - # fix rmilter socket permission (postfix is chrooted in /var/spool/postfix ) - sudo mkdir -p /var/spool/postfix/run/rmilter - sudo chown -R postfix:_rmilter /var/spool/postfix/run/rmilter - sudo chmod g+w /var/spool/postfix/run/rmilter - - [ -z "$regen_conf_files" ] && exit 0 - - # reload systemd daemon - sudo systemctl -q daemon-reload - - # Restart rmilter due to the rspamd update - # https://rspamd.com/announce/2016/08/01/rspamd-1.3.1.html - sudo systemctl -q restart rmilter.service -} - -FORCE=${2:-0} -DRY_RUN=${3:-0} - -case "$1" in - pre) - do_pre_regen $4 - ;; - post) - do_post_regen $4 - ;; - *) - echo "hook called with unknown argument \`$1'" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf deleted file mode 100644 index dcd13e9b7..000000000 --- a/data/templates/rmilter/rmilter.conf +++ /dev/null @@ -1,28 +0,0 @@ -# systemd-specific settings for rmilter - -# DKIM signing -# Defined before including /etc/rmilter.conf.common because rmilter seems to be -# unable to override dkim{} settings, even if it's already defined in -# /etc/rmilter.conf.d/ynh_dkim.conf -dkim { - enable = true; - domain { - key = /etc/dkim; - domain = "*"; - selector = "mail"; - }; - header_canon = relaxed; - body_canon = relaxed; - sign_alg = sha256; -}; - -.include /etc/rmilter.conf.common - -# pidfile - path to pid file -pidfile = /run/rmilter/rmilter.pid; - -bind_socket = unix:/var/spool/postfix/run/rmilter/rmilter.sock; - -# include user's configuration -.try_include /etc/rmilter.conf.local -.try_include /etc/rmilter.conf.d/*.conf diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index fb8c076f9..e3900f87e 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -16,9 +16,6 @@ dovecot: postfix: status: service log: [/var/log/mail.log,/var/log/mail.err] -rmilter: - status: systemctl status rmilter.service - log: /var/log/mail.log rspamd: status: systemctl status rspamd.service log: /var/log/mail.log @@ -60,3 +57,4 @@ udisk-glue: null amavis: null postgrey: null spamassassin: null +rmilter: null From f221c2f06c96c95cb0bd1e425b76e5227b5c254f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 8 Jul 2017 00:09:32 +0000 Subject: [PATCH 0741/1066] Fail2ban was unhappy because port was present two times --- data/templates/fail2ban/jail.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index 648d44fa8..4047306a7 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -152,7 +152,7 @@ filter = pam-generic # port actually must be irrelevant but lets leave it all for some possible uses port = all banaction = iptables-allports -port = anyport +#port = anyport logpath = /var/log/auth.log maxretry = 6 From 0373da4a467a62b76f1b50e24d97db6f31c97fca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 8 Jul 2017 00:44:00 +0000 Subject: [PATCH 0742/1066] Hiding useless message (?) --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5571f7c03..bcca5b0e9 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -104,7 +104,7 @@ def tools_ldapinit(): auth.update('cn=admin', admin_dict) # Force nscd to refresh cache to take admin creation into account - subprocess.call(['nscd', '-i', 'passwd']) + subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) # Check admin actually exists now try: @@ -203,7 +203,7 @@ def _set_hostname(hostname, pretty_hostname=None): pretty_hostname = "(YunoHost/%s)" % hostname # First clear nsswitch cache for hosts to make sure hostname is resolved... - subprocess.call(['nscd', '-i', 'hosts']) + subprocess.call(['nscd', '-i', 'hosts'], stderr=open(os.devnull, 'wb')) # Then call hostnamectl commands = [ From 1839fa44d0fc5ff796689131bd9f2dd78e30f227 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 8 Jul 2017 11:05:08 +0000 Subject: [PATCH 0743/1066] Also remove useless nscd warning in user.py --- src/yunohost/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index bed5fb8c8..740258256 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -194,7 +194,7 @@ def user_create(auth, username, firstname, lastname, mail, password, if auth.add('uid=%s,ou=users' % username, attr_dict): # Invalidate passwd to take user creation into account - subprocess.call(['nscd', '-i', 'passwd']) + subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] @@ -233,7 +233,7 @@ def user_delete(auth, username, purge=False): if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account - subprocess.call(['nscd', '-i', 'passwd']) + subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] From b2648584ab2feebfa7f950cc619f370156ab2c4e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 8 Jul 2017 16:02:16 +0000 Subject: [PATCH 0744/1066] Updating fail2ban conf --- data/hooks/conf_regen/52-fail2ban | 2 + data/templates/fail2ban/jail.conf | 957 +++++++++++++------- data/templates/fail2ban/yunohost-jails.conf | 32 + 3 files changed, 647 insertions(+), 344 deletions(-) create mode 100644 data/templates/fail2ban/yunohost-jails.conf diff --git a/data/hooks/conf_regen/52-fail2ban b/data/hooks/conf_regen/52-fail2ban index 1c262078b..950f27b5b 100755 --- a/data/hooks/conf_regen/52-fail2ban +++ b/data/hooks/conf_regen/52-fail2ban @@ -9,9 +9,11 @@ do_pre_regen() { fail2ban_dir="${pending_dir}/etc/fail2ban" mkdir -p "${fail2ban_dir}/filter.d" + mkdir -p "${fail2ban_dir}/jail.d" cp yunohost.conf "${fail2ban_dir}/filter.d/yunohost.conf" cp jail.conf "${fail2ban_dir}/jail.conf" + cp yunohost-jails.conf "${fail2ban_dir}/jail.d/" } do_post_regen() { diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index 4047306a7..d80e3d0af 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -1,23 +1,52 @@ -# Fail2Ban configuration file. # -# This file was composed for Debian systems from the original one -# provided now under /usr/share/doc/fail2ban/examples/jail.conf -# for additional examples. +# WARNING: heavily refactored in 0.9.0 release. Please review and +# customize settings for your setup. # -# Comments: use '#' for comment lines and ';' for inline comments +# Changes: in most of the cases you should not modify this +# file, but provide customizations in jail.local file, +# or separate .conf files under jail.d/ directory, e.g.: # -# To avoid merges during upgrades DO NOT MODIFY THIS FILE -# and rather provide your changes in /etc/fail2ban/jail.local +# HOW TO ACTIVATE JAILS: # +# YOU SHOULD NOT MODIFY THIS FILE. +# +# It will probably be overwritten or improved in a distribution update. +# +# Provide customizations in a jail.local file or a jail.d/customisation.local. +# For example to change the default bantime for all jails and to enable the +# ssh-iptables jail the following (uncommented) would appear in the .local file. +# See man 5 jail.conf for details. +# +# [DEFAULT] +# bantime = 3600 +# +# [sshd] +# enabled = true +# +# See jail.conf(5) man page for more information + + + +# Comments: use '#' for comment lines and ';' (following a space) for inline comments + + +[INCLUDES] + +#before = paths-distro.conf +before = paths-debian.conf # The DEFAULT allows a global definition of the options. They can be overridden # in each jail afterwards. [DEFAULT] +# +# MISCELLANEOUS OPTIONS +# + # "ignoreip" can be an IP address, a CIDR mask or a DNS host. Fail2ban will not # ban a host which matches an address in this list. Several addresses can be -# defined using space separator. +# defined using space (and/or comma) separator. ignoreip = 127.0.0.1/8 # External command that will take an tagged arguments to ignore, e.g. , @@ -31,57 +60,81 @@ bantime = 600 # A host is banned if it has generated "maxretry" during the last "findtime" # seconds. -findtime = 600 -maxretry = 3 +findtime = 600 + +# "maxretry" is the number of failures before a host get banned. +maxretry = 5 # "backend" specifies the backend used to get files modification. -# Available options are "pyinotify", "gamin", "polling" and "auto". +# Available options are "pyinotify", "gamin", "polling", "systemd" and "auto". # This option can be overridden in each jail as well. # # pyinotify: requires pyinotify (a file alteration monitor) to be installed. -# If pyinotify is not installed, Fail2ban will use auto. +# If pyinotify is not installed, Fail2ban will use auto. # gamin: requires Gamin (a file alteration monitor) to be installed. -# If Gamin is not installed, Fail2ban will use auto. +# If Gamin is not installed, Fail2ban will use auto. # polling: uses a polling algorithm which does not require external libraries. +# systemd: uses systemd python library to access the systemd journal. +# Specifying "logpath" is not valid for this backend. +# See "journalmatch" in the jails associated filter config # auto: will try to use the following backends, in order: -# pyinotify, gamin, polling. +# pyinotify, gamin, polling. +# +# Note: if systemd backend is chosen as the default but you enable a jail +# for which logs are present only in its own log files, specify some other +# backend for that jail (e.g. polling) and provide empty value for +# journalmatch. See https://github.com/fail2ban/fail2ban/issues/959#issuecomment-74901200 backend = auto # "usedns" specifies if jails should trust hostnames in logs, -# warn when reverse DNS lookups are performed, or ignore all hostnames in logs +# warn when DNS lookups are performed, or ignore all hostnames in logs # -# yes: if a hostname is encountered, a reverse DNS lookup will be performed. -# warn: if a hostname is encountered, a reverse DNS lookup will be performed, +# yes: if a hostname is encountered, a DNS lookup will be performed. +# warn: if a hostname is encountered, a DNS lookup will be performed, # but it will be logged as a warning. # no: if a hostname is encountered, will not be used for banning, # but it will be logged as info. +# raw: use raw value (no hostname), allow use it for no-host filters/actions (example user) usedns = warn +# "logencoding" specifies the encoding of the log files handled by the jail +# This is used to decode the lines from the log file. +# Typical examples: "ascii", "utf-8" # -# Destination email address used solely for the interpolations in -# jail.{conf,local} configuration files. -destemail = root@localhost +# auto: will use the system locale setting +logencoding = auto +# "enabled" enables the jails. +# By default all jails are disabled, and it should stay this way. +# Enable only relevant to your setup jails in your .local or jail.d/*.conf # -# Name of the sender for mta actions -sendername = Fail2Ban +# true: jail will be enabled and log files will get monitored for changes +# false: jail is not enabled +enabled = false + + +# "filter" defines the filter to use by the jail. +# By default jails have names matching their filter name +# +filter = %(__name__)s -# Email address of the sender -sender = fail2ban@localhost # # ACTIONS # -# Default banning action (e.g. iptables, iptables-new, -# iptables-multiport, shorewall, etc) It is used to define -# action_* variables. Can be overridden globally or per -# section within jail.local file -banaction = iptables-multiport +# Some options used for actions -# email action. Since 0.8.1 upstream fail2ban uses sendmail -# MTA for the mailing. Change mta configuration parameter to mail -# if you want to revert to conventional 'mail'. +# Destination email address used solely for the interpolations in +# jail.{conf,local,d/*} configuration files. +destemail = root@localhost + +# Sender email address used solely for some actions +sender = root@localhost + +# E-mail action. Since 0.8.1 Fail2Ban uses sendmail MTA for the +# mailing. Change mta configuration parameter to mail if you want to +# revert to conventional 'mail'. mta = sendmail # Default protocol @@ -90,303 +143,461 @@ protocol = tcp # Specify chain where jumps would need to be added in iptables-* actions chain = INPUT +# Ports to be banned +# Usually should be overridden in a particular jail +port = 0:65535 + +# Format of user-agent https://tools.ietf.org/html/rfc7231#section-5.5.3 +fail2ban_agent = Fail2Ban/%(fail2ban_version)s + # # Action shortcuts. To be used to define action parameter +# Default banning action (e.g. iptables, iptables-new, +# iptables-multiport, shorewall, etc) It is used to define +# action_* variables. Can be overridden globally or per +# section within jail.local file +banaction = iptables-multiport +banaction_allports = iptables-allports + # The simplest action to take: ban only -action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] +action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report to the destemail. -action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s", sendername="%(sendername)s"] +action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] + %(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"] # ban & send an e-mail with whois report and relevant log lines # to the destemail. -action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] - %(mta)s-whois-lines[name=%(__name__)s, dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s", sendername="%(sendername)s"] +action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] + %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + +# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action +# +# ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines +# to the destemail. +action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"] + xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath=%(logpath)s, port="%(port)s"] + +# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines +# to the destemail. +action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] + %(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"] + +# Report block via blocklist.de fail2ban reporting service API +# +# See the IMPORTANT note in action.d/blocklist_de.conf for when to +# use this action. Create a file jail.d/blocklist_de.local containing +# [Init] +# blocklist_de_apikey = {api key from registration] +# +action_blocklist_de = blocklist_de[email="%(sender)s", service=%(filter)s, apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] + +# Report ban via badips.com, and use as blacklist +# +# See BadIPsAction docstring in config/action.d/badips.py for +# documentation for this action. +# +# NOTE: This action relies on banaction being present on start and therefore +# should be last action defined for a jail. +# +action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"] +# +# Report ban via badips.com (uses action.d/badips.conf for reporting only) +# +action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"] # Choose default action. To change, just override value of 'action' with the # interpolation to the chosen action shortcut (e.g. action_mw, action_mwl, etc) in jail.local # globally (section [DEFAULT]) or per specific section action = %(action_)s + # # JAILS # -# Next jails corresponds to the standard configuration in Fail2ban 0.6 which -# was shipped in Debian. Enable any defined here jail by including # -# [SECTION_NAME] -# enabled = true - +# SSH servers # -# in /etc/fail2ban/jail.local. -# -# Optionally you may override any other parameter (e.g. banaction, -# action, port, logpath, etc) in that section within jail.local -[ssh] +[sshd] + +port = ssh +logpath = %(sshd_log)s +backend = %(sshd_backend)s + + +[sshd-ddos] +# This jail corresponds to the standard configuration in Fail2ban. +# The mail-whois action send a notification e-mail with a whois request +# in the body. +port = ssh +logpath = %(sshd_log)s +backend = %(sshd_backend)s -enabled = true -port = ssh -filter = sshd -logpath = /var/log/auth.log -maxretry = 6 [dropbear] -enabled = false port = ssh -filter = dropbear -logpath = /var/log/auth.log -maxretry = 6 - -# Generic filter for pam. Has to be used with action which bans all ports -# such as iptables-allports, shorewall -[pam-generic] - -enabled = true -# pam-generic filter can be customized to monitor specific subset of 'tty's -filter = pam-generic -# port actually must be irrelevant but lets leave it all for some possible uses -port = all -banaction = iptables-allports -#port = anyport -logpath = /var/log/auth.log -maxretry = 6 - -[xinetd-fail] - -enabled = false -filter = xinetd-fail -port = all -banaction = iptables-multiport-log -logpath = /var/log/daemon.log -maxretry = 2 +logpath = %(dropbear_log)s +backend = %(dropbear_backend)s -[ssh-ddos] +[selinux-ssh] -enabled = false port = ssh -filter = sshd-ddos -logpath = /var/log/auth.log -maxretry = 6 - - -# Here we use blackhole routes for not requiring any additional kernel support -# to store large volumes of banned IPs - -[ssh-route] - -enabled = false -filter = sshd -action = route -logpath = /var/log/sshd.log -maxretry = 6 - -# Here we use a combination of Netfilter/Iptables and IPsets -# for storing large volumes of banned IPs -# -# IPset comes in two versions. See ipset -V for which one to use -# requires the ipset package and kernel support. -[ssh-iptables-ipset4] - -enabled = false -port = ssh -filter = sshd -banaction = iptables-ipset-proto4 -logpath = /var/log/sshd.log -maxretry = 6 - -[ssh-iptables-ipset6] - -enabled = false -port = ssh -filter = sshd -banaction = iptables-ipset-proto6 -logpath = /var/log/sshd.log -maxretry = 6 +logpath = %(auditd_log)s # # HTTP servers # -[apache] +[apache-auth] -enabled = false port = http,https -filter = apache-auth -logpath = /var/log/apache*/*error.log -maxretry = 6 +logpath = %(apache_error_log)s -# default action is now multiport, so apache-multiport jail was left -# for compatibility with previous (<0.7.6-2) releases -[apache-multiport] -enabled = false -port = http,https -filter = apache-auth -logpath = /var/log/apache*/*error.log -maxretry = 6 +[apache-badbots] +# Ban hosts which agent identifies spammer robots crawling the web +# for email addresses. The mail outputs are buffered. +port = http,https +logpath = %(apache_access_log)s +bantime = 172800 +maxretry = 1 + [apache-noscript] -enabled = false port = http,https -filter = apache-noscript -logpath = /var/log/apache*/*error.log -maxretry = 6 +logpath = %(apache_error_log)s + [apache-overflows] -enabled = false port = http,https -filter = apache-overflows -logpath = /var/log/apache*/*error.log +logpath = %(apache_error_log)s maxretry = 2 -[apache-modsecurity] - -enabled = false -filter = apache-modsecurity -port = http,https -logpath = /var/log/apache*/*error.log -maxretry = 2 [apache-nohome] -enabled = false -filter = apache-nohome port = http,https -logpath = /var/log/apache*/*error.log +logpath = %(apache_error_log)s maxretry = 2 + +[apache-botsearch] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-fakegooglebot] + +port = http,https +logpath = %(apache_access_log)s +maxretry = 1 +ignorecommand = %(ignorecommands_dir)s/apache-fakegooglebot + + +[apache-modsecurity] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 2 + + +[apache-shellshock] + +port = http,https +logpath = %(apache_error_log)s +maxretry = 1 + + +[openhab-auth] + +filter = openhab +action = iptables-allports[name=NoAuthFailures] +logpath = /opt/openhab/logs/request.log + + +[nginx-http-auth] + +port = http,https +logpath = %(nginx_error_log)s + +# To use 'nginx-limit-req' jail you should have `ngx_http_limit_req_module` +# and define `limit_req` and `limit_req_zone` as described in nginx documentation +# http://nginx.org/en/docs/http/ngx_http_limit_req_module.html +# or for example see in 'config/filter.d/nginx-limit-req.conf' +[nginx-limit-req] +port = http,https +logpath = %(nginx_error_log)s + +[nginx-botsearch] + +port = http,https +logpath = %(nginx_error_log)s +maxretry = 2 + + # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year # of usage in production environments. [php-url-fopen] -enabled = false port = http,https -filter = php-url-fopen -logpath = /var/www/*/logs/access_log +logpath = %(nginx_access_log)s + %(apache_access_log)s -# A simple PHP-fastcgi jail which works with lighttpd. -# If you run a lighttpd server, then you probably will -# find these kinds of messages in your error_log: -# ALERT – tried to register forbidden variable ‘GLOBALS’ -# through GET variables (attacker '1.2.3.4', file '/var/www/default/htdocs/index.php') -[lighttpd-fastcgi] +[suhosin] -enabled = false port = http,https -filter = lighttpd-fastcgi -logpath = /var/log/lighttpd/error.log +logpath = %(suhosin_log)s -# Same as above for mod_auth -# It catches wrong authentifications [lighttpd-auth] - -enabled = false +# Same as above for Apache's mod_auth +# It catches wrong authentifications port = http,https -filter = suhosin -logpath = /var/log/lighttpd/error.log +logpath = %(lighttpd_error_log)s -[nginx-http-auth] -enabled = false -filter = nginx-http-auth -port = http,https -logpath = /var/log/nginx/error.log - -# Monitor roundcube server +# +# Webmail and groupware servers +# [roundcube-auth] -enabled = false -filter = roundcube-auth port = http,https -logpath = /var/log/roundcube/userlogins +logpath = %(roundcube_errors_log)s + + +[openwebmail] + +port = http,https +logpath = /var/log/openwebmail.log + + +[horde] + +port = http,https +logpath = /var/log/horde/horde.log + + +[groupoffice] + +port = http,https +logpath = /home/groupoffice/log/info.log [sogo-auth] - -enabled = false -filter = sogo-auth -port = http, https +# Monitor SOGo groupware server # without proxy this would be: # port = 20000 +port = http,https logpath = /var/log/sogo/sogo.log +[tine20] + +logpath = /var/log/tine20/tine20.log +port = http,https + + +# +# Web Applications +# +# + +[drupal-auth] + +port = http,https +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s + +[guacamole] + +port = http,https +logpath = /var/log/tomcat*/catalina.out + +[monit] +#Ban clients brute-forcing the monit gui login +port = 2812 +logpath = /var/log/monit + + +[webmin-auth] + +port = 10000 +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[froxlor-auth] + +port = http,https +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +# +# HTTP Proxy servers +# +# + +[squid] + +port = 80,443,3128,8080 +logpath = /var/log/squid/access.log + + +[3proxy] + +port = 3128 +logpath = /var/log/3proxy.log + + # # FTP servers # -[vsftpd] - -enabled = false -port = ftp,ftp-data,ftps,ftps-data -filter = vsftpd -logpath = /var/log/vsftpd.log -# or overwrite it in jails.local to be -# logpath = /var/log/auth.log -# if you want to rely on PAM failed login attempts -# vsftpd's failregex should match both of those formats -maxretry = 6 - [proftpd] -enabled = false port = ftp,ftp-data,ftps,ftps-data -filter = proftpd -logpath = /var/log/proftpd/proftpd.log -maxretry = 6 +logpath = %(proftpd_log)s +backend = %(proftpd_backend)s [pure-ftpd] -enabled = false port = ftp,ftp-data,ftps,ftps-data -filter = pure-ftpd -logpath = /var/log/syslog -maxretry = 6 +logpath = %(pureftpd_log)s +backend = %(pureftpd_backend)s + + +[gssftpd] + +port = ftp,ftp-data,ftps,ftps-data +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s [wuftpd] -enabled = false port = ftp,ftp-data,ftps,ftps-data -filter = wuftpd -logpath = /var/log/syslog -maxretry = 6 +logpath = %(wuftpd_log)s +backend = %(wuftpd_backend)s + + +[vsftpd] +# or overwrite it in jails.local to be +# logpath = %(syslog_authpriv)s +# if you want to rely on PAM failed login attempts +# vsftpd's failregex should match both of those formats +port = ftp,ftp-data,ftps,ftps-data +logpath = %(vsftpd_log)s # # Mail servers # +# ASSP SMTP Proxy Jail +[assp] + +port = smtp,465,submission +logpath = /root/path/to/assp/logs/maillog.txt + + +[courier-smtp] + +port = smtp,465,submission +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + [postfix] -enabled = true -port = smtp,ssmtp,submission -filter = postfix -logpath = /var/log/mail.log +port = smtp,465,submission +logpath = %(postfix_log)s +backend = %(postfix_backend)s -[couriersmtp] +[postfix-rbl] -enabled = false -port = smtp,ssmtp,submission -filter = couriersmtp -logpath = /var/log/mail.log +port = smtp,465,submission +logpath = %(postfix_log)s +backend = %(postfix_backend)s +maxretry = 1 + + +[sendmail-auth] + +port = submission,465,smtp +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[sendmail-reject] + +port = smtp,465,submission +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[qmail-rbl] + +filter = qmail +port = smtp,465,submission +logpath = /service/qmail/log/main/current + + +# dovecot defaults to logging to the mail syslog facility +# but can be set by syslog_facility in the dovecot configuration. +[dovecot] + +port = pop3,pop3s,imap,imaps,submission,465,sieve +logpath = %(dovecot_log)s +backend = %(dovecot_backend)s + + +[sieve] + +port = smtp,465,submission +logpath = %(dovecot_log)s +backend = %(dovecot_backend)s + + +[solid-pop3d] + +port = pop3,pop3s +logpath = %(solidpop3d_log)s + + +[exim] + +port = smtp,465,submission +logpath = %(exim_main_log)s + + +[exim-spam] + +port = smtp,465,submission +logpath = %(exim_main_log)s + + +[kerio] + +port = imap,smtp,imaps,465 +logpath = /opt/kerio/mailserver/store/logs/security.log # @@ -394,60 +605,55 @@ logpath = /var/log/mail.log # all relevant ports get banned # -[courierauth] +[courier-auth] -enabled = false -port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s -filter = courierlogin -logpath = /var/log/mail.log +port = smtp,465,submission,imap3,imaps,pop3,pop3s +logpath = %(syslog_mail)s +backend = %(syslog_backend)s -[sasl] +[postfix-sasl] -enabled = true -port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s -filter = postfix-sasl +port = smtp,465,submission,imap3,imaps,pop3,pop3s # You might consider monitoring /var/log/mail.warn instead if you are # running postfix since it would provide the same log lines at the # "warn" level but overall at the smaller filesize. -logpath = /var/log/mail.log - -[dovecot] - -enabled = true -port = smtp,ssmtp,submission,imap2,imap3,imaps,pop3,pop3s -filter = dovecot -logpath = /var/log/mail.log - -# To log wrong MySQL access attempts add to /etc/my.cnf: -# log-error=/var/log/mysqld.log -# log-warning = 2 -[mysqld-auth] - -enabled = false -filter = mysqld-auth -port = 3306 -logpath = /var/log/mysqld.log +logpath = %(postfix_log)s +backend = %(postfix_backend)s -# DNS Servers +[perdition] + +port = imap3,imaps,pop3,pop3s +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[squirrelmail] + +port = smtp,465,submission,imap2,imap3,imaps,pop3,pop3s,http,https,socks +logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log + + +[cyrus-imap] + +port = imap3,imaps +logpath = %(syslog_mail)s +backend = %(syslog_backend)s + + +[uwimap-auth] + +port = imap3,imaps +logpath = %(syslog_mail)s +backend = %(syslog_backend)s -# These jails block attacks against named (bind9). By default, logging is off -# with bind9 installation. You will need something like this: # -# logging { -# channel security_file { -# file "/var/log/named/security.log" versions 3 size 30m; -# severity dynamic; -# print-time yes; -# }; -# category security { -# security_file; -# }; -# }; # -# in your named.conf to provide proper logging +# DNS servers +# + # !!! WARNING !!! # Since UDP is connection-less protocol, spoofing of IP and imitation @@ -456,131 +662,194 @@ logpath = /var/log/mysqld.log # victim. See # http://nion.modprobe.de/blog/archives/690-fail2ban-+-dns-fail.html # Please DO NOT USE this jail unless you know what you are doing. -#[named-refused-udp] # -#enabled = false -#port = domain,953 -#protocol = udp -#filter = named-refused -#logpath = /var/log/named/security.log +# IMPORTANT: see filter.d/named-refused for instructions to enable logging +# This jail blocks UDP traffic for DNS requests. +# [named-refused-udp] +# +# filter = named-refused +# port = domain,953 +# protocol = udp +# logpath = /var/log/named/security.log -[named-refused-tcp] +# IMPORTANT: see filter.d/named-refused for instructions to enable logging +# This jail blocks TCP traffic for DNS requests. + +[named-refused] -enabled = false port = domain,953 -protocol = tcp -filter = named-refused logpath = /var/log/named/security.log + +[nsd] + +port = 53 +action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] + %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] +logpath = /var/log/nsd.log + + +# +# Miscellaneous +# + +[asterisk] + +port = 5060,5061 +action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] + %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] + %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] +logpath = /var/log/asterisk/messages +maxretry = 10 + + [freeswitch] -enabled = false -filter = freeswitch +port = 5060,5061 +action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] + %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] + %(mta)s-whois[name=%(__name__)s, dest="%(destemail)s"] logpath = /var/log/freeswitch.log maxretry = 10 -action = iptables-multiport[name=freeswitch-tcp, port="5060,5061,5080,5081", protocol=tcp] - iptables-multiport[name=freeswitch-udp, port="5060,5061,5080,5081", protocol=udp] - -[ejabberd-auth] - -enabled = false -filter = ejabberd-auth -port = xmpp-client -protocol = tcp -logpath = /var/log/ejabberd/ejabberd.log -# Multiple jails, 1 per protocol, are necessary ATM: -# see https://github.com/fail2ban/fail2ban/issues/37 -[asterisk-tcp] +# To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld] or +# equivalent section: +# log-warning = 2 +# +# for syslog (daemon facility) +# [mysqld_safe] +# syslog +# +# for own logfile +# [mysqld] +# log-error=/var/log/mysqld.log +[mysqld-auth] -enabled = false -filter = asterisk -port = 5060,5061 -protocol = tcp -logpath = /var/log/asterisk/messages +port = 3306 +logpath = %(mysql_log)s +backend = %(mysql_backend)s -[asterisk-udp] -enabled = false -filter = asterisk -port = 5060,5061 -protocol = udp -logpath = /var/log/asterisk/messages +# Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf') +[mongodb-auth] +# change port when running with "--shardsvr" or "--configsvr" runtime operation +port = 27017 +logpath = /var/log/mongodb/mongodb.log # Jail for more extended banning of persistent abusers -# !!! WARNING !!! -# Make sure that your loglevel specified in fail2ban.conf/.local -# is not at DEBUG level -- which might then cause fail2ban to fall into -# an infinite loop constantly feeding itself with non-informative lines +# !!! WARNINGS !!! +# 1. Make sure that your loglevel specified in fail2ban.conf/.local +# is not at DEBUG level -- which might then cause fail2ban to fall into +# an infinite loop constantly feeding itself with non-informative lines +# 2. Increase dbpurgeage defined in fail2ban.conf to e.g. 648000 (7.5 days) +# to maintain entries for failed logins for sufficient amount of time [recidive] -enabled = false -filter = recidive logpath = /var/log/fail2ban.log -action = iptables-allports[name=recidive] - sendmail-whois-lines[name=recidive, logpath=/var/log/fail2ban.log] +banaction = %(banaction_allports)s bantime = 604800 ; 1 week findtime = 86400 ; 1 day -maxretry = 5 -# See the IMPORTANT note in action.d/blocklist_de.conf for when to -# use this action -# -# Report block via blocklist.de fail2ban reporting service API -# See action.d/blocklist_de.conf for more information -[ssh-blocklist] -enabled = false -filter = sshd -action = iptables[name=SSH, port=ssh, protocol=tcp] - sendmail-whois[name=SSH, dest="%(destemail)s", sender="%(sender)s", sendername="%(sendername)s"] - blocklist_de[email="%(sender)s", apikey="xxxxxx", service="%(filter)s"] -logpath = /var/log/sshd.log -maxretry = 20 +# Generic filter for PAM. Has to be used with action which bans all +# ports such as iptables-allports, shorewall +[pam-generic] +# pam-generic filter can be customized to monitor specific subset of 'tty's +banaction = %(banaction_allports)s +logpath = %(syslog_authpriv)s +backend = %(syslog_backend)s + + +[xinetd-fail] + +banaction = iptables-multiport-log +logpath = %(syslog_daemon)s +backend = %(syslog_backend)s +maxretry = 2 + + +# stunnel - need to set port for this +[stunnel] + +logpath = /var/log/stunnel4/stunnel.log + + +[ejabberd-auth] + +port = 5222 +logpath = /var/log/ejabberd/ejabberd.log + + +[counter-strike] + +logpath = /opt/cstrike/logs/L[0-9]*.log +# Firewall: http://www.cstrike-planet.com/faq/6 +tcpport = 27030,27031,27032,27033,27034,27035,27036,27037,27038,27039 +udpport = 1200,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010,27011,27012,27013,27014,27015 +action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp] + %(banaction)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp] # consider low maxretry and a long bantime # nobody except your own Nagios server should ever probe nrpe [nagios] -enabled = false -filter = nagios -action = iptables[name=Nagios, port=5666, protocol=tcp] - sendmail-whois[name=Nagios, dest="%(destemail)s", sender="%(sender)s", sendername="%(sendername)s"] -logpath = /var/log/messages ; nrpe.cfg may define a different log_facility + +logpath = %(syslog_daemon)s ; nrpe.cfg may define a different log_facility +backend = %(syslog_backend)s maxretry = 1 -[nginx] -enabled = true -port = http,https -filter = apache-auth -logpath = /var/log/nginx*/*error.log -maxretry = 6 +[oracleims] +# see "oracleims" filter file for configuration requirement for Oracle IMS v6 and above +logpath = /opt/sun/comms/messaging64/log/mail.log_current +banaction = %(banaction_allports)s -[nginx-noscript] +[directadmin] +logpath = /var/log/directadmin/login.log +port = 2222 -enabled = false -port = http,https -filter = apache-noscript -logpath = /var/log/nginx*/*error.log -maxretry = 6 +[portsentry] +logpath = /var/lib/portsentry/portsentry.history +maxretry = 1 -[nginx-overflows] +[pass2allow-ftp] +# this pass2allow example allows FTP traffic after successful HTTP authentication +port = ftp,ftp-data,ftps,ftps-data +# knocking_url variable must be overridden to some secret value in jail.local +knocking_url = /knocking/ +filter = apache-pass[knocking_url="%(knocking_url)s"] +# access log of the website with HTTP auth +logpath = %(apache_access_log)s +blocktype = RETURN +returntype = DROP +bantime = 3600 +maxretry = 1 +findtime = 1 -enabled = false -port = http,https -filter = apache-overflows -logpath = /var/log/nginx*/*error.log -maxretry = 4 -[yunohost] +[murmur] +# AKA mumble-server +port = 64738 +action = %(banaction)s[name=%(__name__)s-tcp, port="%(port)s", protocol=tcp, chain="%(chain)s", actname=%(banaction)s-tcp] + %(banaction)s[name=%(__name__)s-udp, port="%(port)s", protocol=udp, chain="%(chain)s", actname=%(banaction)s-udp] +logpath = /var/log/mumble-server/mumble-server.log -enabled = true -port = http,https -protocol = tcp -filter = yunohost -logpath = /var/log/nginx/*error.log - /var/log/nginx/*access.log -maxretry = 6 + +[screensharingd] +# For Mac OS Screen Sharing Service (VNC) +logpath = /var/log/system.log +logencoding = utf-8 + +[haproxy-http-auth] +# HAProxy by default doesn't log to file you'll need to set it up to forward +# logs to a syslog server which would then write them to disk. +# See "haproxy-http-auth" filter for a brief cautionary note when setting +# maxretry and findtime. +logpath = /var/log/haproxy.log + +[slapd] +port = ldap,ldaps +filter = slapd +logpath = /var/log/slapd.log diff --git a/data/templates/fail2ban/yunohost-jails.conf b/data/templates/fail2ban/yunohost-jails.conf new file mode 100644 index 000000000..e0d7fd7c2 --- /dev/null +++ b/data/templates/fail2ban/yunohost-jails.conf @@ -0,0 +1,32 @@ +[sshd] +enabled = true + +[sshd-ddos] +enabled = true + +[nginx-http-auth] +enabled = true + +[postfix] +enabled = true + +[dovecot] +enabled = true + +[postfix-sasl] +enable = true + +[recidive] +enable = true + +[pam-generic] +enabled = true + +[yunohost] +enabled = true +port = http,https +protocol = tcp +filter = yunohost +logpath = /var/log/nginx/*error.log + /var/log/nginx/*access.log +maxretry = 6 From c763aae4538f5966985681d3c3fdaa4d5ede4762 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 17:50:23 +0000 Subject: [PATCH 0745/1066] Removing 'hostname -d' output from stdout during postinstall --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index bcca5b0e9..1ac123a9d 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -320,7 +320,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): os.system('chmod 755 /home/yunohost.app') # Set hostname to avoid amavis bug - if os.system('hostname -d') != 0: + if os.system('hostname -d >/dev/null') != 0: os.system('hostname yunohost.yunohost.org') # Add a temporary SSOwat rule to redirect SSO to admin page From efc201c9b06c80ed9f270cae1b137acf9a59ad95 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 17:51:18 +0000 Subject: [PATCH 0746/1066] Remove messy openssl output during package install --- data/hooks/conf_regen/02-ssl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index 9f45f1554..555ef3cf8 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -10,6 +10,14 @@ do_init_regen() { exit 1 fi + LOGFILE="/tmp/yunohost-ssl-init" + + echo "Initializing a local SSL certification authority ..." + echo "(logs available in $LOGFILE)" + + rm -f $LOGFILE + touch $LOGFILE + # create certs and SSL directories mkdir -p "/etc/yunohost/certs/yunohost.org" mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts} @@ -24,9 +32,10 @@ do_init_regen() { # create default certificates if [[ ! -f /etc/yunohost/certs/yunohost.org/ca.pem ]]; then + echo -e "\n# Creating the CA key (?)\n" >>$LOGFILE openssl req -x509 -new -config "$openssl_conf" \ -days 3650 -out "${ssl_dir}/ca/cacert.pem" \ - -keyout "${ssl_dir}/ca/cakey.pem" -nodes -batch 2>&1 + -keyout "${ssl_dir}/ca/cakey.pem" -nodes -batch >>$LOGFILE 2>&1 cp "${ssl_dir}/ca/cacert.pem" \ /etc/yunohost/certs/yunohost.org/ca.pem ln -sf /etc/yunohost/certs/yunohost.org/ca.pem \ @@ -35,12 +44,13 @@ do_init_regen() { fi if [[ ! -f /etc/yunohost/certs/yunohost.org/crt.pem ]]; then + echo -e "\n# Creating initial key and certificate (?)\n" >>$LOGFILE openssl req -new -config "$openssl_conf" \ -days 730 -out "${ssl_dir}/certs/yunohost_csr.pem" \ - -keyout "${ssl_dir}/certs/yunohost_key.pem" -nodes -batch 2>&1 + -keyout "${ssl_dir}/certs/yunohost_key.pem" -nodes -batch >>$LOGFILE 2>&1 openssl ca -config "$openssl_conf" \ -days 730 -in "${ssl_dir}/certs/yunohost_csr.pem" \ - -out "${ssl_dir}/certs/yunohost_crt.pem" -batch 2>&1 + -out "${ssl_dir}/certs/yunohost_crt.pem" -batch >>$LOGFILE 2>&1 last_cert=$(ls $ssl_dir/newcerts/*.pem | sort -V | tail -n 1) chmod 640 "${ssl_dir}/certs/yunohost_key.pem" From f25a487c1965ee93596fa4ed27387734ced0359b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 18:15:52 +0000 Subject: [PATCH 0747/1066] nscd service is now unscd --- data/hooks/conf_regen/46-nsswitch | 2 +- debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch index db3a2199a..06a596e44 100755 --- a/data/hooks/conf_regen/46-nsswitch +++ b/data/hooks/conf_regen/46-nsswitch @@ -14,7 +14,7 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || sudo service nscd restart + || sudo service unscd restart } FORCE=${2:-0} diff --git a/debian/control b/debian/control index 46c95fb8b..6d8848e86 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute , mariadb-server | mysql-server, php-mysql | php-mysqlnd - , slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd + , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban From 4595dd2be4998e54af2d8fba9ed2999e3076b38b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 21:26:31 +0000 Subject: [PATCH 0748/1066] Updating master.cf for postfix to avoid a few warnings --- data/templates/postfix/plain/master.cf | 90 ++++++++++++++------------ 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/data/templates/postfix/plain/master.cf b/data/templates/postfix/plain/master.cf index ed6d87bd3..98652f097 100644 --- a/data/templates/postfix/plain/master.cf +++ b/data/templates/postfix/plain/master.cf @@ -1,53 +1,67 @@ # # Postfix master process configuration file. For details on the format -# of the file, see the master(5) manual page (command: "man 5 master"). +# of the file, see the master(5) manual page (command: "man 5 master" or +# on-line: http://www.postfix.org/master.5.html). # # Do not forget to execute "postfix reload" after editing this file. # # ========================================================================== # service type private unpriv chroot wakeup maxproc command + args -# (yes) (yes) (yes) (never) (100) +# (yes) (yes) (no) (never) (100) # ========================================================================== -smtp inet n - - - - smtpd -submission inet n - - - - smtpd - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject +smtp inet n - y - - smtpd +#smtp inet n - y - 1 postscreen +#smtpd pass - - y - - smtpd +#dnsblog unix - - y - 0 dnsblog +#tlsproxy unix - - y - 0 tlsproxy +#submission inet n - y - - smtpd +# -o syslog_name=postfix/submission +# -o smtpd_tls_security_level=encrypt +# -o smtpd_sasl_auth_enable=yes +# -o smtpd_reject_unlisted_recipient=no +# -o smtpd_client_restrictions=$mua_client_restrictions +# -o smtpd_helo_restrictions=$mua_helo_restrictions +# -o smtpd_sender_restrictions=$mua_sender_restrictions +# -o smtpd_recipient_restrictions= +# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject # -o milter_macro_daemon_name=ORIGINATING -smtps inet n - - - - smtpd - -o header_checks=pcre:/etc/postfix/header_checks - -o smtpd_tls_wrappermode=yes - -o smtpd_sasl_auth_enable=yes -# -o smtpd_client_restrictions=permit_sasl_authenticated,reject +#smtps inet n - y - - smtpd +# -o syslog_name=postfix/smtps +# -o smtpd_tls_wrappermode=yes +# -o smtpd_sasl_auth_enable=yes +# -o smtpd_reject_unlisted_recipient=no +# -o smtpd_client_restrictions=$mua_client_restrictions +# -o smtpd_helo_restrictions=$mua_helo_restrictions +# -o smtpd_sender_restrictions=$mua_sender_restrictions +# -o smtpd_recipient_restrictions= +# -o smtpd_relay_restrictions=permit_sasl_authenticated,reject # -o milter_macro_daemon_name=ORIGINATING -#628 inet n - - - - qmqpd -pickup fifo n - - 60 1 pickup -cleanup unix n - - - 0 cleanup -qmgr fifo n - n 300 1 qmgr -#qmgr fifo n - - 300 1 oqmgr -tlsmgr unix - - - 1000? 1 tlsmgr -rewrite unix - - - - - trivial-rewrite -bounce unix - - - - 0 bounce -defer unix - - - - 0 bounce -trace unix - - - - 0 bounce -verify unix - - - - 1 verify -flush unix n - - 1000? 0 flush +#628 inet n - y - - qmqpd +pickup unix n - y 60 1 pickup +cleanup unix n - y - 0 cleanup +qmgr unix n - n 300 1 qmgr +#qmgr unix n - n 300 1 oqmgr +tlsmgr unix - - y 1000? 1 tlsmgr +rewrite unix - - y - - trivial-rewrite +bounce unix - - y - 0 bounce +defer unix - - y - 0 bounce +trace unix - - y - 0 bounce +verify unix - - y - 1 verify +flush unix n - y 1000? 0 flush proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap -smtp unix - - - - - smtp -# When relaying mail as backup MX, disable fallback_relay to avoid MX loops -relay unix - - - - - smtp - -o smtp_fallback_relay= +smtp unix - - y - - smtp +relay unix - - y - - smtp # -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 -showq unix n - - - - showq -error unix - - - - - error -retry unix - - - - - error -discard unix - - - - - discard +showq unix n - y - - showq +error unix - - y - - error +retry unix - - y - - error +discard unix - - y - - discard local unix - n n - - local virtual unix - n n - - virtual -lmtp unix - - - - - lmtp -anvil unix - - - - 1 anvil -scache unix - - - - 1 scache +lmtp unix - - y - - lmtp +anvil unix - - y - 1 anvil +scache unix - - y - 1 scache # # ==================================================================== # Interfaces to non-Postfix software. Be sure to examine the manual @@ -111,8 +125,4 @@ mailman unix - n n - - pipe # Dovecot LDA dovecot unix - n n - - pipe flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -m ${extension} -# ========================================================================== -# service type private unpriv chroot wakeup maxproc command + args -# (yes) (yes) (yes) (never) (100) -# ========================================================================== -# Added using postfix-add-filter script: + From e85421721a8358135e56077192b7273b89cca0e0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 21:27:09 +0000 Subject: [PATCH 0749/1066] Now using rspamd as milter in postfix --- data/templates/postfix/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index bdd364250..04726962d 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -143,7 +143,7 @@ smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter # Rmilter milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} milter_protocol = 6 -smtpd_milters = unix:/run/rmilter/rmilter.sock +smtpd_milters = inet:localhost:11332 # Skip email without checking if milter has died milter_default_action = accept From 29e48965bb14194fd4e209587178f890cc6c070b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Jul 2017 21:28:46 +0000 Subject: [PATCH 0750/1066] Rspamd now have a dedicated log file apparently --- data/templates/yunohost/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index e3900f87e..0544c7e5e 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -18,7 +18,7 @@ postfix: log: [/var/log/mail.log,/var/log/mail.err] rspamd: status: systemctl status rspamd.service - log: /var/log/mail.log + log: /var/log/rspamd/rspamd.log redis-server: status: service log: /var/log/redis/redis-server.log From 291c8747f1460462ff84c608fd25a5fc68016eab Mon Sep 17 00:00:00 2001 From: root Date: Sun, 27 Aug 2017 11:48:51 +0000 Subject: [PATCH 0751/1066] Adding DKIM key generation back from rmilter to rspamd --- data/hooks/conf_regen/31-rspamd | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index afdfc1bf1..1aa38562d 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -18,6 +18,28 @@ do_post_regen() { [ -z "$regen_conf_files" ] && exit 0 + # retrieve variables + domain_list=$(sudo yunohost domain list --output-as plain --quiet) + + # create DKIM directory with proper permission + sudo mkdir -p /etc/dkim + sudo chown _rspamd /etc/dkim + + # create DKIM key for domains + for domain in "$domain_list"; do + domain_key="/etc/dkim/${domain}.mail.key" + [ ! -f $domain_key ] && { + sudo opendkim-genkey --domain="$domain" \ + --selector=mail --directory=/etc/dkim + sudo mv /etc/dkim/mail.private "$domain_key" + sudo mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" + } + done + + # fix DKIM keys permissions + sudo chown _rspamd /etc/dkim/*.mail.key + sudo chmod 400 /etc/dkim/*.mail.key + # compile sieve script [[ "$regen_conf_files" =~ rspamd\.sieve ]] && { sudo sievec /etc/dovecot/global_script/rspamd.sieve From a05d7b1e20ce459530b94122024378d76aa2c640 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 27 Aug 2017 14:25:15 +0200 Subject: [PATCH 0752/1066] Fixes in DKIM generation in rspamd regen-conf --- data/hooks/conf_regen/31-rspamd | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index 1aa38562d..57bac110c 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -14,21 +14,22 @@ do_pre_regen() { } do_post_regen() { - regen_conf_files=$1 - [ -z "$regen_conf_files" ] && exit 0 - - # retrieve variables - domain_list=$(sudo yunohost domain list --output-as plain --quiet) + ## + ## DKIM key generation + ## # create DKIM directory with proper permission sudo mkdir -p /etc/dkim sudo chown _rspamd /etc/dkim + # retrieve domain list + domain_list=$(sudo yunohost domain list --output-as plain --quiet) + # create DKIM key for domains - for domain in "$domain_list"; do + for domain in $domain_list; do domain_key="/etc/dkim/${domain}.mail.key" - [ ! -f $domain_key ] && { + [ ! -f "$domain_key" ] && { sudo opendkim-genkey --domain="$domain" \ --selector=mail --directory=/etc/dkim sudo mv /etc/dkim/mail.private "$domain_key" @@ -40,6 +41,9 @@ do_post_regen() { sudo chown _rspamd /etc/dkim/*.mail.key sudo chmod 400 /etc/dkim/*.mail.key + regen_conf_files=$1 + [ -z "$regen_conf_files" ] && exit 0 + # compile sieve script [[ "$regen_conf_files" =~ rspamd\.sieve ]] && { sudo sievec /etc/dovecot/global_script/rspamd.sieve From 862f8e96978126eb02d1591f2196cd172284ddff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 27 Aug 2017 17:29:56 +0200 Subject: [PATCH 0753/1066] Handle new DKIM record format --- src/yunohost/domain.py | 60 +++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 354df2887..206cd6090 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -384,17 +384,57 @@ def _get_DKIM(domain): with open(DKIM_file) as f: dkim_content = f.read() - dkim = re.match(( - r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+[^"]*' - '(?=.*(;[\s]*|")v=(?P[^";]+))' - '(?=.*(;[\s]*|")k=(?P[^";]+))' - '(?=.*(;[\s]*|")p=(?P

[^";]+))'), dkim_content, re.M | re.S - ) + # Gotta manage two formats : + # + # Legacy + # ----- + # + # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + # "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCYhnvJ+JgF9tfVbUVy6L20b2IVHygZD1GjY6k+/je+3y3C9BzPAlEitL4s2vkQpPfAevw8P6uE7s1usCa/tnTzmq4r6Q/9YRf+Wx5e79XuIY5/ZKJw1YKkDWRlGzpenu8i+6kssaPqPmtmQaYuoOwTlcpXcN9qKNIodDsaWOxBwIDAQAB" ) + # + # New + # ------ + # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " + # "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWIw/R6QIL7nbJr+yX4cS8TfFx1POMvnzbsDWAFG5U4aPqLwCkJNqrum1hG9rzCb43pGkNy5JNAh0tTZpxq+S1kBAu+DAOSHgbYVg2Tr6zTm9YNL1n/thjKB9U/dyaCzWnxlMFJYkXNlDICtSSf47ZWqcrurkAOfmtmGYQivoz8ipXMvou4t22W9DbZR+XpPbtc3RkCKK32E8O" + # "02OT9PHbsBCOakb+W1vkocVZpZo78eu5Q2phOntE9Vl2MXtd54+TEdWv6zPcGrHrF9aazEuGcNQwSUgJaHlEceT2u8X+sliwIr0on3Om2NMaTDkPgZzg2poQIDPkyxDQire7jGBwIDAQAB" + # ) + + is_legacy_format = " h=sha256; " not in dkim_content + + # Legacy DKIM format + if is_legacy_format: + dkim = re.match(( + r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+' + '[^"]*"v=(?P[^";]+);' + '[\s"]*k=(?P[^";]+);' + '[\s"]*p=(?P

[^";]+)'), dkim_content, re.M | re.S + ) + else: + dkim = re.match(( + r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+' + '[^"]*"v=(?P[^";]+);' + '[\s"]*h=(?P[^";]+);' + '[\s"]*k=(?P[^";]+);' + '[\s"]*p=(?P

[^";]+)' + '[\s"]*(?P[^";]+)'), dkim_content, re.M | re.S + ) if not dkim: return (None, None) - return ( - dkim.group('host'), - '"v={v}; k={k}; p={p}"'.format(v=dkim.group('v'), k=dkim.group('k'), p=dkim.group('p')) - ) + if is_legacy_format: + return ( + dkim.group('host'), + '"v={v}; k={k}; p={p}"'.format(v=dkim.group('v'), + k=dkim.group('k'), + p=dkim.group('p')) + ) + else: + return ( + dkim.group('host'), + '"v={v}; h={h}; k={k}; p={p}"'.format(v=dkim.group('v'), + h=dkim.group('h'), + k=dkim.group('k'), + p=dkim.group('p') + +dkim.group('p2')) + ) From 34451babe3463628848e89b53bb2c6dcc53af591 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 27 Aug 2017 19:16:25 +0200 Subject: [PATCH 0754/1066] Fall back to 1024 for the DKIM key size because nsupdate is stupid --- data/hooks/conf_regen/31-rspamd | 4 +++- src/yunohost/domain.py | 13 +++++-------- src/yunohost/dyndns.py | 1 + 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index 57bac110c..2a065e735 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -30,8 +30,10 @@ do_post_regen() { for domain in $domain_list; do domain_key="/etc/dkim/${domain}.mail.key" [ ! -f "$domain_key" ] && { + # We use a 1024 bit size because nsupdate doesn't seem to be able to + # handle 2048... sudo opendkim-genkey --domain="$domain" \ - --selector=mail --directory=/etc/dkim + --selector=mail --directory=/etc/dkim -b 1024 sudo mv /etc/dkim/mail.private "$domain_key" sudo mv /etc/dkim/mail.txt "/etc/dkim/${domain}.mail.txt" } diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 206cd6090..5196d107a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -390,14 +390,13 @@ def _get_DKIM(domain): # ----- # # mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " - # "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCYhnvJ+JgF9tfVbUVy6L20b2IVHygZD1GjY6k+/je+3y3C9BzPAlEitL4s2vkQpPfAevw8P6uE7s1usCa/tnTzmq4r6Q/9YRf+Wx5e79XuIY5/ZKJw1YKkDWRlGzpenu8i+6kssaPqPmtmQaYuoOwTlcpXcN9qKNIodDsaWOxBwIDAQAB" ) + # "p=" ) # # New # ------ + # # mail._domainkey IN TXT ( "v=DKIM1; h=sha256; k=rsa; " - # "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWIw/R6QIL7nbJr+yX4cS8TfFx1POMvnzbsDWAFG5U4aPqLwCkJNqrum1hG9rzCb43pGkNy5JNAh0tTZpxq+S1kBAu+DAOSHgbYVg2Tr6zTm9YNL1n/thjKB9U/dyaCzWnxlMFJYkXNlDICtSSf47ZWqcrurkAOfmtmGYQivoz8ipXMvou4t22W9DbZR+XpPbtc3RkCKK32E8O" - # "02OT9PHbsBCOakb+W1vkocVZpZo78eu5Q2phOntE9Vl2MXtd54+TEdWv6zPcGrHrF9aazEuGcNQwSUgJaHlEceT2u8X+sliwIr0on3Om2NMaTDkPgZzg2poQIDPkyxDQire7jGBwIDAQAB" - # ) + # "p=" ) is_legacy_format = " h=sha256; " not in dkim_content @@ -415,8 +414,7 @@ def _get_DKIM(domain): '[^"]*"v=(?P[^";]+);' '[\s"]*h=(?P[^";]+);' '[\s"]*k=(?P[^";]+);' - '[\s"]*p=(?P

[^";]+)' - '[\s"]*(?P[^";]+)'), dkim_content, re.M | re.S + '[\s"]*p=(?P

[^";]+)'), dkim_content, re.M | re.S ) if not dkim: @@ -435,6 +433,5 @@ def _get_DKIM(domain): '"v={v}; h={h}; k={k}; p={p}"'.format(v=dkim.group('v'), h=dkim.group('h'), k=dkim.group('k'), - p=dkim.group('p') - +dkim.group('p2')) + p=dkim.group('p')) ) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index f564479fe..aaf86decd 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -273,6 +273,7 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, # should be muc.the.domain.tld. or the.domain.tld if record["value"] == "@": record["value"] = domain + record["value"] = record["value"].replace(";","\;") action = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record) action = action.replace(" @.", " ") From 3f64374f9d3566522bdf1936e11366fbe107684d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 27 Aug 2017 22:37:04 +0000 Subject: [PATCH 0755/1066] Move DKIM from rmilter to rspamd --- data/hooks/conf_regen/31-rspamd | 2 ++ data/templates/rmilter/ynh_dkim.conf | 14 -------------- data/templates/rspamd/dkim_signing.conf | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 data/templates/rmilter/ynh_dkim.conf create mode 100644 data/templates/rspamd/dkim_signing.conf diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd index 2a065e735..d263d9cc9 100755 --- a/data/hooks/conf_regen/31-rspamd +++ b/data/hooks/conf_regen/31-rspamd @@ -9,6 +9,8 @@ do_pre_regen() { install -D -m 644 metrics.local.conf \ "${pending_dir}/etc/rspamd/local.d/metrics.conf" + install -D -m 644 dkim_signing.conf \ + "${pending_dir}/etc/rspamd/local.d/dkim_signing.conf" install -D -m 644 rspamd.sieve \ "${pending_dir}/etc/dovecot/global_script/rspamd.sieve" } diff --git a/data/templates/rmilter/ynh_dkim.conf b/data/templates/rmilter/ynh_dkim.conf deleted file mode 100644 index 1e5598d06..000000000 --- a/data/templates/rmilter/ynh_dkim.conf +++ /dev/null @@ -1,14 +0,0 @@ -# DKIM signing -# Note that DKIM signing should be done by rspamd in the near future -# See https://github.com/vstakhov/rmilter/issues/174 -dkim { - enable = true; - domain { - key = /etc/dkim; - domain = "*"; - selector = "mail"; - }; - header_canon = relaxed; - body_canon = relaxed; - sign_alg = sha256; -}; diff --git a/data/templates/rspamd/dkim_signing.conf b/data/templates/rspamd/dkim_signing.conf new file mode 100644 index 000000000..26718e021 --- /dev/null +++ b/data/templates/rspamd/dkim_signing.conf @@ -0,0 +1,16 @@ +allow_envfrom_empty = true; +allow_hdrfrom_mismatch = false; +allow_hdrfrom_multiple = false; +allow_username_mismatch = true; + +auth_only = true; +path = "/etc/dkim/$domain.$selector.key"; +selector = "mail"; +sign_local = true; +symbol = "DKIM_SIGNED"; +try_fallback = true; +use_domain = "header"; +use_esld = false; +use_redis = false; +key_prefix = "DKIM_KEYS"; + From 7d36869fe0607cac17e12b5bc046414e4392ba87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Aug 2017 03:12:51 +0200 Subject: [PATCH 0756/1066] Updating acme-tiny to 7ef9164, include fixes for OpenSSL 1.1 --- src/yunohost/vendor/acme_tiny/acme_tiny.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py index 6fd8558d5..fa1ee4dc5 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -1,21 +1,9 @@ #!/usr/bin/env python -import argparse -import subprocess -import json -import os -import sys -import base64 -import binascii -import time -import hashlib -import re -import copy -import textwrap -import logging +import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging try: - from urllib.request import urlopen # Python 3 + from urllib.request import urlopen # Python 3 except ImportError: - from urllib2 import urlopen # Python 2 + from urllib2 import urlopen # Python 2 #DEFAULT_CA = "https://acme-staging.api.letsencrypt.org" DEFAULT_CA = "https://acme-v01.api.letsencrypt.org" @@ -24,7 +12,6 @@ LOGGER = logging.getLogger(__name__) LOGGER.addHandler(logging.StreamHandler()) LOGGER.setLevel(logging.INFO) - def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): # helper function base64 encode for jose spec def _b64(b): @@ -178,7 +165,6 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))) - def main(argv): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, @@ -208,5 +194,5 @@ def main(argv): signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca) sys.stdout.write(signed_crt) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover main(sys.argv[1:]) From 10546333101c2092898ec5a443ff446078892418 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Aug 2017 20:12:33 +0200 Subject: [PATCH 0757/1066] Add dirty trick to be able to install php5 apps --- src/yunohost/app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a4ab8db7b..d6ce3747c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -743,6 +743,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): app_settings['install_time'] = status['installed_at'] _set_app_settings(app_instance_name, app_settings) + # Apply dirty patch to make php5 apps compatible with php7 + _patch_php5(extracted_app_folder) + os.system('chown -R admin: ' + extracted_app_folder) # Execute App install script @@ -2181,3 +2184,14 @@ def unstable_apps(): return output + +def _patch_php5(app_folder): + + files_to_patch = [] + files_to_patch.extend(glob.glob("%s/conf/*" % app_folder)) + files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) + files_to_patch.append("%s/manifest.json" % app_folder) + + for filename in files_to_patch: + c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' -e 's@php5@php7.0@g' %s" % filename + os.system(c) From 25cef3604939c06e4a8b6933c73f065da5d36950 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 4 Nov 2017 22:58:17 +0100 Subject: [PATCH 0758/1066] Set version in changelog as 3.0.0 so that 3.0.0~timestamp will be higher than 2.7.2 --- debian/changelog | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/debian/changelog b/debian/changelog index 6c671fee5..2cc315c47 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,7 @@ +yunohost (3.0.0) testing; urgency=low + + (to be removed) Dummy version to work around version comparison issues + yunohost (2.7.11.1) testing; urgency=low * [fix] Nginx Regression typo (#459) From 10c83f85c50c0fbbe6e554119c985dc8505c112d Mon Sep 17 00:00:00 2001 From: taziden Date: Mon, 2 Oct 2017 13:44:41 +0200 Subject: [PATCH 0759/1066] Create milter_headers.conf This file define a header : X-Spam which will be added to mail considered as spam by rspamd. This configuration is defined here : https://rspamd.com/doc/modules/milter_headers.html and as of the time I'm creating this PR, this documentation page is not up-to-date because X-Spam was supposed to be added when extended_spam_headers is set to True and it's not the case anymore since 1.6.2. This header is being used by rspamd.sieve which we push into dovecot in order to put spammy e-mails in Junk folder automatically. --- data/templates/rspamd/milter_headers.conf | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 data/templates/rspamd/milter_headers.conf diff --git a/data/templates/rspamd/milter_headers.conf b/data/templates/rspamd/milter_headers.conf new file mode 100644 index 000000000..d57aa6958 --- /dev/null +++ b/data/templates/rspamd/milter_headers.conf @@ -0,0 +1,9 @@ +use = ["spam-header"]; + +routines { + spam-header { + header = "X-Spam"; + value = "Yes"; + remove = 1; + } +} From 1eb5c6131aedcb96bc2d87d7a38d460669767c85 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 26 Dec 2017 17:40:49 +0100 Subject: [PATCH 0760/1066] Apply the php5 path also for upgrades --- src/yunohost/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d6ce3747c..f953c982c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -613,6 +613,9 @@ def app_upgrade(auth, app=[], url=None, file=None): env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + # Apply dirty patch to make php5 apps compatible with php7 + _patch_php5(extracted_app_folder) + # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: From 7cf64bb1c7d3cc8bdf20b447a7f2df5563dd899f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 Jan 2018 18:08:14 +0100 Subject: [PATCH 0761/1066] Also apply the php5/php7 patch for 'hidden' files (e.g. .functions) --- src/yunohost/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f953c982c..39da58180 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2193,6 +2193,7 @@ def _patch_php5(app_folder): files_to_patch = [] files_to_patch.extend(glob.glob("%s/conf/*" % app_folder)) files_to_patch.extend(glob.glob("%s/scripts/*" % app_folder)) + files_to_patch.extend(glob.glob("%s/scripts/.*" % app_folder)) files_to_patch.append("%s/manifest.json" % app_folder) for filename in files_to_patch: From fb01deb990812309615cf13c9063fdd9ac2604b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 16 Jan 2018 19:08:19 +0100 Subject: [PATCH 0762/1066] Don't try to patch folders --- src/yunohost/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 39da58180..f07e34783 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2197,5 +2197,10 @@ def _patch_php5(app_folder): files_to_patch.append("%s/manifest.json" % app_folder) for filename in files_to_patch: + + # Ignore non-regular files + if not os.path.isfile(filename): + continue + c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' -e 's@php5@php7.0@g' %s" % filename os.system(c) From 16166917cbe36b8b62c49652dd91b01342845d8f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 17 Jan 2018 18:51:58 +0100 Subject: [PATCH 0763/1066] Also patch the php socket path --- src/yunohost/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f07e34783..8b9f699d4 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2202,5 +2202,8 @@ def _patch_php5(app_folder): if not os.path.isfile(filename): continue - c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' -e 's@php5@php7.0@g' %s" % filename + c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' " \ + "-e 's@/var/run/php5-fpm@/var/run/php/php7.0-fpm@g' " \ + "-e 's@php5@php7.0@g' " \ + "%s" % filename os.system(c) From 30ad9a95476ae78d05592a1597bb64d7b769f8a1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 17 Jan 2018 23:03:00 +0100 Subject: [PATCH 0764/1066] Nscd version fixed at deb repo level, so this isn't needed anymore --- src/yunohost/tools.py | 4 ++-- src/yunohost/user.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 1ac123a9d..99ae5e636 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -104,7 +104,7 @@ def tools_ldapinit(): auth.update('cn=admin', admin_dict) # Force nscd to refresh cache to take admin creation into account - subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) + subprocess.call(['nscd', '-i', 'passwd']) # Check admin actually exists now try: @@ -203,7 +203,7 @@ def _set_hostname(hostname, pretty_hostname=None): pretty_hostname = "(YunoHost/%s)" % hostname # First clear nsswitch cache for hosts to make sure hostname is resolved... - subprocess.call(['nscd', '-i', 'hosts'], stderr=open(os.devnull, 'wb')) + subprocess.call(['nscd', '-i', 'hosts']) # Then call hostnamectl commands = [ diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 740258256..bed5fb8c8 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -194,7 +194,7 @@ def user_create(auth, username, firstname, lastname, mail, password, if auth.add('uid=%s,ou=users' % username, attr_dict): # Invalidate passwd to take user creation into account - subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) + subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] @@ -233,7 +233,7 @@ def user_delete(auth, username, purge=False): if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account - subprocess.call(['nscd', '-i', 'passwd'], stderr=open(os.devnull, 'wb')) + subprocess.call(['nscd', '-i', 'passwd']) # Update SFTP user group memberlist = auth.search(filter='cn=sftpusers', attrs=['memberUid'])[0]['memberUid'] From 441eeb46800b77a5ecdfafb7348dbb7ade8bd2c2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 21:14:26 +0100 Subject: [PATCH 0765/1066] Update the php-fpm service --- data/templates/yunohost/services.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 0544c7e5e..ada80b980 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -36,9 +36,9 @@ metronome: slapd: status: service log: /var/log/syslog -php5-fpm: +php-fpm: status: service - log: /var/log/php5-fpm.log + log: /var/log/php7.0-fpm.log yunohost-api: status: service log: /var/log/yunohost/yunohost-api.log @@ -58,3 +58,4 @@ amavis: null postgrey: null spamassassin: null rmilter: null +php5-fpm: null From 8fe57fbbc5764d3e13c3513afcdb2c49d04b117e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Jan 2018 21:18:31 +0100 Subject: [PATCH 0766/1066] Add a migration for php5-fpm pools to php7 --- .../0003_php5_to_php7_pools.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/yunohost/data_migrations/0003_php5_to_php7_pools.py diff --git a/src/yunohost/data_migrations/0003_php5_to_php7_pools.py b/src/yunohost/data_migrations/0003_php5_to_php7_pools.py new file mode 100644 index 000000000..55c8ef08d --- /dev/null +++ b/src/yunohost/data_migrations/0003_php5_to_php7_pools.py @@ -0,0 +1,67 @@ +import os +import glob +from shutil import copy2 + +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration +from yunohost.service import _run_service_command + +logger = getActionLogger('yunohost.migration') + +PHP5_POOLS = "/etc/php5/fpm/pool.d" +PHP7_POOLS = "/etc/php/7.0/fpm/pool.d" + +PHP5_SOCKETS_PREFIX = "/var/run/php5-fpm" +PHP7_SOCKETS_PREFIX = "/run/php/php7.0-fpm" + +MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {}".format(PHP5_POOLS) + + +class MyMigration(Migration): + "Migrate php5-fpm 'pool' conf files to php7 stuff" + + def migrate(self): + + # Get list of php5 pool files + php5_pool_files = glob.glob("{}/*.conf".format(PHP5_POOLS)) + + # Keep only basenames + php5_pool_files = [os.path.basename(f) for f in php5_pool_files] + + # Ignore the "www.conf" (default stuff, probably don't want to touch it ?) + php5_pool_files = [f for f in php5_pool_files if f != "www.conf"] + + for f in php5_pool_files: + + # Copy the files to the php7 pool + src = "{}/{}".format(PHP5_POOLS, f) + dest = "{}/{}".format(PHP7_POOLS, f) + copy2(src, dest) + + # Replace the socket prefix if it's found + c = "sed -i -e 's@{}@{}@g' {}".format(PHP5_SOCKETS_PREFIX, PHP7_SOCKETS_PREFIX, dest) + os.system(c) + + # Also add a comment that it was automatically moved from php5 + # (for human traceability and backward migration) + c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) + os.system(c) + + # Reload/restart the php pools + _run_service_command("restart", "php-fpm") + + def backward(self): + + # Get list of php7 pool files + php7_pool_files = glob.glob("{}/*.conf".format(PHP7_POOLS)) + + # Keep only files which have the migration comment + php7_pool_files = [f for f in php7_pool_files if open(f).readline().strip() == MIGRATION_COMMENT] + + # Delete those files + for f in php7_pool_files: + os.remove(f) + + # Reload/restart the php pools + _run_service_command("restart", "php-fpm") From 0877901d8811e7cc81ad3ca5d3765813109a55b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 31 Jan 2018 15:39:12 +0100 Subject: [PATCH 0767/1066] Can't name this service php-fpm, gotta stick to php7.0-fpm... --- data/templates/yunohost/services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index ada80b980..8d640dbbf 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -36,7 +36,7 @@ metronome: slapd: status: service log: /var/log/syslog -php-fpm: +php7.0-fpm: status: service log: /var/log/php7.0-fpm.log yunohost-api: From ab96c7948faff30de7acc67d6d855be139a36c91 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Date: Sat, 3 Feb 2018 23:54:02 +0100 Subject: [PATCH 0768/1066] typo enable/enabled --- data/templates/fail2ban/yunohost-jails.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/fail2ban/yunohost-jails.conf b/data/templates/fail2ban/yunohost-jails.conf index e0d7fd7c2..bf3bcb6e3 100644 --- a/data/templates/fail2ban/yunohost-jails.conf +++ b/data/templates/fail2ban/yunohost-jails.conf @@ -14,10 +14,10 @@ enabled = true enabled = true [postfix-sasl] -enable = true +enabled = true [recidive] -enable = true +enabled = true [pam-generic] enabled = true From 4baa01143e47f88d1dbdfe8336818f8292ba008b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 9 Feb 2018 21:03:40 +0100 Subject: [PATCH 0769/1066] Move php5/7 migration as number 4 --- .../{0003_php5_to_php7_pools.py => 0004_php5_to_php7_pools.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/yunohost/data_migrations/{0003_php5_to_php7_pools.py => 0004_php5_to_php7_pools.py} (100%) diff --git a/src/yunohost/data_migrations/0003_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py similarity index 100% rename from src/yunohost/data_migrations/0003_php5_to_php7_pools.py rename to src/yunohost/data_migrations/0004_php5_to_php7_pools.py From d597f31e13ebbf31828645e2764d0013188d59af Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 10 Feb 2018 00:32:32 +0100 Subject: [PATCH 0770/1066] Forgot to fix the php service name in the migration... --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 55c8ef08d..ad659b5cf 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -49,7 +49,7 @@ class MyMigration(Migration): os.system(c) # Reload/restart the php pools - _run_service_command("restart", "php-fpm") + _run_service_command("restart", "php7.0-fpm") def backward(self): From fbc18c56aa043fba75e864cae6e0d281ec8ed031 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 6 Mar 2018 00:35:19 +0100 Subject: [PATCH 0771/1066] Typo for php-fpm service --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index ad659b5cf..0ff9b77c8 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -64,4 +64,4 @@ class MyMigration(Migration): os.remove(f) # Reload/restart the php pools - _run_service_command("restart", "php-fpm") + _run_service_command("restart", "php7.0-fpm") From e0ac622ffcaf9fbbb9b5ee827d5c92334893f588 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 6 Mar 2018 13:54:16 +0100 Subject: [PATCH 0772/1066] Skip all migrations during postinstall --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 99ae5e636..ba90b0002 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -397,7 +397,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): _install_appslist_fetch_cron() # Init migrations (skip them, no need to run them on a fresh system) - tools_migrations_migrate(target=2, skip=True, auto=True) + tools_migrations_migrate(skip=True, auto=True) os.system('touch /etc/yunohost/installed') From 45bc155921d52ef980e38d161271e1c72217e106 Mon Sep 17 00:00:00 2001 From: kitoy Date: Tue, 19 Dec 2017 19:00:03 +0100 Subject: [PATCH 0773/1066] Bug connu sur bug.debian.org https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=844271 --- data/templates/dovecot/dovecot.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/dovecot/dovecot.conf b/data/templates/dovecot/dovecot.conf index 5ea10ea79..116bb2db7 100644 --- a/data/templates/dovecot/dovecot.conf +++ b/data/templates/dovecot/dovecot.conf @@ -16,7 +16,7 @@ mail_plugins = $mail_plugins quota ssl = yes ssl_cert = Date: Thu, 15 Mar 2018 01:06:50 +0100 Subject: [PATCH 0774/1066] Also patch nginx conf for the php5/7 migration --- .../0004_php5_to_php7_pools.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 0ff9b77c8..560b7a25c 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -51,6 +51,16 @@ class MyMigration(Migration): # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") + # Get list of nginx conf file + nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") + for f in nginx_conf_files: + # Replace the socket prefix if it's found + c = "sed -i -e 's@{}@{}@g' {}".format(PHP5_SOCKETS_PREFIX, PHP7_SOCKETS_PREFIX, f) + os.system(c) + + # Reload nginx + _run_service_command("reload", "nginx") + def backward(self): # Get list of php7 pool files @@ -65,3 +75,13 @@ class MyMigration(Migration): # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") + + # Get list of nginx conf file + nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") + for f in nginx_conf_files: + # Replace the socket prefix if it's found + c = "sed -i -e 's@{}@{}@g' {}".format(PHP7_SOCKETS_PREFIX, PHP5_SOCKETS_PREFIX, f) + os.system(c) + + # Reload nginx + _run_service_command("reload", "nginx") From 696ab8a98955f3c955c71f3f67e28e2e797acf84 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 16 Mar 2018 17:43:22 +0100 Subject: [PATCH 0775/1066] Stop dis php5 fpm old guy --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 560b7a25c..90023bcd4 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -50,6 +50,7 @@ class MyMigration(Migration): # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") + os.system("systemctl stop php5-fpm") # Get list of nginx conf file nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") @@ -74,7 +75,8 @@ class MyMigration(Migration): os.remove(f) # Reload/restart the php pools - _run_service_command("restart", "php7.0-fpm") + _run_service_command("stop", "php7.0-fpm") + os.system("systemctl start php5-fpm") # Get list of nginx conf file nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") From 1d7171543688497ba92d14ff4650ea869c53dfe7 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Thu, 8 Mar 2018 18:53:21 +0100 Subject: [PATCH 0776/1066] [fix] Remove warning from equivs --- data/helpers.d/package | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/helpers.d/package b/data/helpers.d/package index a1d29651e..71771a568 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -101,6 +101,10 @@ ynh_package_install_from_equivs () { # Build and install the package local TMPDIR=$(mktemp -d) + + # Force the compatibility level at 10, levels below are deprecated + echo 10 > /usr/share/equivs/template/debian/compat + # Note that the cd executes into a sub shell # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg From e36c7daa99238dfdaeb93f2ecd5d1b8b02682412 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 17 Apr 2018 18:57:01 +0200 Subject: [PATCH 0777/1066] Apply the dirty php5/php7 patch to scripts/conf when restoring an app --- src/yunohost/backup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 15c793802..f8176e79b 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -43,7 +43,7 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file from yunohost.app import ( - app_info, _is_installed, _parse_app_instance_name + app_info, _is_installed, _parse_app_instance_name, _patch_php5 ) from yunohost.hook import ( hook_list, hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER @@ -1199,6 +1199,9 @@ class RestoreManager(): app_settings_in_archive = os.path.join(app_dir_in_archive, 'settings') app_scripts_in_archive = os.path.join(app_settings_in_archive, 'scripts') + # Apply dirty patch to make php5 apps compatible with php7 + _patch_php5(app_settings_in_archive) + # Check if the app has a restore script app_restore_script_in_archive = os.path.join(app_scripts_in_archive, 'restore') From f846f5b7f8df5e64e2d32e7b18178437da70aeb1 Mon Sep 17 00:00:00 2001 From: kitoy Date: Tue, 19 Dec 2017 19:02:15 +0100 Subject: [PATCH 0778/1066] Ajout de connexion smtp over tls et STARTTLS --- data/templates/postfix/plain/master.cf | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/data/templates/postfix/plain/master.cf b/data/templates/postfix/plain/master.cf index 98652f097..cf6b4bcae 100644 --- a/data/templates/postfix/plain/master.cf +++ b/data/templates/postfix/plain/master.cf @@ -10,6 +10,16 @@ # (yes) (yes) (no) (never) (100) # ========================================================================== smtp inet n - y - - smtpd + +smtps inet n - y - - smtpd + -o header_checks=pcre:/etc/postfix/header_checks + -o smtpd_tls_wrappermode=yes + -o smtpd_sasl_auth_enable=yes + +submission inet n - y - - smtpd + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_client_restrictions=permit_sasl_authenticated,reject #smtp inet n - y - 1 postscreen #smtpd pass - - y - - smtpd #dnsblog unix - - y - 0 dnsblog @@ -122,7 +132,6 @@ mailman unix - n n - - pipe flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user} -# Dovecot LDA -dovecot unix - n n - - pipe +# Dovecot LDA +dovecot unix - n n - - pipe flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -m ${extension} - From 6d158aae194c1621271d28f9d48873feb273e759 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 17 Apr 2018 16:25:37 +0200 Subject: [PATCH 0779/1066] master.cf : comment smtps and tweak submission --- data/templates/postfix/plain/master.cf | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/data/templates/postfix/plain/master.cf b/data/templates/postfix/plain/master.cf index cf6b4bcae..2d8712604 100644 --- a/data/templates/postfix/plain/master.cf +++ b/data/templates/postfix/plain/master.cf @@ -10,24 +10,14 @@ # (yes) (yes) (no) (never) (100) # ========================================================================== smtp inet n - y - - smtpd - -smtps inet n - y - - smtpd - -o header_checks=pcre:/etc/postfix/header_checks - -o smtpd_tls_wrappermode=yes - -o smtpd_sasl_auth_enable=yes - -submission inet n - y - - smtpd - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_client_restrictions=permit_sasl_authenticated,reject #smtp inet n - y - 1 postscreen #smtpd pass - - y - - smtpd #dnsblog unix - - y - 0 dnsblog #tlsproxy unix - - y - 0 tlsproxy -#submission inet n - y - - smtpd -# -o syslog_name=postfix/submission -# -o smtpd_tls_security_level=encrypt -# -o smtpd_sasl_auth_enable=yes +submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes # -o smtpd_reject_unlisted_recipient=no # -o smtpd_client_restrictions=$mua_client_restrictions # -o smtpd_helo_restrictions=$mua_helo_restrictions @@ -132,6 +122,6 @@ mailman unix - n n - - pipe flags=FR user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user} -# Dovecot LDA -dovecot unix - n n - - pipe +# Dovecot LDA +dovecot unix - n n - - pipe flags=DRhu user=vmail:mail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -m ${extension} From cc3c704ba84c3ed6260153ebd88aa4332ee3c620 Mon Sep 17 00:00:00 2001 From: frju365 Date: Mon, 23 Apr 2018 22:05:39 +0200 Subject: [PATCH 0780/1066] [enh] Enable HTTP2 protocol in nginx conf (#448) * [Add] http2 * Update yunohost_admin.conf --- data/templates/nginx/plain/yunohost_admin.conf | 4 ++-- data/templates/nginx/server.tpl.conf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index b1fb0d2ef..85e29e556 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -12,8 +12,8 @@ server { } server { - listen 443 ssl default_server; - listen [::]:443 ssl default_server; + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; ssl_certificate /etc/yunohost/certs/yunohost.org/crt.pem; ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index df722b526..1a5e21502 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -16,8 +16,8 @@ server { } server { - listen 443 ssl; - listen [::]:443 ssl; + listen 443 ssl http2; + listen [::]:443 ssl http2; server_name {{ domain }}; ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; From 13d50734167239702b7fe5059a61b5214862e4e1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 24 Apr 2018 17:17:27 +0200 Subject: [PATCH 0781/1066] [fix] postrm is called in a lot of various way, deleting the installed flag when it shouldnt --- debian/postrm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/postrm b/debian/postrm index cf2b6afe7..93338c4ff 100644 --- a/debian/postrm +++ b/debian/postrm @@ -1,12 +1,19 @@ #!/bin/bash +# See https://manpages.debian.org/testing/dpkg-dev/deb-postrm.5.en.html +# to understand when / how this script is called... + set -e if [ "$1" = "purge" ]; then update-rc.d yunohost-firewall remove >/dev/null + rm -f /etc/yunohost/installed +fi + +if [ "$1" = "remove" ]; then + rm -f /etc/yunohost/installed fi -rm -f /etc/yunohost/installed #DEBHELPER# From c5d4ab5d79fbe549b07a03f0ab6a8c664707242c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 24 Apr 2018 18:41:01 +0200 Subject: [PATCH 0782/1066] Remove imap3 things in fail2ban conf, c.f. https://github.com/fail2ban/fail2ban/issues/1942 --- data/templates/fail2ban/jail.conf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/templates/fail2ban/jail.conf b/data/templates/fail2ban/jail.conf index d80e3d0af..05eb7e7a8 100644 --- a/data/templates/fail2ban/jail.conf +++ b/data/templates/fail2ban/jail.conf @@ -607,14 +607,14 @@ logpath = /opt/kerio/mailserver/store/logs/security.log [courier-auth] -port = smtp,465,submission,imap3,imaps,pop3,pop3s +port = smtp,465,submission,imaps,pop3,pop3s logpath = %(syslog_mail)s backend = %(syslog_backend)s [postfix-sasl] -port = smtp,465,submission,imap3,imaps,pop3,pop3s +port = smtp,465,submission,imap,imaps,pop3,pop3s # You might consider monitoring /var/log/mail.warn instead if you are # running postfix since it would provide the same log lines at the # "warn" level but overall at the smaller filesize. @@ -624,27 +624,27 @@ backend = %(postfix_backend)s [perdition] -port = imap3,imaps,pop3,pop3s +port = imap,imaps,pop3,pop3s logpath = %(syslog_mail)s backend = %(syslog_backend)s [squirrelmail] -port = smtp,465,submission,imap2,imap3,imaps,pop3,pop3s,http,https,socks +port = smtp,465,submission,imap,imap2,imaps,pop3,pop3s,http,https,socks logpath = /var/lib/squirrelmail/prefs/squirrelmail_access_log [cyrus-imap] -port = imap3,imaps +port = imap,imaps logpath = %(syslog_mail)s backend = %(syslog_backend)s [uwimap-auth] -port = imap3,imaps +port = imap,imaps logpath = %(syslog_mail)s backend = %(syslog_backend)s From 922821418e174e3208504615003bb2d67f0226ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Apr 2018 19:18:17 +0200 Subject: [PATCH 0783/1066] Draft of DB migration from Postgresql 9.4 to 9.6 --- locales/en.json | 3 ++ .../0005_postgresql_9p4_to_9p6.py | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py diff --git a/locales/en.json b/locales/en.json index 0f45c7a8b..7efe0df97 100644 --- a/locales/en.json +++ b/locales/en.json @@ -225,6 +225,7 @@ "migration_description_0001_change_cert_group_to_sslcert": "Change certificates group permissions from 'metronome' to 'ssl-cert'", "migration_description_0002_migrate_to_tsig_sha256": "Improve security of dyndns TSIG by using SHA512 instead of MD5", "migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0", + "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", @@ -237,6 +238,8 @@ "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.", "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an applist or are not flagged as 'working'. Consequently, we cannot guarantee that they will still work after the upgrade : {problematic_apps}", "migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten at the end of the upgrade : {manually_modified_files}", + "migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!", + "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py new file mode 100644 index 000000000..d71a356ed --- /dev/null +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -0,0 +1,44 @@ +import subprocess + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration +from yunohost.service import _run_service_command + +logger = getActionLogger('yunohost.migration') + + +class MyMigration(Migration): + "Migrate DBs from Postgresql 9.4 to 9.6 after migrating to Stretch" + + + def migrate(self): + + if not self.package_is_installed("postgresql-9.4"): + logger.warning(m18n.n("migration_0005_postgresql_94_not_installed")) + return + + if not self.package_is_installed("postgresql-9.6"): + raise MoulinetteError(m18n.n("migration_0005_postgresql_96_not_installed")) + + # FIXME / TODO : maybe add checks about the size of + # /var/lib/postgresql/9.4/main/base/ compared to available space ? + + subprocess.check_call("service postgresql stop", shell=True) + subprocess.check_call("pg_dropcluster --stop 9.6 main", shell=True) + subprocess.check_call("pg_upgradecluster -m upgrade 9.4 main", shell=True) + subprocess.check_call("pg_dropcluster --stop 9.4 main", shell=True) + subprocess.check_call("service postgresql start", shell=True) + + def backward(self): + + pass + + + def package_is_installed(self, package_name): + + p = subprocess.Popen("dpkg --list | grep -q -w {}".format(package_name), shell=True) + p.communicate() + return p.returncode == 0 + From 3955aee5ca789312f69cb171941bfa2924202b2c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Apr 2018 19:22:20 +0200 Subject: [PATCH 0784/1066] Add missing description for PHP5->7 migration --- locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/en.json b/locales/en.json index 7efe0df97..b7a2190df 100644 --- a/locales/en.json +++ b/locales/en.json @@ -225,6 +225,7 @@ "migration_description_0001_change_cert_group_to_sslcert": "Change certificates group permissions from 'metronome' to 'ssl-cert'", "migration_description_0002_migrate_to_tsig_sha256": "Improve security of dyndns TSIG by using SHA512 instead of MD5", "migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0", + "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", From cc4a2f2f2edff2b8e78808a095b7580f62a88c53 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 30 Apr 2018 02:12:48 +0200 Subject: [PATCH 0785/1066] Also patch php5 app when calling remove --- src/yunohost/app.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8b9f699d4..20997de77 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -843,6 +843,10 @@ def app_remove(auth, app): except: pass + # Apply dirty patch to make php5 apps compatible with php7 (e.g. the remove + # script might date back from jessie install) + _patch_php5(app_setting_path) + os.system('cp -a %s /tmp/yunohost_remove && chown -hR admin: /tmp/yunohost_remove' % app_setting_path) os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') From 25bd5648a790caf2884adfa916fd220f3dd1e64b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 May 2018 18:16:08 +0200 Subject: [PATCH 0786/1066] Disabling http2 for now as it's causing weird issues with curl --- data/templates/nginx/server.tpl.conf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 1a5e21502..cfead0c0f 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -16,8 +16,11 @@ server { } server { - listen 443 ssl http2; - listen [::]:443 ssl http2; + # Disabling http2 for now as it's causing weird issues with curl + #listen 443 ssl http2; + #listen [::]:443 ssl http2; + listen 443 ssl; + listen [::]:443 ssl; server_name {{ domain }}; ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; From 3feae2e87768c05d7c74e1ca3a2abb75fb10c6dd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 May 2018 19:43:33 +0200 Subject: [PATCH 0787/1066] Some ecdh_curve don't work on jessie --- data/templates/nginx/plain/yunohost_admin.conf | 7 ++++++- data/templates/nginx/server.tpl.conf | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index b1fb0d2ef..dff6d0636 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -19,8 +19,13 @@ server { ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + # (this doesn't work on jessie though ...?) + # ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + + # As suggested by https://cipherli.st/ + ssl_ecdh_curve secp384r1; ssl_prefer_server_ciphers on; diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index df722b526..f55df65f1 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -24,8 +24,13 @@ server { ssl_certificate_key /etc/yunohost/certs/{{ domain }}/key.pem; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + # (this doesn't work on jessie though ...?) + # ssl_ecdh_curve secp521r1:secp384r1:prime256v1; + + # As suggested by https://cipherli.st/ + ssl_ecdh_curve secp384r1; ssl_prefer_server_ciphers on; From 906ea985928d5082fb5170a3a94107b9f9a101ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 3 May 2018 01:15:55 +0200 Subject: [PATCH 0788/1066] Update changelog for Stretch beta 3.0.0~beta1 --- debian/changelog | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 2cc315c47..39a202dc8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,6 +1,8 @@ -yunohost (3.0.0) testing; urgency=low +yunohost (3.0.0~beta1) testing; urgency=low - (to be removed) Dummy version to work around version comparison issues + Beta release for Stretch + + -- Alexandre Aubin Thu, 03 May 2018 03:04:45 +0000 yunohost (2.7.11.1) testing; urgency=low From 51fdef0c537f1239d105539bb19bbc66484d7b27 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Sat, 5 May 2018 10:32:43 +0200 Subject: [PATCH 0789/1066] Helper - Add no_extract_option --- data/helpers.d/utils | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index cc057eb0b..1cbc7d23c 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -109,6 +109,10 @@ ynh_backup_before_upgrade () { # # (Optionnal) Name of the local archive (offline setup support) # # default: ${src_id}.${src_format} # SOURCE_FILENAME=example.tar.gz +# # (Optional) If it set as false don't extract the source. +# # (Usefull to get a debian package of a python wheel.) +# # default: true +# SOURCE_EXTRACT=(true|false) # # Details: # This helper downloads sources from SOURCE_URL if there is no local source @@ -137,6 +141,7 @@ ynh_setup_source () { local src_sum=$(grep 'SOURCE_SUM=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_sumprg=$(grep 'SOURCE_SUM_PRG=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_format=$(grep 'SOURCE_FORMAT=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) + local src_extract=$(grep 'SOURCE_EXTRACT=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_in_subdir=$(grep 'SOURCE_IN_SUBDIR=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) local src_filename=$(grep 'SOURCE_FILENAME=' "$YNH_CWD/../conf/${src_id}.src" | cut -d= -f2-) @@ -145,6 +150,7 @@ ynh_setup_source () { src_in_subdir=${src_in_subdir:-true} src_format=${src_format:-tar.gz} src_format=$(echo "$src_format" | tr '[:upper:]' '[:lower:]') + src_extract=${src_extract:-true} if [ "$src_filename" = "" ] ; then src_filename="${src_id}.${src_format}" fi @@ -163,7 +169,11 @@ ynh_setup_source () { # Extract source into the app dir mkdir -p "$dest_dir" - if [ "$src_format" = "zip" ] + + if ! "$src_extract" + then + mv $src_filename $dest_dir + elif [ "$src_format" = "zip" ] then # Zip format # Using of a temp directory, because unzip doesn't manage --strip-components From 9d5b918929aafd525e95af454e62d40ffdcf006e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 5 May 2018 12:25:19 +0200 Subject: [PATCH 0790/1066] [mod] remove useless import --- src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index d71a356ed..0e6edf5e6 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -4,7 +4,6 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.service import _run_service_command logger = getActionLogger('yunohost.migration') @@ -41,4 +40,3 @@ class MyMigration(Migration): p = subprocess.Popen("dpkg --list | grep -q -w {}".format(package_name), shell=True) p.communicate() return p.returncode == 0 - From a9b931b622c05033d660a7dc753ebaba0b721533 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 5 May 2018 12:25:36 +0200 Subject: [PATCH 0791/1066] [fix] missing import --- src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index 0e6edf5e6..a6bfafcf2 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -1,5 +1,6 @@ import subprocess +from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger From 87624d1327485ef4ce04dd5f2b83679f96a4ecc2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 6 May 2018 03:06:58 +0200 Subject: [PATCH 0792/1066] Updating changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 39a202dc8..36168f6c5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (3.0.0~beta1.1) testing; urgency=low + + Fixes in the postgresql migration + + -- Alexandre Aubin Sun, 06 May 2018 03:06:00 +0000 + yunohost (3.0.0~beta1) testing; urgency=low Beta release for Stretch From e66a9d5e34d36f0a1e9322b65f9dd73ab57432da Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Sun, 6 May 2018 18:02:41 +0200 Subject: [PATCH 0793/1066] [i18n] Translated using Weblate (Portuguese) (#461) Currently translated at 28.8% (111 of 385 strings) --- locales/pt.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index b0260b73a..88690249c 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -162,5 +162,10 @@ "backup_extracting_archive": "Extraindo arquivo de backup...", "backup_hook_unknown": "Gancho de backup '{hook:s}' desconhecido", "backup_nothings_done": "Não há nada para guardar", - "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas" + "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas", + "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Olhe para o `app changeurl` se estiver disponível.", + "app_already_up_to_date": "{app:s} já está atualizado", + "app_argument_choice_invalid": "Escolha inválida para o argumento '{name:s}', deve ser um dos {choices:s}", + "app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}", + "app_argument_required": "O argumento '{name:s}' é obrigatório" } From 460a40e61d1d791ed165744a94989efc0986dd77 Mon Sep 17 00:00:00 2001 From: YunoHost Bot Date: Sun, 6 May 2018 18:02:41 +0200 Subject: [PATCH 0794/1066] [i18n] Translated using Weblate (Portuguese) (#461) Currently translated at 28.8% (111 of 385 strings) --- locales/pt.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index b0260b73a..88690249c 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -162,5 +162,10 @@ "backup_extracting_archive": "Extraindo arquivo de backup...", "backup_hook_unknown": "Gancho de backup '{hook:s}' desconhecido", "backup_nothings_done": "Não há nada para guardar", - "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas" + "backup_output_directory_forbidden": "Diretório de saída proibido. Os backups não podem ser criados em /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives subpastas", + "app_already_installed_cant_change_url": "Este aplicativo já está instalado. A URL não pode ser alterada apenas por esta função. Olhe para o `app changeurl` se estiver disponível.", + "app_already_up_to_date": "{app:s} já está atualizado", + "app_argument_choice_invalid": "Escolha inválida para o argumento '{name:s}', deve ser um dos {choices:s}", + "app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}", + "app_argument_required": "O argumento '{name:s}' é obrigatório" } From 34838d53f868851da52ff79362b41ddeda1abda0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 6 May 2018 16:41:18 +0000 Subject: [PATCH 0795/1066] Update changelog for 2.7.12 release --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 42f50bae8..bc20b60dc 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (2.7.12) stable; urgency=low + + * [i18n] Improve translation for Portuguese + * Bump version number for stable release + + -- Alexandre Aubin Sun, 06 May 2018 16:40:11 +0000 + yunohost (2.7.11.1) testing; urgency=low * [fix] Nginx Regression typo (#459) From 6d8dd984982a19ee698d573dc6e45264af0e54bc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 6 May 2018 23:13:01 +0000 Subject: [PATCH 0796/1066] Update changelog for 2.7.13 release --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2d15bd795..a935b1b20 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +yunohost (2.7.13) testing; urgency=low + + * [enh] Add 'manual migration' mechanism to the migration framework (#429) + * [enh] Add Stretch migration (#433) + * [enh] Use recommended ECDH curves (#454) + + -- Alexandre Aubin Sun, 06 May 2018 23:10:13 +0000 + yunohost (2.7.12) stable; urgency=low * [i18n] Improve translation for Portuguese From c75a3f1b4480cb1bd47917af1b80f39eaae341ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 May 2018 05:50:39 +0200 Subject: [PATCH 0797/1066] Also disabling httpt2 in yunohost_admin.conf --- data/templates/nginx/plain/yunohost_admin.conf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 85e29e556..f7b46e767 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -12,8 +12,11 @@ server { } server { - listen 443 ssl http2 default_server; - listen [::]:443 ssl http2 default_server; + # Disabling http2 for now as it's causing weird issues with curl + #listen 443 ssl http2 default_server; + #listen [::]:443 ssl http2 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; ssl_certificate /etc/yunohost/certs/yunohost.org/crt.pem; ssl_certificate_key /etc/yunohost/certs/yunohost.org/key.pem; From 75513f4d297bf437bd0cb8b3c50330ce0732ac2e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 May 2018 05:52:19 +0200 Subject: [PATCH 0798/1066] Updating changelog --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 36168f6c5..a00fcceed 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (3.0.0~beta1.2) testing; urgency=low + + Removing http2 also from yunohost_admin.conf since there still are some + issues with wordpress ? + + -- Alexandre Aubin Tue, 08 May 2018 05:52:00 +0000 + yunohost (3.0.0~beta1.1) testing; urgency=low Fixes in the postgresql migration From a3c6cc2ca8aaa322d8667ea54edb3ab63abd4922 Mon Sep 17 00:00:00 2001 From: JimboJoe Date: Tue, 8 May 2018 18:21:52 +0200 Subject: [PATCH 0799/1066] Typo fix --- data/helpers.d/utils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 1cbc7d23c..51edf45f0 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -110,7 +110,7 @@ ynh_backup_before_upgrade () { # # default: ${src_id}.${src_format} # SOURCE_FILENAME=example.tar.gz # # (Optional) If it set as false don't extract the source. -# # (Usefull to get a debian package of a python wheel.) +# # (Useful to get a debian package or a python wheel.) # # default: true # SOURCE_EXTRACT=(true|false) # From 7de61ce8ab754426f3036ac205c0eef4db510d74 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 May 2018 19:00:55 +0200 Subject: [PATCH 0800/1066] mysql-server can be removed now, it's a meta-package that depends on mariadb --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 6d8848e86..c60b62689 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Depends: ${python:Depends}, ${misc:Depends} , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute - , mariadb-server | mysql-server, php-mysql | php-mysqlnd + , mariadb-server, php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved From 2f0db53d8c99a6d222f1c070d0b335c97ac25344 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 May 2018 19:01:25 +0200 Subject: [PATCH 0801/1066] We have mariadb-server-10.1 now --- data/hooks/conf_regen/34-mysql | 12 ++++-------- data/hooks/restore/11-conf_ynh_mysql | 12 ++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index bda1859d8..5ee91827b 100755 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -1,6 +1,7 @@ #!/bin/bash set -e +MYSQL_PKG="mariadb-server-10.1" do_pre_regen() { pending_dir=$1 @@ -31,19 +32,14 @@ do_post_regen() { "applications, and is going to reset the MySQL root password." \ "You can find this new password in /etc/yunohost/mysql." >&2 - # retrieve MySQL package provider - ynh_package_is_installed "mariadb-server-10.0" \ - && mysql_pkg="mariadb-server-10.0" \ - || mysql_pkg="mysql-server-5.5" - # set new password with debconf sudo debconf-set-selections << EOF -$mysql_pkg mysql-server/root_password password $mysql_password -$mysql_pkg mysql-server/root_password_again password $mysql_password +$MYSQL_PKG mysql-server/root_password password $mysql_password +$MYSQL_PKG mysql-server/root_password_again password $mysql_password EOF # reconfigure Debian package - sudo dpkg-reconfigure -freadline -u "$mysql_pkg" 2>&1 + sudo dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 else echo "It seems that you have already configured MySQL." \ "YunoHost needs to have a root access to MySQL to runs its" \ diff --git a/data/hooks/restore/11-conf_ynh_mysql b/data/hooks/restore/11-conf_ynh_mysql index 8b8438c0e..0aaaccd54 100644 --- a/data/hooks/restore/11-conf_ynh_mysql +++ b/data/hooks/restore/11-conf_ynh_mysql @@ -1,4 +1,5 @@ backup_dir="$1/conf/ynh/mysql" +MYSQL_PKG="mariadb-server-10.1" # ensure that mysql is running service mysql status >/dev/null 2>&1 \ @@ -23,19 +24,14 @@ sudo mysqladmin -s -u root -p"$curr_pwd" password "$new_pwd" || { "applications, and is going to reset the MySQL root password." \ "You can find this new password in /etc/yunohost/mysql." >&2 - # retrieve MySQL package provider - ynh_package_is_installed "mariadb-server-10.0" \ - && mysql_pkg="mariadb-server-10.0" \ - || mysql_pkg="mysql-server-5.5" - # set new password with debconf sudo debconf-set-selections << EOF -$mysql_pkg mysql-server/root_password password $new_pwd -$mysql_pkg mysql-server/root_password_again password $new_pwd +$MYSQL_PKG mysql-server/root_password password $new_pwd +$MYSQL_PKG mysql-server/root_password_again password $new_pwd EOF # reconfigure Debian package - sudo dpkg-reconfigure -freadline -u "$mysql_pkg" 2>&1 + sudo dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 } # store new root password From 75fc1c03c59e6ca8d36d6728f3e7d3376819aeb5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 May 2018 20:32:34 +0200 Subject: [PATCH 0802/1066] Factorize the building of the logo at the top of the script --- bin/yunoprompt | 88 +++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 54 deletions(-) mode change 100644 => 100755 bin/yunoprompt diff --git a/bin/yunoprompt b/bin/yunoprompt old mode 100644 new mode 100755 index 83b28296f..bb71e96b9 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -1,7 +1,9 @@ #!/bin/bash +# Fetch ips ip=$(hostname --all-ip-address) +# Fetch SSH fingerprints i=0 for key in /etc/ssh/ssh_host_*_key.pub ; do output=$(ssh-keygen -l -f $key) @@ -9,7 +11,11 @@ for key in /etc/ssh/ssh_host_*_key.pub ; do i=$(($i + 1)) done -cat > /etc/issue.net << EOF +# +# Build the logo +# + +LOGO=$(cat << 'EOF' '. ' '' -d. /M+ h- .shh/ // /NMy- hMdosso 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ @@ -19,7 +25,18 @@ cat > /etc/issue.net << EOF 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - hy /yy: :- -. -Nh ' . - +EOF +) + +# ' Put a quote in comment to make vim happy about syntax highlighting :s + +# +# Build the actual message +# + +LOGO_AND_FINGERPRINTS=$(cat << EOF +"$LOGO" + IP: ${ip} SSH fingerprints: ${fingerprint[0]} @@ -28,38 +45,22 @@ ${fingerprint[2]} ${fingerprint[3]} ${fingerprint[4]} EOF +) + + +echo "$LOGO_AND_FINGERPRINTS" > /etc/issue.net + if [[ ! -f /etc/yunohost/installed ]] then - if [[ ! -f /etc/yunohost/from_script ]] - then -sleep 5 -chvt 2 -cat << EOF - '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso - 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ - sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' - .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. - mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: - 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' - . - -IP: ${ip} -SSH fingerprints*: -${fingerprint[0]} -${fingerprint[1]} -${fingerprint[2]} -${fingerprint[3]} -${fingerprint[4]} -EOF - -echo -e "\e[m Post-installation \e[0m" -cat << EOF -Congratulations! YunoHost has been successfully installed. -Two more steps are required to activate the services of your server. -EOF -read -p "Proceed to post-installation? (y/n) " -n 1 + if [[ ! -f /etc/yunohost/from_script ]] + then + sleep 5 + chvt 2 + echo "$LOGO_AND_FINGERPRINTS" + echo -e "\e[m Post-installation \e[0m" + echo "Congratulations! YunoHost has been successfully installed. + Two more steps are required to activate the services of your server." + read -p "Proceed to post-installation? (y/n) " -n 1 RESULT=1 while [ $RESULT -gt 0 ]; do if [[ $REPLY =~ ^[Nn]$ ]]; then @@ -76,27 +77,6 @@ read -p "Proceed to post-installation? (y/n) " -n 1 done fi else # YunoHost is already post-installed - -cat > /etc/issue << EOF - '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso - 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ - sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' - .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. - mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: - 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' - . - -IP: ${ip} -SSH fingerprints: -${fingerprint[0]} -${fingerprint[1]} -${fingerprint[2]} -${fingerprint[3]} -${fingerprint[4]} -EOF - - + echo "$LOGO_AND_FINGERPRINTS" > /etc/issue fi From e8f97e7b3ef2dc0a107d24f4dbff9f27fea4482f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 May 2018 00:25:11 +0200 Subject: [PATCH 0803/1066] Also patch jessie-updates in sources.list --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 5f65dbb89..5a5240507 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -146,12 +146,13 @@ class MyMigration(Migration): # This : # - replace single 'jessie' occurence by 'stretch' # - comments lines containing "backports" - # - replace 'jessie/updates' by 'strech/updates' + # - replace 'jessie/updates' by 'strech/updates' (or same with a -) # - switch yunohost's repo to forge for f in sources_list: command = "sed -i -e 's@ jessie @ stretch @g' " \ "-e '/backports/ s@^#*@#@' " \ "-e 's@ jessie/updates @ stretch/updates @g' " \ + "-e 's@ jessie-updates @ stretch-updates @g' " \ "-e 's@repo.yunohost@forge.yunohost@g' " \ "{}".format(f) os.system(command) From 4f9901c8617922c623b544d85080d14fd89ddac5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 May 2018 01:32:26 +0200 Subject: [PATCH 0804/1066] Use lsb_release instead of platform to check debian version during migration --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 5a5240507..cfecad6b6 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -5,7 +5,6 @@ import base64 import time import json import errno -import platform from shutil import copy2 from moulinette import m18n, msettings @@ -80,7 +79,11 @@ class MyMigration(Migration): self.upgrade_yunohost_packages() def debian_major_version(self): - return int(platform.dist()[1][0]) + # We rely on lsb_release instead of the python module "platform", + # because "platform" relies on uname, which on some weird setups does + # not behave correctly (still says running Jessie when lsb_release says + # Stretch...) + return int(check_output("lsb_release -r").split("\t")[1][0]) def yunohost_major_version(self): return int(get_installed_version("yunohost").split('.')[0]) From a00e8a02a5e8bab32da48535dc1063530f0aa0af Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 May 2018 02:41:56 +0200 Subject: [PATCH 0805/1066] Bit cooler message at the end of the migration --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index cfecad6b6..ab4974e2e 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -213,8 +213,8 @@ class MyMigration(Migration): wait_until_end_of_yunohost_command = "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) - command = "({} && {}; echo 'Done!') &".format(wait_until_end_of_yunohost_command, - upgrade_command) + command = "({} && {}; echo 'Migration complete!') &".format(wait_until_end_of_yunohost_command, + upgrade_command) logger.debug("Running command :\n{}".format(command)) From 9570832f42c62c3afabb1631511c12ab1306320c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 May 2018 00:45:26 +0000 Subject: [PATCH 0806/1066] Update changelog for 2.7.13.1 release --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index a935b1b20..1fa1544ad 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (2.7.13.1) testing; urgency=low + + * [fix] Misc fixes on stretch migration following feedback + + -- Alexandre Aubin Wed, 09 May 2018 00:44:50 +0000 + yunohost (2.7.13) testing; urgency=low * [enh] Add 'manual migration' mechanism to the migration framework (#429) From 0f8cef9cf843d30fdfcda93642a66b4d35fdcbb1 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Thu, 10 May 2018 00:05:46 +0200 Subject: [PATCH 0807/1066] Helper - improve ynh_add_nginx_config Add 2 new features : - Permit to replace some others variables in the template - Manage automatically the redirection (alias_transversal issue fix) --- data/helpers.d/backend | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 28c5b8e91..a912cd337 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -117,7 +117,10 @@ ynh_remove_systemd_config () { # Create a dedicated nginx config # -# usage: ynh_add_nginx_config +# usage: ynh_add_nginx_config "list of others variables to replace" +# +# | arg: list of others variables to replace separeted by a space +# | for example : 'path_2 port_2 ...' # # This will use a template in ../conf/nginx.conf # __PATH__ by $path_url @@ -126,8 +129,13 @@ ynh_remove_systemd_config () { # __NAME__ by $app # __FINALPATH__ by $final_path # +# And dynamic variables (from the last example) : +# __PATH_2__ by $path_2 +# __PORT_2__ by $port_2 +# ynh_add_nginx_config () { - finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + local others_var=${1:-} ynh_backup_if_checksum_is_different "$finalnginxconf" sudo cp ../conf/nginx.conf "$finalnginxconf" @@ -151,6 +159,18 @@ ynh_add_nginx_config () { if test -n "${final_path:-}"; then ynh_replace_string "__FINALPATH__" "$final_path" "$finalnginxconf" fi + + # Replace all other variable given as arguments + for v in $others_var + do + ynh_replace_string "__${v^^}__" "${!v}" "$finalnginxconf" + done + + if [ "${path_url:-}" != "/" ] + then + ynh_replace_string "^#sub_path_only" "" "$finalnginxconf" + fi + ynh_store_file_checksum "$finalnginxconf" sudo systemctl reload nginx From 844b35589e31deec76cd997567e43a4262d28c80 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 01:14:53 +0200 Subject: [PATCH 0808/1066] Add ynh_render_template helper to render templates with jinja2 --- data/helpers.d/utils | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index cc057eb0b..05f9a4703 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -234,3 +234,20 @@ ynh_local_curl () { # Curl the URL curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" } + +# Render templates with Jinja2 +# +# Attention : Variables should be exported before calling this helper to be +# accessible inside templates. +# +# usage: ynh_render_template some_template output_path +# | arg: some_template - Template file to be rendered +# | arg: output_path - The path where the output will be redirected to +ynh_render_template() { + local template_path=$1 + local output_path=$2 + # Taken from https://stackoverflow.com/a/35009576 + python2.7 -c 'import os, sys, jinja2; sys.stdout.write( + jinja2.Template(sys.stdin.read() + ).render(os.environ));' < $template_path > $output_path +} From 890eb7a885ef7b2df7611dee3fe1cdd1b75b2b9b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 01:29:03 +0200 Subject: [PATCH 0809/1066] Add dependency to python-jinja2 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index d1505994a..d069dc678 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) , python-psutil, python-requests, python-dnspython, python-openssl - , python-apt, python-miniupnpc + , python-apt, python-miniupnpc, python-jinja2 , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute From 77fcb6ad1285f61424190efd5ab12c5de3bd38eb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 03:26:52 +0200 Subject: [PATCH 0810/1066] Squashed 'src/yunohost/vendor/spectre-meltdown-checker/' changes from 7f92717..edebe4d edebe4d bump to v0.37 83ea78f fix: arm: also detect variant 1 mitigation when using native objdump 602b68d fix(spectrev2): explain that retpoline is possible for Skylake+ if there is RSB filling, even if IBRS is still better 97bccaa feat: rephrase IBPB warning when only retpoline is enabled in non-paranoid mode 68e619b feat: show RSB filling capability for non-Skylake in verbose mode a6f4475 feat: make IBRS_FW blue instead of green 223f502 feat: add --paranoid to choose whether we require IBPB c0108b9 fix(spectre2): don't explain how to fix when NOT VULNERABLE a301613 feat: make RSB filling support mandatory for Skylake+ CPUs 59d85b3 feat: detect RSB filling capability in the kernel baaefb0 fix: remove shellcheck warnings d452aca fix: invalid bash syntax when ibpb_enabled or ibrs_enabled are empty 10b8d94 feat: detect latest Red Hat kernels' RO ibpb_enabled knob 8606e60 refactor: no longer display the retoline-aware compiler test when we can't tell for sure 6a48251 fix: regression in 51aeae25, when retpoline & ibpb are enabled f4bf5e9 fix: typos 60eac1a feat: also do PTI performance check with (inv)pcid for BSD b3cc06a fix regression introduced by 82c25dc 5553576 feat(amd/zen): re-introduce IBRS for AMD except ZEN family e16ad80 feat(ibpb=2): add detection of SMT before concluding the system is not vulnerable 29c294e feat(bsd): explain how to mitigate variant2 5971401 refactor: IBRS_ALL & RDCL_NO are Intel-only 51e8261 refactor: separate hw checks for Intel & AMD 2a4bfad refactor: add is_amd and is_intel funcs 7e52cea feat(spectre2): refined how status of this vuln is decided and more precise explanations on how to fix 417d7aa Fix trailing whitespace and mixed indent styles; 67bf761 Fix some user facing typos with codespell -w -q3 . 0eabd26 refactor: decrease default verbosity for some tests b77fb0f fix: don't override ibrs/ibpb results with later tests 89c2e0f fix(amd): show cpuinfo and ucode details b88f32e feat: print raw cpuid, and fetch ucode version under BSD 7a4ebe8 refactor: rewrite read_cpuid to get more common code parts between BSD and Linux 0919f5c feat: add explanations of what to do when a vulnerability is not mitigated de02dad feat: rework Spectre V2 mitigations detection w/ latest vanilla & Red Hat 7 kernels 07484d0 add dump of variables at end of script in debug mode a8b557b fix(cpu): skip CPU checks if asked to (--no-hw) or if inspecting a kernel of another architecture 619b274 fix(sysfs): only check for sysfs for spectre2 when in live mode 94857c9 update readme 056ed00 feat(arm): detect spectre variant 1 mitigation aef99d2 fix(pti): when PTI activation is unknown, don't say we're vulnerable e2d7ed2 feat(arm): support for variant2 and meltdown mitigation detection eeaeff8 set version to v0.36+ for master branch between releases f5269a3 feat(bsd): add retpoline detection for BSD f3883a3 fix(xen): adjust message for DomUs w/ sysfs b6fd69a release: v0.36 7adb766 enh: change colors and use red only to report vulnerability c7892e3 update README.md aa74315 feat: speed up kernel version detection 0b8a09e fix: mis adjustments for BSD compat b42d8f2 fix(write_msr): use /dev/zero instead of manually echoing zeroes f191ec7 feat: add --hw-only to only show CPU microcode/cpuid/msr details 28da7a0 misc: message clarifications ece25b9 feat: implement support for NetBSD/FreeBSD/DragonFlyBSD 889172d feat: add special extract_vmlinux mode for old RHEL kernels 37ce032 fix: bypass MSR/CPUID checks for non-x86 CPUs 701cf88 feat: more robust validation of extracted kernel image 6a94c3f feat(extract_vmlinux): look for ELF magic in decompressed blob and cut at found offset 2d99381 feat: add --prefix-arch for cross-arch kernel inspection 4961f83 fix(ucode): fix blacklist detection for some ucode versions ecdc448 Check MSR in each CPU/Thread (#136) 12ea49f fix(kvm): properly detect PVHVM mode (fixes #163) 053f161 fix(doc): use https:// URLs in the script comment header bda18d0 fix: pine64: re-add vmlinuz location and some error checks 2551295 doc: use https URLs d5832dc feat: add ELF magic detection on kernel image blob for some arm64 systems d2f4674 feat: enhance kernel image version detection for some old kernels 2f6a655 Produce output for consumption by prometheus-node-exporter 30842dd release: bump to v0.35 b4ac5fc feat(variant2): better explanation when kernel supports IBRS but CPU does not fef380d feat(readme): add quick run section 55a6fd3 feat(variant1): better detection for Red Hat/Ubuntu patch 35c8a63 Remove the color in the title 5f914e5 fix(xen): declare Xen's PTI patch as a valid mitigation for variant3 66dce2c fix(ucode): update blacklisted ucodes list from latest Intel info 155cac2 Teach checker how to find kernels installed by systemd kernel-install 22cae60 fix(retpoline): remove the "retpoline enabled" test eb75e51 fix(ucode): update list of blacklisted ucodes from 2018-02-08 Intel document 253e180 Update spectre-meltdown-checker.sh 5d6102a enh: show kernel version in offline mode a2dfca6 feat: detect disrepancy between found kernel image and running kernel 36bd80d enh: speedup by not decompressing kernel on --sysfs-only 1834dd6 feat: add skylake era cpu detection routine 3d765bc enh: lazy loading of cpu informations 07afd95 feat: better cleanup routine on exit & interrupt b7a1012 fix: ARM CPU display name & detection 6346a0d fix: --no-color workaround for android's sed 8106f91 release: bump to v0.34 b1fdf88 enh: display ucode info even when not blacklisted 4d29607 cleanup: shellcheck pass 0267659 cleanup: remove superseded atom detection code 247b176 feat: detect known speculative-execution free CPUs bcae882 refacto: create a dedicated func to read cpuid bits 71e7109 refacto: move cpu discovery bits to a dedicated function aa18b51 fix(variant1): smarter lfence check b738ac4 fix: regression introduced by previous commit 799ce3e update blacklisted ucode list from kernel source f1e18c1 doc(disclaimer): Spectre affects all software e05ec5c feat(variant1): detect vanilla mitigation 6e544d6 fix(cpu): Pentium Exxxx are vulnerable to Meltdown 90a6596 adjust: show how to enable IBRS/IBPB in -v only 9b53635 refacto: fix shellcheck warnings for better compat 7404929 Fix printing of microcode to use cpuinfo values bf46fd5 update: new screenshots for README.md 0798bd4 fix: report arch_capabilities as NO when no MSR 42094c4 release: v0.33 03d2dfe feat: add blacklisted Intel ucode detection 9f00ffa fix: fallback to UNKNOWN when we get -EACCES 7f0d80b xen: detect if the host is a Xen Dom0 or PV DomU (fixes #83) d1c1f0f fix(batch): fix regression introduced by acf12a6 acf12a6 feat(cpu) add STIBP, RDCL_NO, IBRS_ALL checks b45e40b feat(stibp): add STIBP cpuid feature check 3c1d452 fix(cpuid): fix off-by-one SPEC_CTRL bit check 53b9eda fix: don't make IBPB mandatory when it's not there 3b0ec99 fix(cosmetic): tiny msg fixes d55bafd fix(cpu): trust is_cpu_vulnerable even w/ debugfs 147462c fix(variant3): do our checks even if sysfs is here ddc7197 fix(retpoline): retpoline-compiler detection e7aa3b9 feat(retpoline): check if retpoline is enabled ff5c92f feat(sysfs): print details even with sysfs 443d9a2 feat(ibpb): now also check for IBPB on variant 2 3e454f1 fix(offline): report unknown when too few info c8a25c5 feat: detect invalid kconfig files 4038134 fix(dmesg): detect when dmesg is truncated 0aa5857 fix(cpu): Pentium Exxxx series are not vulnerable b3b7f63 fix(display): use text-mode compatible colors 263ef65 bump to v0.32 a1bd233 revert to a simpler check_vmlinux() de6590c cache is_cpu_vulnerable result for performance 56d4f82 is_cpu_vulnerable: implement check for multi-arm systems 7fa2d63 check_vmlinux: when readelf doesn't work, try harder with another way 3be5e90 be smarter to find a usable echo command 995620a add pine64 vmlinuz location 193e0d8 arm: cosmetic fix for name and handle aarch64 72ef94a ARM: display a friendly name instead of empty string ccc0453 search in /lib/modules/$(uname -r) for vmlinuz, config, System.map 14ca49a Atom N270: implement another variation db357b8 CoreOS: remove ephemeral install of a non-used package 42a57dd add kern.log as another backend of dmesg output 5ab95f3 fix(atom): don't use a pcre regex, only an extended one 5b6e399 fix(atom): properly detect Nxxx Atom series 556951d Add Support for Slackware. 7a88aec Implement CoreOS compatibility mode (#84) bd18323 bump to v0.31 to reflect changes b89d67d meltdown: detecting Xen PV, reporting as not vulnerable 704e540 is_cpu_vulnerable: add check for old Atoms d960931 verbose: add PCID check for performance impact of PTI dcc4488 Merge pull request #80 from speed47/cpuid_spec_ctrl 32e3fe6 bump to v0.30 to reflect changes f488947 Merge pull request #79 from andir/add-nixos 71213c1 ibrs: check for spec_ctrl_ibrs in cpuinfo 2964c4a add support for NixOS kernel 749f432 also check for spec_ctrl flag in cpuinfo a422b53 also check for cpuinfo flag c483a2c check spec_ctrl support using cpuid dead005 fix: proper detail msg in vuln status 8ed7d46 Merge pull request #77 from speed47/exitcode e5e4851 proper return codes regardless of the batch mode git-subtree-dir: src/yunohost/vendor/spectre-meltdown-checker git-subtree-split: edebe4dcd47cb8457d778406ed9de7670d6d8eb5 --- README.md | 55 +- spectre-meltdown-checker.sh | 2791 +++++++++++++++++++++++++++++------ 2 files changed, 2381 insertions(+), 465 deletions(-) diff --git a/README.md b/README.md index 518b3ec9b..4a9c71828 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,57 @@ Spectre & Meltdown Checker ========================== -A simple shell script to tell if your Linux installation is vulnerable against the 3 "speculative execution" CVEs that were made public early 2018. +A shell script to tell if your system is vulnerable against the 3 "speculative execution" CVEs that were made public early 2018. -Without options, it'll inspect your currently running kernel. -You can also specify a kernel image on the command line, if you'd like to inspect a kernel you're not running. +Supported operating systems: +- Linux (all versions, flavors and distros) +- BSD (FreeBSD, NetBSD, DragonFlyBSD) -The script will do its best to detect mitigations, including backported non-vanilla patches, regardless of the advertised kernel version number. +Supported architectures: +- x86 (32 bits) +- amd64/x86_64 (64 bits) +- ARM and ARM64 +- other architectures will work, but mitigations (if they exist) might not always be detected + +For Linux systems, the script will detect mitigations, including backported non-vanilla patches, regardless of the advertised kernel version number and the distribution (such as Debian, Ubuntu, CentOS, RHEL, Fedora, openSUSE, Arch, ...), it also works if you've compiled your own kernel. + +For BSD systems, the detection will work as long as the BSD you're using supports `cpuctl` and `linprocfs` (this is not the case of OpenBSD for example). + +## Easy way to run the script + +- Get the latest version of the script using `curl` *or* `wget` + +```bash +curl -L https://meltdown.ovh -o spectre-meltdown-checker.sh +wget https://meltdown.ovh -O spectre-meltdown-checker.sh +``` + +- Inspect the script. You never blindly run scripts you downloaded from the Internet, do you? + +```bash +vim spectre-meltdown-checker.sh +``` + +- When you're ready, run the script as root + +```bash +chmod +x spectre-meltdown-checker.sh +sudo ./spectre-meltdown-checker.sh +``` ## Example of script output -![checker](https://framapic.org/6O4v4AAwMenv/M6J4CFWwsB3z.png) +- Intel Haswell CPU running under Ubuntu 16.04 LTS + +![haswell](https://framapic.org/1kWmNwE6ll0p/ayTRX9JRlHJ7.png) + +- AMD Ryzen running under OpenSUSE Tumbleweed + +![ryzen](https://framapic.org/TkWbuh421YQR/6MAGUP3lL6Ne.png) + +- Batch mode (JSON flavor) + +![batch](https://framapic.org/HEcWFPrLewbs/om1LdufspWTJ.png) ## Quick summary of the CVEs @@ -38,8 +79,10 @@ The script will do its best to detect mitigations, including backported non-vani This tool does its best to determine whether your system is immune (or has proper mitigations in place) for the collectively named "speculative execution" vulnerabilities. It doesn't attempt to run any kind of exploit, and can't guarantee that your system is secure, but rather helps you verifying whether your system has the known correct mitigations in place. However, some mitigations could also exist in your kernel that this script doesn't know (yet) how to detect, or it might falsely detect mitigations that in the end don't work as expected (for example, on backported or modified kernels). -Your system exposure also depends on your CPU. As of now, AMD and ARM processors are marked as immune to some or all of these vulnerabilities (except some specific ARM models). All Intel processors manufactured since circa 1995 are thought to be vulnerable. Whatever processor one uses, one might seek more information from the manufacturer of that processor and/or of the device in which it runs. +Your system exposure also depends on your CPU. As of now, AMD and ARM processors are marked as immune to some or all of these vulnerabilities (except some specific ARM models). All Intel processors manufactured since circa 1995 are thought to be vulnerable, except some specific/old models, such as some early Atoms. Whatever processor one uses, one might seek more information from the manufacturer of that processor and/or of the device in which it runs. The nature of the discovered vulnerabilities being quite new, the landscape of vulnerable processors can be expected to change over time, which is why this script makes the assumption that all CPUs are vulnerable, except if the manufacturer explicitly stated otherwise in a verifiable public announcement. +Please also note that for Spectre vulnerabilities, all software can possibly be exploited, this tool only verifies that the kernel (which is the core of the system) you're using has the proper protections in place. Verifying all the other software is out of the scope of this tool. As a general measure, ensure you always have the most up to date stable versions of all the software you use, especially for those who are exposed to the world, such as network daemons and browsers. + This tool has been released in the hope that it'll be useful, but don't use it to jump to conclusions about your security. diff --git a/spectre-meltdown-checker.sh b/spectre-meltdown-checker.sh index f71deb5bf..0f3c10575 100755 --- a/spectre-meltdown-checker.sh +++ b/spectre-meltdown-checker.sh @@ -4,19 +4,35 @@ # Check for the latest version at: # https://github.com/speed47/spectre-meltdown-checker # git clone https://github.com/speed47/spectre-meltdown-checker.git -# or wget https://raw.githubusercontent.com/speed47/spectre-meltdown-checker/master/spectre-meltdown-checker.sh +# or wget https://meltdown.ovh -O spectre-meltdown-checker.sh +# or curl -L https://meltdown.ovh -o spectre-meltdown-checker.sh # # Stephane Lesimple # -VERSION=0.29 +VERSION='0.37' + +trap 'exit_cleanup' EXIT +trap '_warn "interrupted, cleaning up..."; exit_cleanup; exit 1' INT +exit_cleanup() +{ + # cleanup the temp decompressed config & kernel image + [ -n "$dumped_config" ] && [ -f "$dumped_config" ] && rm -f "$dumped_config" + [ -n "$kerneltmp" ] && [ -f "$kerneltmp" ] && rm -f "$kerneltmp" + [ -n "$kerneltmp2" ] && [ -f "$kerneltmp2" ] && rm -f "$kerneltmp2" + [ "$mounted_debugfs" = 1 ] && umount /sys/kernel/debug 2>/dev/null + [ "$mounted_procfs" = 1 ] && umount "$procfs" 2>/dev/null + [ "$insmod_cpuid" = 1 ] && rmmod cpuid 2>/dev/null + [ "$insmod_msr" = 1 ] && rmmod msr 2>/dev/null + [ "$kldload_cpuctl" = 1 ] && kldunload cpuctl 2>/dev/null +} -# Script configuration show_usage() { + # shellcheck disable=SC2086 cat <] [--config ] [--map ] + Live mode: $(basename $0) [options] [--live] + Offline mode: $(basename $0) [options] [--kernel ] [--config ] [--map ] Modes: Two modes are available. @@ -25,22 +41,36 @@ show_usage() To run under this mode, just start the script without any option (you can also use --live explicitly) Second mode is the "offline" mode, where you can inspect a non-running kernel. - You'll need to specify the location of the vmlinux file, and if possible, the corresponding config and System.map files: + You'll need to specify the location of the kernel file, config and System.map files: - --kernel vmlinux_file Specify a (possibly compressed) vmlinux file - --config kernel_config Specify a kernel config file - --map kernel_map_file Specify a kernel System.map file + --kernel kernel_file specify a (possibly compressed) Linux or BSD kernel file + --config kernel_config specify a kernel config file (Linux only) + --map kernel_map_file specify a kernel System.map file (Linux only) Options: - --no-color Don't use color codes - --verbose, -v Increase verbosity level - --no-sysfs Don't use the /sys interface even if present - --batch text Produce machine readable output, this is the default if --batch is specified alone - --batch json Produce JSON output formatted for Puppet, Ansible, Chef... - --batch nrpe Produce machine readable output formatted for NRPE - --variant [1,2,3] Specify which variant you'd like to check, by default all variants are checked - Can be specified multiple times (e.g. --variant 2 --variant 3) + --no-color don't use color codes + --verbose, -v increase verbosity level, possibly several times + --no-explain don't produce a human-readable explanation of actions to take to mitigate a vulnerability + --paranoid require IBPB to deem Variant 2 as mitigated + --no-sysfs don't use the /sys interface even if present [Linux] + --sysfs-only only use the /sys interface, don't run our own checks [Linux] + --coreos special mode for CoreOS (use an ephemeral toolbox to inspect kernel) [Linux] + + --arch-prefix PREFIX specify a prefix for cross-inspecting a kernel of a different arch, for example "aarch64-linux-gnu-", + so that invoked tools will be prefixed with this (i.e. aarch64-linux-gnu-objdump) + --batch text produce machine readable output, this is the default if --batch is specified alone + --batch json produce JSON output formatted for Puppet, Ansible, Chef... + --batch nrpe produce machine readable output formatted for NRPE + --batch prometheus produce output for consumption by prometheus-node-exporter + + --variant [1,2,3] specify which variant you'd like to check, by default all variants are checked, + can be specified multiple times (e.g. --variant 2 --variant 3) + --hw-only only check for CPU information, don't check for any variant + --no-hw skip CPU information and checks, if you're inspecting a kernel not to be run on this host + + Return codes: + 0 (not vulnerable), 2 (vulnerable), 3 (unknown), 255 (error) IMPORTANT: A false sense of security is worse than no security at all. @@ -61,19 +91,26 @@ However, some mitigations could also exist in your kernel that this script doesn falsely detect mitigations that in the end don't work as expected (for example, on backported or modified kernels). Your system exposure also depends on your CPU. As of now, AMD and ARM processors are marked as immune to some or all of these -vulnerabilities (except some specific ARM models). All Intel processors manufactured since circa 1995 are thought to be vulnerable. -Whatever processor one uses, one might seek more information from the manufacturer of that processor and/or of the device -in which it runs. +vulnerabilities (except some specific ARM models). All Intel processors manufactured since circa 1995 are thought to be vulnerable, +except some specific/old models, such as some early Atoms. Whatever processor one uses, one might seek more information +from the manufacturer of that processor and/or of the device in which it runs. The nature of the discovered vulnerabilities being quite new, the landscape of vulnerable processors can be expected to change over time, which is why this script makes the assumption that all CPUs are vulnerable, except if the manufacturer explicitly stated otherwise in a verifiable public announcement. +Please also note that for Spectre vulnerabilities, all software can possibly be exploited, this tool only verifies that the +kernel (which is the core of the system) you're using has the proper protections in place. Verifying all the other software +is out of the scope of this tool. As a general measure, ensure you always have the most up to date stable versions of all +the software you use, especially for those who are exposed to the world, such as network daemons and browsers. + This tool has been released in the hope that it'll be useful, but don't use it to jump to conclusions about your security. EOF } +os=$(uname -s) + # parse options opt_kernel='' opt_config='' @@ -89,63 +126,130 @@ opt_variant2=0 opt_variant3=0 opt_allvariants=1 opt_no_sysfs=0 +opt_sysfs_only=0 +opt_coreos=0 +opt_arch_prefix='' +opt_hw_only=0 +opt_no_hw=0 +opt_no_explain=0 +opt_paranoid=0 -nrpe_critical=0 -nrpe_unknown=0 +global_critical=0 +global_unknown=0 nrpe_vuln="" +# find a sane command to print colored messages, we prefer `printf` over `echo` +# because `printf` behavior is more standard across Linux/BSD +# we'll try to avoid using shell builtins that might not take options +echo_cmd_type=echo +if which printf >/dev/null 2>&1; then + echo_cmd=$(which printf) + echo_cmd_type=printf +elif which echo >/dev/null 2>&1; then + echo_cmd=$(which echo) +else + # which command is broken? + [ -x /bin/echo ] && echo_cmd=/bin/echo + # for Android + [ -x /system/bin/echo ] && echo_cmd=/system/bin/echo +fi +# still empty ? fallback to builtin +[ -z "$echo_cmd" ] && echo_cmd=echo __echo() { opt="$1" shift - _msg="$@" + _msg="$*" + if [ "$opt_no_color" = 1 ] ; then # strip ANSI color codes - _msg=$(/bin/echo -e "$_msg" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g") + # some sed versions (i.e. toybox) can't seem to handle + # \033 aka \x1B correctly, so do it for them. + if [ "$echo_cmd_type" = printf ]; then + _interpret_chars='' + else + _interpret_chars='-e' + fi + _ctrlchar=$($echo_cmd $_interpret_chars "\033") + _msg=$($echo_cmd $_interpret_chars "$_msg" | sed -r "s/$_ctrlchar\[([0-9][0-9]?(;[0-9][0-9]?)?)?m//g") + fi + if [ "$echo_cmd_type" = printf ]; then + if [ "$opt" = "-n" ]; then + $echo_cmd "$_msg" + else + $echo_cmd "$_msg\n" + fi + else + # shellcheck disable=SC2086 + $echo_cmd $opt -e "$_msg" fi - # explicitly call /bin/echo to avoid shell builtins that might not take options - /bin/echo $opt -e "$_msg" } _echo() { - if [ $opt_verbose -ge $1 ]; then + if [ "$opt_verbose" -ge "$1" ]; then shift - __echo '' "$@" + __echo '' "$*" fi } _echo_nol() { - if [ $opt_verbose -ge $1 ]; then + if [ "$opt_verbose" -ge "$1" ]; then shift - __echo -n "$@" + __echo -n "$*" fi } _warn() { - _echo 0 "\033[31m${@}\033[0m" >&2 + _echo 0 "\033[31m$*\033[0m" >&2 } _info() { - _echo 1 "$@" + _echo 1 "$*" } _info_nol() { - _echo_nol 1 "$@" + _echo_nol 1 "$*" } _verbose() { - _echo 2 "$@" + _echo 2 "$*" +} + +_verbose_nol() +{ + _echo_nol 2 "$*" } _debug() { - _echo 3 "\033[34m(debug) $@\033[0m" + _echo 3 "\033[34m(debug) $*\033[0m" +} + +explain() +{ + if [ "$opt_no_explain" != 1 ] ; then + _info '' + _info "> \033[41m\033[30mHow to fix:\033[0m $*" + fi +} + +is_cpu_vulnerable_cached=0 +_is_cpu_vulnerable_cached() +{ + # shellcheck disable=SC2086 + [ "$1" = 1 ] && return $variant1 + # shellcheck disable=SC2086 + [ "$1" = 2 ] && return $variant2 + # shellcheck disable=SC2086 + [ "$1" = 3 ] && return $variant3 + echo "$0: error: invalid variant '$1' passed to is_cpu_vulnerable()" >&2 + exit 255 } is_cpu_vulnerable() @@ -155,52 +259,140 @@ is_cpu_vulnerable() # (note that in shell, a return of 0 is success) # by default, everything is vulnerable, we work in a "whitelist" logic here. # usage: is_cpu_vulnerable 2 && do something if vulnerable - variant1=0 - variant2=0 - variant3=0 - - if grep -q AMD /proc/cpuinfo; then - # AMD revised their statement about variant2 => vulnerable - # https://www.amd.com/en/corporate/speculative-execution - variant3=1 - elif grep -qi 'CPU implementer\s*:\s*0x41' /proc/cpuinfo; then - # ARM - # reference: https://developer.arm.com/support/security-update - cpupart=$(awk '/CPU part/ {print $4;exit}' /proc/cpuinfo) - cpuarch=$(awk '/CPU architecture/ {print $3;exit}' /proc/cpuinfo) - if [ -n "$cpupart" -a -n "$cpuarch" ]; then - # Cortex-R7 and Cortex-R8 are real-time and only used in medical devices or such - # I can't find their CPU part number, but it's probably not that useful anyway - # model R7 R8 A9 A15 A17 A57 A72 A73 A75 - # part ? ? 0xc09 0xc0f 0xc0e 0xd07 0xd08 0xd09 0xd0a - # arch 7? 7? 7 7 7 8 8 8 8 - if [ "$cpuarch" = 7 ] && echo "$cpupart" | grep -Eq '^0x(c09|c0f|c0e)$'; then - # armv7 vulnerable chips - : - elif [ "$cpuarch" = 8 ] && echo "$cpupart" | grep -Eq '^0x(d07|d08|d09|d0a)$'; then - # armv8 vulnerable chips - : - else - variant1=1 - variant2=1 - fi - # for variant3, only A75 is vulnerable - if ! [ "$cpuarch" = 8 -a "$cpupart" = 0xd0a ]; then - variant3=1 - fi - fi + if [ "$is_cpu_vulnerable_cached" = 1 ]; then + _is_cpu_vulnerable_cached "$1" + return $? fi - [ "$1" = 1 ] && return $variant1 - [ "$1" = 2 ] && return $variant2 - [ "$1" = 3 ] && return $variant3 - echo "$0: error: invalid variant '$1' passed to is_cpu_vulnerable()" >&2 - exit 1 + variant1='' + variant2='' + variant3='' + + if is_cpu_specex_free; then + variant1=immune + variant2=immune + variant3=immune + elif is_intel; then + # Intel + # https://github.com/crozone/SpectrePoC/issues/1 ^F E5200 => spectre 2 not vulnerable + # https://github.com/paboldin/meltdown-exploit/issues/19 ^F E5200 => meltdown vulnerable + # model name : Pentium(R) Dual-Core CPU E5200 @ 2.50GHz + if grep -qE '^model name.+ Pentium\(R\) Dual-Core[[:space:]]+CPU[[:space:]]+E[0-9]{4}K? ' "$procfs/cpuinfo"; then + variant1=vuln + [ -z "$variant2" ] && variant2=immune + variant3=vuln + fi + if [ "$capabilities_rdcl_no" = 1 ]; then + # capability bit for future Intel processor that will explicitly state + # that they're not vulnerable to Meltdown + # this var is set in check_cpu() + variant3=immune + _debug "is_cpu_vulnerable: RDCL_NO is set so not vuln to meltdown" + fi + elif is_amd; then + # AMD revised their statement about variant2 => vulnerable + # https://www.amd.com/en/corporate/speculative-execution + variant1=vuln + variant2=vuln + [ -z "$variant3" ] && variant3=immune + elif [ "$cpu_vendor" = ARM ]; then + # ARM + # reference: https://developer.arm.com/support/security-update + # some devices (phones or other) have several ARMs and as such different part numbers, + # an example is "bigLITTLE". we shouldn't rely on the first CPU only, so we check the whole list + i=0 + for cpupart in $cpu_part_list + do + i=$(( i + 1 )) + # do NOT quote $cpu_arch_list below + # shellcheck disable=SC2086 + cpuarch=$(echo $cpu_arch_list | awk '{ print $'$i' }') + _debug "checking cpu$i: <$cpupart> <$cpuarch>" + # some kernels report AArch64 instead of 8 + [ "$cpuarch" = "AArch64" ] && cpuarch=8 + if [ -n "$cpupart" ] && [ -n "$cpuarch" ]; then + # Cortex-R7 and Cortex-R8 are real-time and only used in medical devices or such + # I can't find their CPU part number, but it's probably not that useful anyway + # model R7 R8 A9 A15 A17 A57 A72 A73 A75 + # part ? ? 0xc09 0xc0f 0xc0e 0xd07 0xd08 0xd09 0xd0a + # arch 7? 7? 7 7 7 8 8 8 8 + # + # variant 1 & variant 2 + if [ "$cpuarch" = 7 ] && echo "$cpupart" | grep -Eq '^0x(c09|c0f|c0e)$'; then + # armv7 vulnerable chips + _debug "checking cpu$i: this armv7 vulnerable to spectre 1 & 2" + variant1=vuln + variant2=vuln + elif [ "$cpuarch" = 8 ] && echo "$cpupart" | grep -Eq '^0x(d07|d08|d09|d0a)$'; then + # armv8 vulnerable chips + _debug "checking cpu$i: this armv8 vulnerable to spectre 1 & 2" + variant1=vuln + variant2=vuln + else + _debug "checking cpu$i: this arm non vulnerable to 1 & 2" + # others are not vulnerable + [ -z "$variant1" ] && variant1=immune + [ -z "$variant2" ] && variant2=immune + fi + + # for variant3, only A75 is vulnerable + if [ "$cpuarch" = 8 ] && [ "$cpupart" = 0xd0a ]; then + _debug "checking cpu$i: arm A75 vulnerable to meltdown" + variant3=vuln + else + _debug "checking cpu$i: this arm non vulnerable to meltdown" + [ -z "$variant3" ] && variant3=immune + fi + fi + _debug "is_cpu_vulnerable: for cpu$i and so far, we have <$variant1> <$variant2> <$variant3>" + done + fi + _debug "is_cpu_vulnerable: temp results are <$variant1> <$variant2> <$variant3>" + # if at least one of the cpu is vulnerable, then the system is vulnerable + [ "$variant1" = "immune" ] && variant1=1 || variant1=0 + [ "$variant2" = "immune" ] && variant2=1 || variant2=0 + [ "$variant3" = "immune" ] && variant3=1 || variant3=0 + _debug "is_cpu_vulnerable: final results are <$variant1> <$variant2> <$variant3>" + is_cpu_vulnerable_cached=1 + _is_cpu_vulnerable_cached "$1" + return $? +} + +is_cpu_specex_free() +{ + # return true (0) if the CPU doesn't do speculative execution, false (1) if it does. + # if it's not in the list we know, return false (1). + # source: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/cpu/common.c#n882 + # { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_CEDARVIEW, X86_FEATURE_ANY }, + # { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_CLOVERVIEW, X86_FEATURE_ANY }, + # { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_LINCROFT, X86_FEATURE_ANY }, + # { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_PENWELL, X86_FEATURE_ANY }, + # { X86_VENDOR_INTEL, 6, INTEL_FAM6_ATOM_PINEVIEW, X86_FEATURE_ANY }, + # { X86_VENDOR_CENTAUR, 5 }, + # { X86_VENDOR_INTEL, 5 }, + # { X86_VENDOR_NSC, 5 }, + # { X86_VENDOR_ANY, 4 }, + parse_cpu_details + if is_intel; then + if [ "$cpu_family" = 6 ]; then + if [ "$cpu_model" = "$INTEL_FAM6_ATOM_CEDARVIEW" ] || \ + [ "$cpu_model" = "$INTEL_FAM6_ATOM_CLOVERVIEW" ] || \ + [ "$cpu_model" = "$INTEL_FAM6_ATOM_LINCROFT" ] || \ + [ "$cpu_model" = "$INTEL_FAM6_ATOM_PENWELL" ] || \ + [ "$cpu_model" = "$INTEL_FAM6_ATOM_PINEVIEW" ]; then + return 0 + fi + elif [ "$cpu_family" = 5 ]; then + return 0 + fi + fi + [ "$cpu_family" = 4 ] && return 0 + return 1 } show_header() { - _info "\033[1;34mSpectre and Meltdown mitigation detection tool v$VERSION\033[0m" + _info "Spectre and Meltdown mitigation detection tool v$VERSION" _info } @@ -233,20 +425,23 @@ parse_opt_file() while [ -n "$1" ]; do if [ "$1" = "--kernel" ]; then - opt_kernel=$(parse_opt_file kernel "$2") - [ $? -ne 0 ] && exit $? + opt_kernel=$(parse_opt_file kernel "$2"); ret=$? + [ $ret -ne 0 ] && exit 255 shift 2 opt_live=0 elif [ "$1" = "--config" ]; then - opt_config=$(parse_opt_file config "$2") - [ $? -ne 0 ] && exit $? + opt_config=$(parse_opt_file config "$2"); ret=$? + [ $ret -ne 0 ] && exit 255 shift 2 opt_live=0 elif [ "$1" = "--map" ]; then - opt_map=$(parse_opt_file map "$2") - [ $? -ne 0 ] && exit $? + opt_map=$(parse_opt_file map "$2"); ret=$? + [ $ret -ne 0 ] && exit 255 shift 2 opt_live=0 + elif [ "$1" = "--arch-prefix" ]; then + opt_arch_prefix="$2" + shift 2 elif [ "$1" = "--live" ]; then opt_live_explicit=1 shift @@ -256,27 +451,49 @@ while [ -n "$1" ]; do elif [ "$1" = "--no-sysfs" ]; then opt_no_sysfs=1 shift + elif [ "$1" = "--sysfs-only" ]; then + opt_sysfs_only=1 + shift + elif [ "$1" = "--coreos" ]; then + opt_coreos=1 + shift + elif [ "$1" = "--coreos-within-toolbox" ]; then + # don't use directly: used internally by --coreos + opt_coreos=0 + shift + elif [ "$1" = "--paranoid" ]; then + opt_paranoid=1 + shift + elif [ "$1" = "--hw-only" ]; then + opt_hw_only=1 + shift + elif [ "$1" = "--no-hw" ]; then + opt_no_hw=1 + shift + elif [ "$1" = "--no-explain" ]; then + opt_no_explain=1 + shift elif [ "$1" = "--batch" ]; then opt_batch=1 opt_verbose=0 shift case "$1" in - text|nrpe|json) opt_batch_format="$1"; shift;; + text|nrpe|json|prometheus) opt_batch_format="$1"; shift;; --*) ;; # allow subsequent flags '') ;; # allow nothing at all *) - echo "$0: error: unknown batch format '$1'" - echo "$0: error: --batch expects a format from: text, nrpe, json" - exit 1 >&2 + echo "$0: error: unknown batch format '$1'" >&2 + echo "$0: error: --batch expects a format from: text, nrpe, json" >&2 + exit 255 ;; esac - elif [ "$1" = "-v" -o "$1" = "--verbose" ]; then - opt_verbose=$(expr $opt_verbose + 1) + elif [ "$1" = "-v" ] || [ "$1" = "--verbose" ]; then + opt_verbose=$(( opt_verbose + 1 )) shift elif [ "$1" = "--variant" ]; then if [ -z "$2" ]; then echo "$0: error: option --variant expects a parameter (1, 2 or 3)" >&2 - exit 1 + exit 255 fi case "$2" in 1) opt_variant1=1; opt_allvariants=0;; @@ -284,17 +501,18 @@ while [ -n "$1" ]; do 3) opt_variant3=1; opt_allvariants=0;; *) echo "$0: error: invalid parameter '$2' for --variant, expected either 1, 2 or 3" >&2; - exit 1;; + exit 255 + ;; esac shift 2 - elif [ "$1" = "-h" -o "$1" = "--help" ]; then + elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then show_header show_usage exit 0 elif [ "$1" = "--version" ]; then opt_no_color=1 show_header - exit 1 + exit 0 elif [ "$1" = "--disclaimer" ]; then show_header show_disclaimer @@ -303,12 +521,22 @@ while [ -n "$1" ]; do show_header show_usage echo "$0: error: unknown option '$1'" - exit 1 + exit 255 fi done show_header +if [ "$opt_no_sysfs" = 1 ] && [ "$opt_sysfs_only" = 1 ]; then + _warn "Incompatible options specified (--no-sysfs and --sysfs-only), aborting" + exit 255 +fi + +if [ "$opt_no_hw" = 1 ] && [ "$opt_hw_only" = 1 ]; then + _warn "Incompatible options specified (--no-hw and --hw-only), aborting" + exit 255 +fi + # print status function pstatus() { @@ -316,54 +544,63 @@ pstatus() _info_nol "$2" else case "$1" in - red) col="\033[101m\033[30m";; - green) col="\033[102m\033[30m";; - yellow) col="\033[103m\033[30m";; - blue) col="\033[104m\033[30m";; + red) col="\033[41m\033[30m";; + green) col="\033[42m\033[30m";; + yellow) col="\033[43m\033[30m";; + blue) col="\033[44m\033[30m";; *) col="";; esac _info_nol "$col $2 \033[0m" fi [ -n "$3" ] && _info_nol " ($3)" _info + unset col } # Print the final status of a vulnerability (incl. batch mode) # Arguments are: CVE UNK/OK/VULN description pvulnstatus() { + pvulnstatus_last_cve="$1" if [ "$opt_batch" = 1 ]; then - case "$opt_batch_format" in - text) _echo 0 "$1: $2 ($3)";; - nrpe) - case "$2" in - UKN) nrpe_unknown="1";; - VULN) nrpe_critical="1"; nrpe_vuln="$nrpe_vuln $1";; - esac - ;; - json) - case "$1" in - CVE-2017-5753) aka="SPECTRE VARIANT 1";; - CVE-2017-5715) aka="SPECTRE VARIANT 2";; - CVE-2017-5754) aka="MELTDOWN";; - esac - case "$2" in - UKN) is_vuln="unknown";; - VULN) is_vuln="true";; - OK) is_vuln="false";; - esac - json_output="${json_output:-[}{\"NAME\":\""$aka"\",\"CVE\":\""$1"\",\"VULNERABLE\":$is_vuln,\"INFOS\":\""$3"\"}," - ;; + case "$1" in + CVE-2017-5753) aka="SPECTRE VARIANT 1";; + CVE-2017-5715) aka="SPECTRE VARIANT 2";; + CVE-2017-5754) aka="MELTDOWN";; + esac + + case "$opt_batch_format" in + text) _echo 0 "$1: $2 ($3)";; + json) + case "$2" in + UNK) is_vuln="null";; + VULN) is_vuln="true";; + OK) is_vuln="false";; + esac + json_output="${json_output:-[}{\"NAME\":\"$aka\",\"CVE\":\"$1\",\"VULNERABLE\":$is_vuln,\"INFOS\":\"$3\"}," + ;; + + nrpe) [ "$2" = VULN ] && nrpe_vuln="$nrpe_vuln $1";; + prometheus) + prometheus_output="${prometheus_output:+$prometheus_output\n}specex_vuln_status{name=\"$aka\",cve=\"$1\",status=\"$2\",info=\"$3\"} 1" + ;; esac fi - _info_nol "> \033[46m\033[30mSTATUS:\033[0m " + # always fill global_* vars because we use that do decide the program exit code + case "$2" in + UNK) global_unknown="1";; + VULN) global_critical="1";; + esac + + # display info if we're not in quiet/batch mode vulnstatus="$2" shift 2 + _info_nol "> \033[46m\033[30mSTATUS:\033[0m " case "$vulnstatus" in - UNK) pstatus yellow UNKNOWN "$@";; - VULN) pstatus red 'VULNERABLE' "$@";; - OK) pstatus green 'NOT VULNERABLE' "$@";; + UNK) pstatus yellow 'UNKNOWN' "$@";; + VULN) pstatus red 'VULNERABLE' "$@";; + OK) pstatus green 'NOT VULNERABLE' "$@";; esac } @@ -384,12 +621,38 @@ pvulnstatus() # Licensed under the GNU General Public License, version 2 (GPLv2). # ---------------------------------------------------------------------- -vmlinux='' -vmlinux_err='' -check_vmlinux() +kernel='' +kernel_err='' +check_kernel() { - readelf -h "$1" > /dev/null 2>&1 || return 1 - return 0 + _file="$1" + _desperate_mode="$2" + # checking the return code of readelf -h is not enough, we could get + # a damaged ELF file and validate it, check for stderr warnings too + _readelf_warnings=$("${opt_arch_prefix}readelf" -S "$_file" 2>&1 >/dev/null | tr "\n" "/"); ret=$? + _readelf_sections=$("${opt_arch_prefix}readelf" -S "$_file" 2>/dev/null | grep -c -e data -e text -e init) + _kernel_size=$(stat -c %s "$_file" 2>/dev/null || stat -f %z "$_file" 2>/dev/null || echo 10000) + _debug "check_kernel: ret=$? size=$_kernel_size sections=$_readelf_sections warnings=$_readelf_warnings" + if [ -n "$_desperate_mode" ]; then + if "${opt_arch_prefix}strings" "$_file" | grep -Eq '^Linux version '; then + _debug "check_kernel (desperate): ... matched!" + return 0 + else + _debug "check_kernel (desperate): ... invalid" + fi + else + if [ $ret -eq 0 ] && [ -z "$_readelf_warnings" ] && [ "$_readelf_sections" -gt 0 ]; then + if [ "$_kernel_size" -ge 100000 ]; then + _debug "check_kernel: ... file is valid" + return 0 + else + _debug "check_kernel: ... file seems valid but is too small, ignoring" + fi + else + _debug "check_kernel: ... file is invalid" + fi + fi + return 1 } try_decompress() @@ -398,159 +661,66 @@ try_decompress() # "grep" that report the byte offset of the line instead of the pattern. # Try to find the header ($1) and decompress from here - for pos in `tr "$1\n$2" "\n$2=" < "$6" | grep -abo "^$2"` + _debug "try_decompress: looking for $3 magic in $6" + for pos in $(tr "$1\n$2" "\n$2=" < "$6" | grep -abo "^$2") do _debug "try_decompress: magic for $3 found at offset $pos" if ! which "$3" >/dev/null 2>&1; then - vmlinux_err="missing '$3' tool, please install it, usually it's in the '$5' package" + kernel_err="missing '$3' tool, please install it, usually it's in the '$5' package" return 0 fi pos=${pos%%:*} - tail -c+$pos "$6" 2>/dev/null | $3 $4 > $vmlinuxtmp 2>/dev/null - if check_vmlinux "$vmlinuxtmp"; then - vmlinux="$vmlinuxtmp" + # shellcheck disable=SC2086 + tail -c+$pos "$6" 2>/dev/null | $3 $4 > "$kerneltmp" 2>/dev/null; ret=$? + if [ ! -s "$kerneltmp" ]; then + # don't rely on $ret, sometimes it's != 0 but worked + # (e.g. gunzip ret=2 just means there was trailing garbage) + _debug "try_decompress: decompression with $3 failed (err=$ret)" + elif check_kernel "$kerneltmp" "$7"; then + kernel="$kerneltmp" _debug "try_decompress: decompressed with $3 successfully!" return 0 + elif [ "$3" != "cat" ]; then + _debug "try_decompress: decompression with $3 worked but result is not a kernel, trying with an offset" + [ -z "$kerneltmp2" ] && kerneltmp2=$(mktemp /tmp/kernel-XXXXXX) + cat "$kerneltmp" > "$kerneltmp2" + try_decompress '\177ELF' xxy 'cat' '' cat "$kerneltmp2" && return 0 else - _debug "try_decompress: decompression with $3 did not work" + _debug "try_decompress: decompression with $3 worked but result is not a kernel" fi done return 1 } -extract_vmlinux() +extract_kernel() { [ -n "$1" ] || return 1 # Prepare temp files: - vmlinuxtmp="$(mktemp /tmp/vmlinux-XXXXXX)" - trap "rm -f $vmlinuxtmp" EXIT + kerneltmp="$(mktemp /tmp/kernel-XXXXXX)" # Initial attempt for uncompressed images or objects: - if check_vmlinux "$1"; then - cat "$1" > "$vmlinuxtmp" - vmlinux=$vmlinuxtmp + if check_kernel "$1"; then + cat "$1" > "$kerneltmp" + kernel=$kerneltmp return 0 fi # That didn't work, so retry after decompression. - try_decompress '\037\213\010' xy gunzip '' gunzip "$1" && return 0 - try_decompress '\3757zXZ\000' abcde unxz '' xz-utils "$1" && return 0 - try_decompress 'BZh' xy bunzip2 '' bzip2 "$1" && return 0 - try_decompress '\135\0\0\0' xxx unlzma '' xz-utils "$1" && return 0 - try_decompress '\211\114\132' xy 'lzop' '-d' lzop "$1" && return 0 - try_decompress '\002\041\114\030' xyy 'lz4' '-d -l' liblz4-tool "$1" && return 0 + for mode in '' 'desperate'; do + try_decompress '\037\213\010' xy gunzip '' gunzip "$1" "$mode" && return 0 + try_decompress '\3757zXZ\000' abcde unxz '' xz-utils "$1" "$mode" && return 0 + try_decompress 'BZh' xy bunzip2 '' bzip2 "$1" "$mode" && return 0 + try_decompress '\135\0\0\0' xxx unlzma '' xz-utils "$1" "$mode" && return 0 + try_decompress '\211\114\132' xy 'lzop' '-d' lzop "$1" "$mode" && return 0 + try_decompress '\002\041\114\030' xyy 'lz4' '-d -l' liblz4-tool "$1" "$mode" && return 0 + try_decompress '\177ELF' xxy 'cat' '' cat "$1" "$mode" && return 0 + done + _verbose "Couldn't extract the kernel image, accuracy might be reduced" return 1 } # end of extract-vmlinux functions -# check for mode selection inconsistency -if [ "$opt_live_explicit" = 1 ]; then - if [ -n "$opt_kernel" -o -n "$opt_config" -o -n "$opt_map" ]; then - show_usage - echo "$0: error: incompatible modes specified, use either --live or --kernel/--config/--map" - exit 1 - fi -fi - -# root check (only for live mode, for offline mode, we already checked if we could read the files) - -if [ "$opt_live" = 1 ]; then - if [ "$(id -u)" -ne 0 ]; then - _warn "Note that you should launch this script with root privileges to get accurate information." - _warn "We'll proceed but you might see permission denied errors." - _warn "To run it as root, you can try the following command: sudo $0" - _warn - fi - _info "Checking for vulnerabilities against running kernel \033[35m"$(uname -s) $(uname -r) $(uname -v) $(uname -m)"\033[0m" - _info "CPU is\033[35m"$(grep '^model name' /proc/cpuinfo | cut -d: -f2 | head -1)"\033[0m" - - # try to find the image of the current running kernel - # first, look for the BOOT_IMAGE hint in the kernel cmdline - if [ -r /proc/cmdline ] && grep -q 'BOOT_IMAGE=' /proc/cmdline; then - opt_kernel=$(grep -Eo 'BOOT_IMAGE=[^ ]+' /proc/cmdline | cut -d= -f2) - _debug "found opt_kernel=$opt_kernel in /proc/cmdline" - # if we have a dedicated /boot partition, our bootloader might have just called it / - # so try to prepend /boot and see if we find anything - [ -e "/boot/$opt_kernel" ] && opt_kernel="/boot/$opt_kernel" - _debug "opt_kernel is now $opt_kernel" - # else, the full path is already there (most probably /boot/something) - fi - # if we didn't find a kernel, default to guessing - if [ ! -e "$opt_kernel" ]; then - [ -e /boot/vmlinuz-linux ] && opt_kernel=/boot/vmlinuz-linux - [ -e /boot/vmlinuz-linux-libre ] && opt_kernel=/boot/vmlinuz-linux-libre - [ -e /boot/vmlinuz-$(uname -r) ] && opt_kernel=/boot/vmlinuz-$(uname -r) - [ -e /boot/kernel-$( uname -r) ] && opt_kernel=/boot/kernel-$( uname -r) - [ -e /boot/bzImage-$(uname -r) ] && opt_kernel=/boot/bzImage-$(uname -r) - [ -e /boot/kernel-genkernel-$(uname -m)-$(uname -r) ] && opt_kernel=/boot/kernel-genkernel-$(uname -m)-$(uname -r) - fi - - # system.map - if [ -e /proc/kallsyms ] ; then - opt_map="/proc/kallsyms" - elif [ -e /boot/System.map-$(uname -r) ] ; then - opt_map=/boot/System.map-$(uname -r) - fi - - # config - if [ -e /proc/config.gz ] ; then - dumped_config="$(mktemp /tmp/config-XXXXXX)" - gunzip -c /proc/config.gz > $dumped_config - # dumped_config will be deleted at the end of the script - opt_config=$dumped_config - elif [ -e /boot/config-$(uname -r) ]; then - opt_config=/boot/config-$(uname -r) - fi -else - _info "Checking for vulnerabilities against specified kernel" -fi - -if [ -n "$opt_kernel" ]; then - _verbose "Will use vmlinux image \033[35m$opt_kernel\033[0m" -else - _verbose "Will use no vmlinux image (accuracy might be reduced)" - bad_accuracy=1 -fi -if [ -n "$dumped_config" ]; then - _verbose "Will use kconfig \033[35m/proc/config.gz\033[0m" -elif [ -n "$opt_config" ]; then - _verbose "Will use kconfig \033[35m$opt_config\033[0m" -else - _verbose "Will use no kconfig (accuracy might be reduced)" - bad_accuracy=1 -fi -if [ -n "$opt_map" ]; then - _verbose "Will use System.map file \033[35m$opt_map\033[0m" -else - _verbose "Will use no System.map file (accuracy might be reduced)" - bad_accuracy=1 -fi - -if [ "$bad_accuracy" = 1 ]; then - _info "We're missing some kernel info (see -v), accuracy might be reduced" -fi - -if [ -e "$opt_kernel" ]; then - if ! which readelf >/dev/null 2>&1; then - vmlinux_err="missing 'readelf' tool, please install it, usually it's in the 'binutils' package" - else - extract_vmlinux "$opt_kernel" - fi -else - vmlinux_err="couldn't find your kernel image in /boot, if you used netboot, this is normal" -fi -if [ -z "$vmlinux" -o ! -r "$vmlinux" ]; then - [ -z "$vmlinux_err" ] && vmlinux_err="couldn't extract your kernel from $opt_kernel" -fi - -_info - -# end of header stuff - -# now we define some util functions and the check_*() funcs, as -# the user can choose to execute only some of those - mount_debugfs() { if [ ! -e /sys/kernel/debug/sched_features ]; then @@ -559,188 +729,1444 @@ mount_debugfs() fi } -umount_debugfs() +load_msr() { - if [ "$mounted_debugfs" = 1 ]; then - # umount debugfs if we did mount it ourselves - umount /sys/kernel/debug + if [ "$os" = Linux ]; then + modprobe msr 2>/dev/null && insmod_msr=1 + _debug "attempted to load module msr, insmod_msr=$insmod_msr" + else + if ! kldstat -q -m cpuctl; then + kldload cpuctl 2>/dev/null && kldload_cpuctl=1 + _debug "attempted to load module cpuctl, kldload_cpuctl=$kldload_cpuctl" + else + _debug "cpuctl module already loaded" + fi fi } +load_cpuid() +{ + if [ "$os" = Linux ]; then + modprobe cpuid 2>/dev/null && insmod_cpuid=1 + _debug "attempted to load module cpuid, insmod_cpuid=$insmod_cpuid" + else + if ! kldstat -q -m cpuctl; then + kldload cpuctl 2>/dev/null && kldload_cpuctl=1 + _debug "attempted to load module cpuctl, kldload_cpuctl=$kldload_cpuctl" + else + _debug "cpuctl module already loaded" + fi + fi +} + +# shellcheck disable=SC2034 +{ +EAX=1; EBX=2; ECX=3; EDX=4; +} +read_cpuid() +{ + # leaf is the value of the eax register when calling the cpuid instruction: + _leaf="$1" + # eax=1 ebx=2 ecx=3 edx=4: + _register="$2" + # number of bits to shift the register right to: + _shift="$3" + # mask to apply as an AND operand to the shifted register value + _mask="$4" + # wanted value (optional), if present we return 0(true) if the obtained value is equal, 1 otherwise: + _wanted="$5" + # in any case, the read value is globally available in $read_cpuid_value + + read_cpuid_value='' + if [ ! -e /dev/cpu/0/cpuid ] && [ ! -e /dev/cpuctl0 ]; then + # try to load the module ourselves (and remember it so we can rmmod it afterwards) + load_cpuid + fi + + if [ -e /dev/cpu/0/cpuid ]; then + # Linux + # we need _leaf to be converted to decimal for dd + _leaf=$(( _leaf )) + _cpuid=$(dd if=/dev/cpu/0/cpuid bs=16 skip="$_leaf" iflag=skip_bytes count=1 2>/dev/null | od -A n -t u4) + elif [ -e /dev/cpuctl0 ]; then + # BSD + _cpuid=$(cpucontrol -i "$_leaf" /dev/cpuctl0 2>/dev/null | awk '{print $4,$5,$6,$7}') + # cpuid level 0x1: 0x000306d4 0x00100800 0x4dfaebbf 0xbfebfbff + else + return 2 + fi + + _debug "cpuid: leaf$_leaf on cpu0, eax-ebx-ecx-edx: $_cpuid" + [ -z "$_cpuid" ] && return 2 + # get the value of the register we want + _reg=$(echo "$_cpuid" | awk '{print $'"$_register"'}') + # Linux returns it as decimal, BSD as hex, normalize to decimal + _reg=$(( _reg )) + # shellcheck disable=SC2046 + _debug "cpuid: wanted register ($_register) has value $_reg aka "$(printf "%08x" "$_reg") + _reg_shifted=$(( _reg >> _shift )) + # shellcheck disable=SC2046 + _debug "cpuid: shifted value by $_shift is $_reg_shifted aka "$(printf "%x" "$_reg_shifted") + read_cpuid_value=$(( _reg_shifted & _mask )) + # shellcheck disable=SC2046 + _debug "cpuid: after AND $_mask, final value is $read_cpuid_value aka "$(printf "%x" "$read_cpuid_value") + if [ -n "$_wanted" ]; then + _debug "cpuid: wanted $_wanted and got $read_cpuid_value" + if [ "$read_cpuid_value" = "$_wanted" ]; then + return 0 + else + return 1 + fi + fi + + return 0 +} + +dmesg_grep() +{ + # grep for something in dmesg, ensuring that the dmesg buffer + # has not been truncated + dmesg_grepped='' + if ! dmesg | grep -qE -e '(^|\] )Linux version [0-9]' -e '^FreeBSD is a registered' ; then + # dmesg truncated + return 2 + fi + dmesg_grepped=$(dmesg | grep -E "$1" | head -1) + # not found: + [ -z "$dmesg_grepped" ] && return 1 + # found, output is in $dmesg_grepped + return 0 +} + +is_coreos() +{ + which coreos-install >/dev/null 2>&1 && which toolbox >/dev/null 2>&1 && return 0 + return 1 +} + +parse_cpu_details() +{ + [ "$parse_cpu_details_done" = 1 ] && return 0 + + if [ -e "$procfs/cpuinfo" ]; then + cpu_vendor=$( grep '^vendor_id' "$procfs/cpuinfo" | awk '{print $3}' | head -1) + cpu_friendly_name=$(grep '^model name' "$procfs/cpuinfo" | cut -d: -f2- | head -1 | sed -e 's/^ *//') + # special case for ARM follows + if grep -qi 'CPU implementer[[:space:]]*:[[:space:]]*0x41' "$procfs/cpuinfo"; then + cpu_vendor='ARM' + # some devices (phones or other) have several ARMs and as such different part numbers, + # an example is "bigLITTLE", so we need to store the whole list, this is needed for is_cpu_vulnerable + cpu_part_list=$(awk '/CPU part/ {print $4}' "$procfs/cpuinfo") + cpu_arch_list=$(awk '/CPU architecture/ {print $3}' "$procfs/cpuinfo") + # take the first one to fill the friendly name, do NOT quote the vars below + # shellcheck disable=SC2086 + cpu_arch=$(echo $cpu_arch_list | awk '{ print $1 }') + # shellcheck disable=SC2086 + cpu_part=$(echo $cpu_part_list | awk '{ print $1 }') + [ "$cpu_arch" = "AArch64" ] && cpu_arch=8 + cpu_friendly_name="ARM" + [ -n "$cpu_arch" ] && cpu_friendly_name="$cpu_friendly_name v$cpu_arch" + [ -n "$cpu_part" ] && cpu_friendly_name="$cpu_friendly_name model $cpu_part" + fi + + cpu_family=$( grep '^cpu family' "$procfs/cpuinfo" | awk '{print $4}' | grep -E '^[0-9]+$' | head -1) + cpu_model=$( grep '^model' "$procfs/cpuinfo" | awk '{print $3}' | grep -E '^[0-9]+$' | head -1) + cpu_stepping=$(grep '^stepping' "$procfs/cpuinfo" | awk '{print $3}' | grep -E '^[0-9]+$' | head -1) + cpu_ucode=$( grep '^microcode' "$procfs/cpuinfo" | awk '{print $3}' | head -1) + else + cpu_friendly_name=$(sysctl -n hw.model) + fi + + # get raw cpuid, it's always useful (referenced in the Intel doc for firmware updates for example) + if read_cpuid 0x1 $EAX 0 0xFFFFFFFF; then + cpuid="$read_cpuid_value" + fi + + # under BSD, linprocfs often doesn't export ucode information, so fetch it ourselves the good old way + if [ -z "$cpu_ucode" ] && [ "$os" != Linux ]; then + load_cpuid + if [ -e /dev/cpuctl0 ]; then + # init MSR with NULLs + cpucontrol -m 0x8b=0 /dev/cpuctl0 + # call CPUID + cpucontrol -i 1 /dev/cpuctl0 >/dev/null + # read MSR + cpu_ucode=$(cpucontrol -m 0x8b /dev/cpuctl0 | awk '{print $3}') + # convert to decimal + cpu_ucode=$(( cpu_ucode )) + # convert back to hex + cpu_ucode=$(printf "0x%x" "$cpu_ucode") + fi + fi + + echo "$cpu_ucode" | grep -q ^0x && cpu_ucode_decimal=$(( cpu_ucode )) + ucode_found="model $cpu_model stepping $cpu_stepping ucode $cpu_ucode cpuid "$(printf "0x%x" "$cpuid") + + # also define those that we will need in other funcs + # taken from ttps://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/intel-family.h + # shellcheck disable=SC2034 + { + INTEL_FAM6_CORE_YONAH=$(( 0x0E )) + + INTEL_FAM6_CORE2_MEROM=$(( 0x0F )) + INTEL_FAM6_CORE2_MEROM_L=$(( 0x16 )) + INTEL_FAM6_CORE2_PENRYN=$(( 0x17 )) + INTEL_FAM6_CORE2_DUNNINGTON=$(( 0x1D )) + + INTEL_FAM6_NEHALEM=$(( 0x1E )) + INTEL_FAM6_NEHALEM_G=$(( 0x1F )) + INTEL_FAM6_NEHALEM_EP=$(( 0x1A )) + INTEL_FAM6_NEHALEM_EX=$(( 0x2E )) + + INTEL_FAM6_WESTMERE=$(( 0x25 )) + INTEL_FAM6_WESTMERE_EP=$(( 0x2C )) + INTEL_FAM6_WESTMERE_EX=$(( 0x2F )) + + INTEL_FAM6_SANDYBRIDGE=$(( 0x2A )) + INTEL_FAM6_SANDYBRIDGE_X=$(( 0x2D )) + INTEL_FAM6_IVYBRIDGE=$(( 0x3A )) + INTEL_FAM6_IVYBRIDGE_X=$(( 0x3E )) + + INTEL_FAM6_HASWELL_CORE=$(( 0x3C )) + INTEL_FAM6_HASWELL_X=$(( 0x3F )) + INTEL_FAM6_HASWELL_ULT=$(( 0x45 )) + INTEL_FAM6_HASWELL_GT3E=$(( 0x46 )) + + INTEL_FAM6_BROADWELL_CORE=$(( 0x3D )) + INTEL_FAM6_BROADWELL_GT3E=$(( 0x47 )) + INTEL_FAM6_BROADWELL_X=$(( 0x4F )) + INTEL_FAM6_BROADWELL_XEON_D=$(( 0x56 )) + + INTEL_FAM6_SKYLAKE_MOBILE=$(( 0x4E )) + INTEL_FAM6_SKYLAKE_DESKTOP=$(( 0x5E )) + INTEL_FAM6_SKYLAKE_X=$(( 0x55 )) + INTEL_FAM6_KABYLAKE_MOBILE=$(( 0x8E )) + INTEL_FAM6_KABYLAKE_DESKTOP=$(( 0x9E )) + + # /* "Small Core" Processors (Atom) */ + + INTEL_FAM6_ATOM_PINEVIEW=$(( 0x1C )) + INTEL_FAM6_ATOM_LINCROFT=$(( 0x26 )) + INTEL_FAM6_ATOM_PENWELL=$(( 0x27 )) + INTEL_FAM6_ATOM_CLOVERVIEW=$(( 0x35 )) + INTEL_FAM6_ATOM_CEDARVIEW=$(( 0x36 )) + INTEL_FAM6_ATOM_SILVERMONT1=$(( 0x37 )) + INTEL_FAM6_ATOM_SILVERMONT2=$(( 0x4D )) + INTEL_FAM6_ATOM_AIRMONT=$(( 0x4C )) + INTEL_FAM6_ATOM_MERRIFIELD=$(( 0x4A )) + INTEL_FAM6_ATOM_MOOREFIELD=$(( 0x5A )) + INTEL_FAM6_ATOM_GOLDMONT=$(( 0x5C )) + INTEL_FAM6_ATOM_DENVERTON=$(( 0x5F )) + INTEL_FAM6_ATOM_GEMINI_LAKE=$(( 0x7A )) + + # /* Xeon Phi */ + + INTEL_FAM6_XEON_PHI_KNL=$(( 0x57 )) + INTEL_FAM6_XEON_PHI_KNM=$(( 0x85 )) + } + parse_cpu_details_done=1 +} + +is_amd() +{ + [ "$cpu_vendor" = AuthenticAMD ] && return 0 + return 1 +} + +is_intel() +{ + [ "$cpu_vendor" = GenuineIntel ] && return 0 + return 1 +} + +is_cpu_smt_enabled() +{ + # SMT / HyperThreading is enabled if siblings != cpucores + if [ -e "$procfs/cpuinfo" ]; then + _siblings=$(awk '/^siblings/ {print $3;exit}' "$procfs/cpuinfo") + _cpucores=$(awk '/^cpu cores/ {print $4;exit}' "$procfs/cpuinfo") + if [ -n "$_siblings" ] && [ -n "$_cpucores" ]; then + if [ "$_siblings" = "$_cpucores" ]; then + return 1 + else + return 0 + fi + fi + fi + # we can't tell + return 2 +} + +is_ucode_blacklisted() +{ + parse_cpu_details + # if it's not an Intel, don't bother: it's not blacklisted + is_intel || return 1 + # it also needs to be family=6 + [ "$cpu_family" = 6 ] || return 1 + # now, check each known bad microcode + # source: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/kernel/cpu/intel.c#n105 + # 2018-02-08 update: https://newsroom.intel.com/wp-content/uploads/sites/11/2018/02/microcode-update-guidance.pdf + # model,stepping,microcode + for tuple in \ + $INTEL_FAM6_KABYLAKE_DESKTOP,0x0B,0x80 \ + $INTEL_FAM6_KABYLAKE_DESKTOP,0x0A,0x80 \ + $INTEL_FAM6_KABYLAKE_DESKTOP,0x09,0x80 \ + $INTEL_FAM6_KABYLAKE_MOBILE,0x0A,0x80 \ + $INTEL_FAM6_KABYLAKE_MOBILE,0x09,0x80 \ + $INTEL_FAM6_SKYLAKE_X,0x03,0x0100013e \ + $INTEL_FAM6_SKYLAKE_X,0x04,0x02000036 \ + $INTEL_FAM6_SKYLAKE_X,0x04,0x0200003a \ + $INTEL_FAM6_SKYLAKE_X,0x04,0x0200003c \ + $INTEL_FAM6_BROADWELL_CORE,0x04,0x28 \ + $INTEL_FAM6_BROADWELL_GT3E,0x01,0x1b \ + $INTEL_FAM6_BROADWELL_XEON_D,0x02,0x14 \ + $INTEL_FAM6_BROADWELL_XEON_D,0x03,0x07000011 \ + $INTEL_FAM6_BROADWELL_X,0x01,0x0b000023 \ + $INTEL_FAM6_BROADWELL_X,0x01,0x0b000025 \ + $INTEL_FAM6_HASWELL_ULT,0x01,0x21 \ + $INTEL_FAM6_HASWELL_GT3E,0x01,0x18 \ + $INTEL_FAM6_HASWELL_CORE,0x03,0x23 \ + $INTEL_FAM6_HASWELL_X,0x02,0x3b \ + $INTEL_FAM6_HASWELL_X,0x04,0x10 \ + $INTEL_FAM6_IVYBRIDGE_X,0x04,0x42a \ + $INTEL_FAM6_SANDYBRIDGE_X,0x06,0x61b \ + $INTEL_FAM6_SANDYBRIDGE_X,0x07,0x712 + do + model=$(echo $tuple | cut -d, -f1) + stepping=$(( $(echo $tuple | cut -d, -f2) )) + ucode=$(echo $tuple | cut -d, -f3) + echo "$ucode" | grep -q ^0x && ucode_decimal=$(( ucode )) + if [ "$cpu_model" = "$model" ] && [ "$cpu_stepping" = "$stepping" ]; then + if [ "$cpu_ucode_decimal" = "$ucode_decimal" ] || [ "$cpu_ucode" = "$ucode" ]; then + _debug "is_ucode_blacklisted: we have a match! ($cpu_model/$cpu_stepping/$cpu_ucode)" + return 0 + fi + fi + done + _debug "is_ucode_blacklisted: no ($cpu_model/$cpu_stepping/$cpu_ucode)" + return 1 +} + +is_skylake_cpu() +{ + # is this a skylake cpu? + # return 0 if yes, 1 otherwise + #if (boot_cpu_data.x86_vendor == X86_VENDOR_INTEL && + # boot_cpu_data.x86 == 6) { + # switch (boot_cpu_data.x86_model) { + # case INTEL_FAM6_SKYLAKE_MOBILE: + # case INTEL_FAM6_SKYLAKE_DESKTOP: + # case INTEL_FAM6_SKYLAKE_X: + # case INTEL_FAM6_KABYLAKE_MOBILE: + # case INTEL_FAM6_KABYLAKE_DESKTOP: + # return true; + parse_cpu_details + is_intel || return 1 + [ "$cpu_family" = 6 ] || return 1 + if [ "$cpu_model" = $INTEL_FAM6_SKYLAKE_MOBILE ] || \ + [ "$cpu_model" = $INTEL_FAM6_SKYLAKE_DESKTOP ] || \ + [ "$cpu_model" = $INTEL_FAM6_SKYLAKE_X ] || \ + [ "$cpu_model" = $INTEL_FAM6_KABYLAKE_MOBILE ] || \ + [ "$cpu_model" = $INTEL_FAM6_KABYLAKE_DESKTOP ]; then + return 0 + fi + return 1 +} + +is_zen_cpu() +{ + # is this CPU from the AMD ZEN family ? (ryzen, epyc, ...) + parse_cpu_details + is_amd || return 1 + [ "$cpu_family" = 23 ] && return 0 + return 1 +} + +# ENTRYPOINT + +# we can't do anything useful under WSL +if uname -a | grep -qE -- '-Microsoft #[0-9]+-Microsoft '; then + _warn "This script doesn't work under Windows Subsystem for Linux" + _warn "You should use the official Microsoft tool instead." + _warn "It can be found under https://aka.ms/SpeculationControlPS" + exit 1 +fi + +# check for mode selection inconsistency +if [ "$opt_live_explicit" = 1 ]; then + if [ -n "$opt_kernel" ] || [ -n "$opt_config" ] || [ -n "$opt_map" ]; then + show_usage + echo "$0: error: incompatible modes specified, use either --live or --kernel/--config/--map" >&2 + exit 255 + fi +fi +if [ "$opt_hw_only" = 1 ]; then + if [ "$opt_allvariants" = 0 ]; then + show_usage + echo "$0: error: incompatible modes specified, --hw-only vs --variant" >&2 + exit 255 + else + opt_allvariants=0 + opt_variant1=0 + opt_variant2=0 + opt_variant3=0 + fi +fi + +# coreos mode +if [ "$opt_coreos" = 1 ]; then + if ! is_coreos; then + _warn "CoreOS mode asked, but we're not under CoreOS!" + exit 255 + fi + _warn "CoreOS mode, starting an ephemeral toolbox to launch the script" + load_msr + load_cpuid + mount_debugfs + toolbox --ephemeral --bind-ro /dev/cpu:/dev/cpu -- sh -c "dnf install -y binutils which && /media/root$PWD/$0 $* --coreos-within-toolbox" + exitcode=$? + exit $exitcode +else + if is_coreos; then + _warn "You seem to be running CoreOS, you might want to use the --coreos option for better results" + _warn + fi +fi + +# if we're under a BSD, try to mount linprocfs for "$procfs/cpuinfo" +procfs=/proc +if echo "$os" | grep -q BSD; then + _debug "We're under BSD, check if we have procfs" + procfs=$(mount | awk '/^linprocfs/ { print $3; exit; }') + if [ -z "$procfs" ]; then + _debug "we don't, try to mount it" + procfs=/proc + [ -d /compat/linux/proc ] && procfs=/compat/linux/proc + test -d $procfs || mkdir $procfs + if mount -t linprocfs linprocfs $procfs 2>/dev/null; then + mounted_procfs=1 + _debug "procfs just mounted at $procfs" + else + procfs='' + fi + else + _debug "We do: $procfs" + fi +fi + +parse_cpu_details +if [ "$opt_live" = 1 ]; then + # root check (only for live mode, for offline mode, we already checked if we could read the files) + if [ "$(id -u)" -ne 0 ]; then + _warn "Note that you should launch this script with root privileges to get accurate information." + _warn "We'll proceed but you might see permission denied errors." + _warn "To run it as root, you can try the following command: sudo $0" + _warn + fi + _info "Checking for vulnerabilities on current system" + _info "Kernel is \033[35m$(uname -s) $(uname -r) $(uname -v) $(uname -m)\033[0m" + _info "CPU is \033[35m$cpu_friendly_name\033[0m" + + # try to find the image of the current running kernel + # first, look for the BOOT_IMAGE hint in the kernel cmdline + if [ -r /proc/cmdline ] && grep -q 'BOOT_IMAGE=' /proc/cmdline; then + opt_kernel=$(grep -Eo 'BOOT_IMAGE=[^ ]+' /proc/cmdline | cut -d= -f2) + _debug "found opt_kernel=$opt_kernel in /proc/cmdline" + # if we have a dedicated /boot partition, our bootloader might have just called it / + # so try to prepend /boot and see if we find anything + [ -e "/boot/$opt_kernel" ] && opt_kernel="/boot/$opt_kernel" + # special case for CoreOS if we're inside the toolbox + [ -e "/media/root/boot/$opt_kernel" ] && opt_kernel="/media/root/boot/$opt_kernel" + _debug "opt_kernel is now $opt_kernel" + # else, the full path is already there (most probably /boot/something) + fi + # if we didn't find a kernel, default to guessing + if [ ! -e "$opt_kernel" ]; then + # Fedora: + [ -e "/lib/modules/$(uname -r)/vmlinuz" ] && opt_kernel="/lib/modules/$(uname -r)/vmlinuz" + # Slackare: + [ -e "/boot/vmlinuz" ] && opt_kernel="/boot/vmlinuz" + # Arch: + [ -e "/boot/vmlinuz-linux" ] && opt_kernel="/boot/vmlinuz-linux" + # Linux-Libre: + [ -e "/boot/vmlinuz-linux-libre" ] && opt_kernel="/boot/vmlinuz-linux-libre" + # pine64 + [ -e "/boot/pine64/Image" ] && opt_kernel="/boot/pine64/Image" + # generic: + [ -e "/boot/vmlinuz-$(uname -r)" ] && opt_kernel="/boot/vmlinuz-$(uname -r)" + [ -e "/boot/kernel-$( uname -r)" ] && opt_kernel="/boot/kernel-$( uname -r)" + [ -e "/boot/bzImage-$(uname -r)" ] && opt_kernel="/boot/bzImage-$(uname -r)" + # Gentoo: + [ -e "/boot/kernel-genkernel-$(uname -m)-$(uname -r)" ] && opt_kernel="/boot/kernel-genkernel-$(uname -m)-$(uname -r)" + # NixOS: + [ -e "/run/booted-system/kernel" ] && opt_kernel="/run/booted-system/kernel" + # systemd kernel-install: + [ -e "/etc/machine-id" ] && [ -e "/boot/$(cat /etc/machine-id)/$(uname -r)/linux" ] && opt_kernel="/boot/$(cat /etc/machine-id)/$(uname -r)/linux" + fi + + # system.map + if [ -e /proc/kallsyms ] ; then + opt_map=/proc/kallsyms + elif [ -e "/lib/modules/$(uname -r)/System.map" ] ; then + opt_map="/lib/modules/$(uname -r)/System.map" + elif [ -e "/boot/System.map-$(uname -r)" ] ; then + opt_map="/boot/System.map-$(uname -r)" + fi + + # config + if [ -e /proc/config.gz ] ; then + dumped_config="$(mktemp /tmp/config-XXXXXX)" + gunzip -c /proc/config.gz > "$dumped_config" + # dumped_config will be deleted at the end of the script + opt_config="$dumped_config" + elif [ -e "/lib/modules/$(uname -r)/config" ]; then + opt_config="/lib/modules/$(uname -r)/config" + elif [ -e "/boot/config-$(uname -r)" ]; then + opt_config="/boot/config-$(uname -r)" + fi +else + _info "Checking for vulnerabilities against specified kernel" + _info "CPU is \033[35m$cpu_friendly_name\033[0m" +fi + +if [ -n "$opt_kernel" ]; then + _verbose "Will use kernel image \033[35m$opt_kernel\033[0m" +else + _verbose "Will use no kernel image (accuracy might be reduced)" + bad_accuracy=1 +fi + +if [ "$os" = Linux ]; then + if [ -n "$opt_config" ] && ! grep -q '^CONFIG_' "$opt_config"; then + # given file is invalid! + _warn "The kernel config file seems invalid, was expecting a plain-text file, ignoring it!" + opt_config='' + fi + + if [ -n "$dumped_config" ] && [ -n "$opt_config" ]; then + _verbose "Will use kconfig \033[35m/proc/config.gz (decompressed)\033[0m" + elif [ -n "$opt_config" ]; then + _verbose "Will use kconfig \033[35m$opt_config\033[0m" + else + _verbose "Will use no kconfig (accuracy might be reduced)" + bad_accuracy=1 + fi + + if [ -n "$opt_map" ]; then + _verbose "Will use System.map file \033[35m$opt_map\033[0m" + else + _verbose "Will use no System.map file (accuracy might be reduced)" + bad_accuracy=1 + fi + + if [ "$bad_accuracy" = 1 ]; then + _info "We're missing some kernel info (see -v), accuracy might be reduced" + fi +fi + +if [ -e "$opt_kernel" ]; then + if ! which "${opt_arch_prefix}readelf" >/dev/null 2>&1; then + _debug "readelf not found" + kernel_err="missing '${opt_arch_prefix}readelf' tool, please install it, usually it's in the 'binutils' package" + elif [ "$opt_sysfs_only" = 1 ]; then + kernel_err='kernel image decompression skipped' + else + extract_kernel "$opt_kernel" + fi +else + _debug "no opt_kernel defined" + kernel_err="couldn't find your kernel image in /boot, if you used netboot, this is normal" +fi +if [ -z "$kernel" ] || [ ! -r "$kernel" ]; then + [ -z "$kernel_err" ] && kernel_err="couldn't extract your kernel from $opt_kernel" +else + # vanilla kernels have with ^Linux version + # also try harder with some kernels (such as Red Hat) that don't have ^Linux version before their version string + # and check for FreeBSD + kernel_version=$("${opt_arch_prefix}strings" "$kernel" 2>/dev/null | grep -E \ + -e '^Linux version ' \ + -e '^[[:alnum:]][^[:space:]]+ \([^[:space:]]+\) #[0-9]+ .+ (19|20)[0-9][0-9]$' \ + -e '^FreeBSD [0-9]' | head -1) + if [ -z "$kernel_version" ]; then + # try even harder with some kernels (such as ARM) that split the release (uname -r) and version (uname -v) in 2 adjacent strings + kernel_version=$("${opt_arch_prefix}strings" "$kernel" 2>/dev/null | grep -E -B1 '^#[0-9]+ .+ (19|20)[0-9][0-9]$' | tr "\n" " ") + fi + if [ -n "$kernel_version" ]; then + # in live mode, check if the img we found is the correct one + if [ "$opt_live" = 1 ]; then + _verbose "Kernel image is \033[35m$kernel_version" + if ! echo "$kernel_version" | grep -qF "$(uname -r)"; then + _warn "Possible disrepancy between your running kernel '$(uname -r)' and the image '$kernel_version' we found ($opt_kernel), results might be incorrect" + fi + else + _info "Kernel image is \033[35m$kernel_version" + fi + else + _verbose "Kernel image version is unknown" + fi +fi + +_info + +# end of header stuff + +# now we define some util functions and the check_*() funcs, as +# the user can choose to execute only some of those + sys_interface_check() { - [ "$opt_live" = 1 -a "$opt_no_sysfs" = 0 -a -r "$1" ] || return 1 - _info_nol "* Checking whether we're safe according to the /sys interface: " + [ "$opt_live" = 1 ] && [ "$opt_no_sysfs" = 0 ] && [ -r "$1" ] || return 1 + _info_nol "* Mitigated according to the /sys interface: " + msg=$(cat "$1") if grep -qi '^not affected' "$1"; then # Not affected status=OK - pstatus green YES "kernel confirms that your CPU is unaffected" + pstatus green YES "$msg" elif grep -qi '^mitigation' "$1"; then # Mitigation: PTI status=OK - pstatus green YES "kernel confirms that the mitigation is active" + pstatus green YES "$msg" elif grep -qi '^vulnerable' "$1"; then # Vulnerable status=VULN - pstatus red NO "kernel confirms your system is vulnerable" + pstatus yellow NO "$msg" else status=UNK - pstatus yellow UNKNOWN "unknown value reported by kernel" + pstatus yellow UNKNOWN "$msg" fi - msg=$(cat "$1") _debug "sys_interface_check: $1=$msg" return 0 } +number_of_cpus() +{ + if echo "$os" | grep -q BSD; then + n=$(sysctl -n hw.ncpu 2>/dev/null || echo 1) + elif [ -e "$procfs/cpuinfo" ]; then + n=$(grep -c ^processor "$procfs/cpuinfo" 2>/dev/null || echo 1) + else + # if we don't know, default to 1 CPU + n=1 + fi + return "$n" +} + +# $1 - msr number +# $2 - cpu index +write_msr() +{ + if [ "$os" != Linux ]; then + cpucontrol -m "$1=0" "/dev/cpuctl$2" >/dev/null 2>&1; ret=$? + else + # convert to decimal + _msrindex=$(( $1 )) + if [ ! -w /dev/cpu/"$2"/msr ]; then + ret=200 # permission error + else + dd if=/dev/zero of=/dev/cpu/"$2"/msr bs=8 count=1 seek="$_msrindex" oflag=seek_bytes 2>/dev/null; ret=$? + fi + fi + _debug "write_msr: for cpu $2 on msr $1 ($_msrindex), ret=$ret" + return $ret +} + +read_msr() +{ + # _msr must be in hex, in the form 0x1234: + _msr="$1" + # cpu index, starting from 0: + _cpu="$2" + read_msr_value='' + if [ "$os" != Linux ]; then + _msr=$(cpucontrol -m "$_msr" "/dev/cpuctl$_cpu" 2>/dev/null); ret=$? + [ $ret -ne 0 ] && return 1 + # MSR 0x10: 0x000003e1 0xb106dded + _msr_h=$(echo "$_msr" | awk '{print $3}'); + _msr_h="$(( _msr_h >> 24 & 0xFF )) $(( _msr_h >> 16 & 0xFF )) $(( _msr_h >> 8 & 0xFF )) $(( _msr_h & 0xFF ))" + _msr_l=$(echo "$_msr" | awk '{print $4}'); + _msr_l="$(( _msr_l >> 24 & 0xFF )) $(( _msr_l >> 16 & 0xFF )) $(( _msr_l >> 8 & 0xFF )) $(( _msr_l & 0xFF ))" + read_msr_value="$_msr_h $_msr_l" + else + # convert to decimal + _msr=$(( _msr )) + if [ ! -r /dev/cpu/"$_cpu"/msr ]; then + return 200 # permission error + fi + read_msr_value=$(dd if=/dev/cpu/"$_cpu"/msr bs=8 count=1 skip="$_msr" iflag=skip_bytes 2>/dev/null | od -t u1 -A n) + if [ -z "$read_msr_value" ]; then + # MSR doesn't exist, don't check for $? because some versions of dd still return 0! + return 1 + fi + fi + _debug "read_msr: MSR=$1 value is $read_msr_value" + return 0 +} + + +check_cpu() +{ + _info "\033[1;34mHardware check\033[0m" + + if ! uname -m | grep -qwE 'x86_64|i[3-6]86|amd64'; then + return + fi + + _info "* Hardware support (CPU microcode) for mitigation techniques" + _info " * Indirect Branch Restricted Speculation (IBRS)" + _info_nol " * SPEC_CTRL MSR is available: " + number_of_cpus + ncpus=$? + idx_max_cpu=$((ncpus-1)) + if [ ! -e /dev/cpu/0/msr ] && [ ! -e /dev/cpuctl0 ]; then + # try to load the module ourselves (and remember it so we can rmmod it afterwards) + load_msr + fi + if [ ! -e /dev/cpu/0/msr ] && [ ! -e /dev/cpuctl0 ]; then + spec_ctrl_msr=-1 + pstatus yellow UNKNOWN "is msr kernel module available?" + else + # the new MSR 'SPEC_CTRL' is at offset 0x48 + # here we use dd, it's the same as using 'rdmsr 0x48' but without needing the rdmsr tool + # if we get a read error, the MSR is not there. bs has to be 8 for msr + # skip=9 because 8*9=72=0x48 + val=0 + cpu_mismatch=0 + for i in $(seq 0 "$idx_max_cpu") + do + read_msr 0x48 "$i"; ret=$? + if [ "$i" -eq 0 ]; then + val=$ret + else + if [ "$ret" -eq $val ]; then + continue + else + cpu_mismatch=1 + fi + fi + done + if [ $val -eq 0 ]; then + if [ $cpu_mismatch -eq 0 ]; then + spec_ctrl_msr=1 + pstatus green YES + else + spec_ctrl_msr=1 + pstatus green YES "But not in all CPUs" + fi + elif [ $val -eq 200 ]; then + pstatus yellow UNKNOWN "is msr kernel module available?" + spec_ctrl_msr=-1 + else + spec_ctrl_msr=0 + pstatus yellow NO + fi + fi + + _info_nol " * CPU indicates IBRS capability: " + # from kernel src: { X86_FEATURE_SPEC_CTRL, CPUID_EDX,26, 0x00000007, 0 }, + # amd: https://developer.amd.com/wp-content/resources/Architecture_Guidelines_Update_Indirect_Branch_Control.pdf + # amd: 8000_0008 EBX[14]=1 + if is_intel; then + read_cpuid 0x7 $EDX 26 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES "SPEC_CTRL feature bit" + cpuid_spec_ctrl=1 + cpuid_ibrs='SPEC_CTRL' + fi + elif is_amd; then + read_cpuid 0x80000008 $EBX 14 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES "IBRS_SUPPORT feature bit" + cpuid_ibrs='IBRS_SUPPORT' + fi + else + ret=-1 + pstatus yellow UNKNOWN "unknown CPU" + fi + if [ $ret -eq 1 ]; then + pstatus yellow NO + elif [ $ret -eq 2 ]; then + pstatus yellow UNKNOWN "is cpuid kernel module available?" + cpuid_spec_ctrl=-1 + fi + + if is_amd; then + _info_nol " * CPU indicates preferring IBRS always-on: " + # amd + read_cpuid 0x80000008 $EBX 16 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES + else + pstatus yellow NO + fi + + _info_nol " * CPU indicates preferring IBRS over retpoline: " + # amd + read_cpuid 0x80000008 $EBX 18 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES + else + pstatus yellow NO + fi + fi + + # IBPB + _info " * Indirect Branch Prediction Barrier (IBPB)" + _info_nol " * PRED_CMD MSR is available: " + if [ ! -e /dev/cpu/0/msr ] && [ ! -e /dev/cpuctl0 ]; then + pstatus yellow UNKNOWN "is msr kernel module available?" + else + # the new MSR 'PRED_CTRL' is at offset 0x49, write-only + # here we use dd, it's the same as using 'wrmsr 0x49 0' but without needing the wrmsr tool + # if we get a write error, the MSR is not there + val=0 + cpu_mismatch=0 + for i in $(seq 0 "$idx_max_cpu") + do + write_msr 0x49 "$i"; ret=$? + if [ "$i" -eq 0 ]; then + val=$ret + else + if [ "$ret" -eq $val ]; then + continue + else + cpu_mismatch=1 + fi + fi + done + + if [ $val -eq 0 ]; then + if [ $cpu_mismatch -eq 0 ]; then + pstatus green YES + else + pstatus green YES "But not in all CPUs" + fi + elif [ $val -eq 200 ]; then + pstatus yellow UNKNOWN "is msr kernel module available?" + else + pstatus yellow NO + fi + fi + + _info_nol " * CPU indicates IBPB capability: " + # CPUID EAX=0x80000008, ECX=0x00 return EBX[12] indicates support for just IBPB. + if [ "$cpuid_spec_ctrl" = 1 ]; then + # spec_ctrl implies ibpb + cpuid_ibpb='SPEC_CTRL' + pstatus green YES "SPEC_CTRL feature bit" + elif is_intel; then + if [ "$cpuid_spec_ctrl" = -1 ]; then + pstatus yellow UNKNOWN "is cpuid kernel module available?" + else + pstatus yellow NO + fi + elif is_amd; then + read_cpuid 0x80000008 $EBX 12 1 1; ret=$? + if [ $ret -eq 0 ]; then + cpuid_ibpb='IBPB_SUPPORT' + pstatus green YES "IBPB_SUPPORT feature bit" + elif [ $ret -eq 1 ]; then + pstatus yellow NO + else + pstatus yellow UNKNOWN "is cpuid kernel module available?" + fi + fi + + # STIBP + _info " * Single Thread Indirect Branch Predictors (STIBP)" + _info_nol " * SPEC_CTRL MSR is available: " + if [ "$spec_ctrl_msr" = 1 ]; then + pstatus green YES + elif [ "$spec_ctrl_msr" = 0 ]; then + pstatus yellow NO + else + pstatus yellow UNKNOWN "is msr kernel module available?" + fi + + _info_nol " * CPU indicates STIBP capability: " + # intel: A processor supports STIBP if it enumerates CPUID (EAX=7H,ECX=0):EDX[27] as 1 + # amd: 8000_0008 EBX[15]=1 + if is_intel; then + read_cpuid 0x7 $EDX 27 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES "Intel STIBP feature bit" + #cpuid_stibp='Intel STIBP' + fi + elif is_amd; then + read_cpuid 0x80000008 $EBX 15 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES "AMD STIBP feature bit" + #cpuid_stibp='AMD STIBP' + fi + else + ret=-1 + pstatus yellow UNKNOWN "unknown CPU" + fi + if [ $ret -eq 1 ]; then + pstatus yellow NO + elif [ $ret -eq 2 ]; then + pstatus yellow UNKNOWN "is cpuid kernel module available?" + fi + + + if is_amd; then + _info_nol " * CPU indicates preferring STIBP always-on: " + read_cpuid 0x80000008 $EBX 17 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES + else + pstatus yellow NO + fi + fi + + if is_intel; then + _info " * Enhanced IBRS (IBRS_ALL)" + _info_nol " * CPU indicates ARCH_CAPABILITIES MSR availability: " + cpuid_arch_capabilities=-1 + # A processor supports the ARCH_CAPABILITIES MSR if it enumerates CPUID (EAX=7H,ECX=0):EDX[29] as 1 + read_cpuid 0x7 $EDX 29 1 1; ret=$? + if [ $ret -eq 0 ]; then + pstatus green YES + cpuid_arch_capabilities=1 + elif [ $ret -eq 2 ]; then + pstatus yellow UNKNOWN "is cpuid kernel module available?" + else + pstatus yellow NO + cpuid_arch_capabilities=0 + fi + + _info_nol " * ARCH_CAPABILITIES MSR advertises IBRS_ALL capability: " + capabilities_rdcl_no=-1 + capabilities_ibrs_all=-1 + if [ "$cpuid_arch_capabilities" = -1 ]; then + pstatus yellow UNKNOWN + elif [ "$cpuid_arch_capabilities" != 1 ]; then + capabilities_rdcl_no=0 + capabilities_ibrs_all=0 + pstatus yellow NO + elif [ ! -e /dev/cpu/0/msr ] && [ ! -e /dev/cpuctl0 ]; then + spec_ctrl_msr=-1 + pstatus yellow UNKNOWN "is msr kernel module available?" + else + # the new MSR 'ARCH_CAPABILITIES' is at offset 0x10a + # here we use dd, it's the same as using 'rdmsr 0x10a' but without needing the rdmsr tool + # if we get a read error, the MSR is not there. bs has to be 8 for msr + val=0 + val_cap_msr=0 + cpu_mismatch=0 + for i in $(seq 0 "$idx_max_cpu") + do + read_msr 0x10a "$i"; ret=$? + capabilities=$(echo "$read_msr_value" | awk '{print $8}') + if [ "$i" -eq 0 ]; then + val=$ret + val_cap_msr=$capabilities + else + if [ "$ret" -eq "$val" ] && [ "$capabilities" -eq "$val_cap_msr" ]; then + continue + else + cpu_mismatch=1 + fi + fi + done + capabilities=$val_cap_msr + capabilities_rdcl_no=0 + capabilities_ibrs_all=0 + if [ $val -eq 0 ]; then + _debug "capabilities MSR lower byte is $capabilities (decimal)" + [ $(( capabilities & 1 )) -eq 1 ] && capabilities_rdcl_no=1 + [ $(( capabilities & 2 )) -eq 2 ] && capabilities_ibrs_all=1 + _debug "capabilities says rdcl_no=$capabilities_rdcl_no ibrs_all=$capabilities_ibrs_all" + if [ "$capabilities_ibrs_all" = 1 ]; then + if [ $cpu_mismatch -eq 0 ]; then + pstatus green YES + else: + pstatus green YES "But not in all CPUs" + fi + else + pstatus yellow NO + fi + elif [ $val -eq 200 ]; then + pstatus yellow UNKNOWN "is msr kernel module available?" + else + pstatus yellow NO + fi + fi + + _info_nol " * CPU explicitly indicates not being vulnerable to Meltdown (RDCL_NO): " + if [ "$capabilities_rdcl_no" = -1 ]; then + pstatus yellow UNKNOWN + elif [ "$capabilities_rdcl_no" = 1 ]; then + pstatus green YES + else + pstatus yellow NO + fi + fi + + _info_nol " * CPU microcode is known to cause stability problems: " + if is_ucode_blacklisted; then + pstatus red YES "$ucode_found" + _warn + _warn "The microcode your CPU is running on is known to cause instability problems," + _warn "such as intempestive reboots or random crashes." + _warn "You are advised to either revert to a previous microcode version (that might not have" + _warn "the mitigations for Spectre), or upgrade to a newer one if available." + _warn + else + pstatus blue NO "$ucode_found" + fi +} + +check_cpu_vulnerabilities() +{ + _info "* CPU vulnerability to the three speculative execution attack variants" + for v in 1 2 3; do + _info_nol " * Vulnerable to Variant $v: " + if is_cpu_vulnerable $v; then + pstatus yellow YES + else + pstatus green NO + fi + done +} + +check_redhat_canonical_spectre() +{ + # if we were already called, don't do it again + [ -n "$redhat_canonical_spectre" ] && return + + if ! which "${opt_arch_prefix}strings" >/dev/null 2>&1; then + redhat_canonical_spectre=-1 + elif [ -n "$kernel_err" ]; then + redhat_canonical_spectre=-2 + else + # Red Hat / Ubuntu specific variant1 patch is difficult to detect, + # let's use the two same tricks than the official Red Hat detection script uses: + if "${opt_arch_prefix}strings" "$kernel" | grep -qw noibrs && "${opt_arch_prefix}strings" "$kernel" | grep -qw noibpb; then + # 1) detect their specific variant2 patch. If it's present, it means + # that the variant1 patch is also present (both were merged at the same time) + _debug "found redhat/canonical version of the variant2 patch (implies variant1)" + redhat_canonical_spectre=1 + elif "${opt_arch_prefix}strings" "$kernel" | grep -q 'x86/pti:'; then + # 2) detect their specific variant3 patch. If it's present, but the variant2 + # is not, it means that only variant1 is present in addition to variant3 + _debug "found redhat/canonical version of the variant3 patch (implies variant1 but not variant2)" + redhat_canonical_spectre=2 + else + redhat_canonical_spectre=0 + fi + fi +} + + ################### # SPECTRE VARIANT 1 check_variant1() { _info "\033[1;34mCVE-2017-5753 [bounds check bypass] aka 'Spectre Variant 1'\033[0m" + if [ "$os" = Linux ]; then + check_variant1_linux + elif echo "$os" | grep -q BSD; then + check_variant1_bsd + else + _warn "Unsupported OS ($os)" + fi +} +check_variant1_linux() +{ status=UNK sys_interface_available=0 msg='' if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v1"; then # this kernel has the /sys interface, trust it over everything + # v0.33+: don't. some kernels have backported the array_index_mask_nospec() workaround without + # modifying the vulnerabilities/spectre_v1 file. that's bad. we can't trust it when it says Vulnerable :( + # see "silent backport" detection at the bottom of this func sys_interface_available=1 - else + fi + if [ "$opt_sysfs_only" != 1 ]; then # no /sys interface (or offline mode), fallback to our own ways - _info_nol "* Checking count of LFENCE opcodes in kernel: " - if [ -n "$vmlinux_err" ]; then - msg="couldn't check ($vmlinux_err)" - status=UNK - pstatus yellow UNKNOWN + _info_nol "* Kernel has array_index_mask_nospec (x86): " + # vanilla: look for the Linus' mask aka array_index_mask_nospec() + # that is inlined at least in raw_copy_from_user (__get_user_X symbols) + #mov PER_CPU_VAR(current_task), %_ASM_DX + #cmp TASK_addr_limit(%_ASM_DX),%_ASM_AX + #jae bad_get_user + # /* array_index_mask_nospec() are the 2 opcodes that follow */ + #+sbb %_ASM_DX, %_ASM_DX + #+and %_ASM_DX, %_ASM_AX + #ASM_STAC + # x86 64bits: jae(0x0f 0x83 0x?? 0x?? 0x?? 0x??) sbb(0x48 0x19 0xd2) and(0x48 0x21 0xd0) + # x86 32bits: cmp(0x3b 0x82 0x?? 0x?? 0x00 0x00) jae(0x73 0x??) sbb(0x19 0xd2) and(0x21 0xd0) + if [ -n "$kernel_err" ]; then + pstatus yellow UNKNOWN "couldn't check ($kernel_err)" + elif ! which perl >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing 'perl' binary, please install it" else - if ! which objdump >/dev/null 2>&1; then - msg="missing 'objdump' tool, please install it, usually it's in the binutils package" - status=UNK - pstatus yellow UNKNOWN + perl -ne '/\x0f\x83....\x48\x19\xd2\x48\x21\xd0/ and $found++; END { exit($found) }' "$kernel"; ret=$? + if [ $ret -gt 0 ]; then + pstatus green YES "$ret occurrence(s) found of 64 bits array_index_mask_nospec()" + v1_mask_nospec="64 bits array_index_mask_nospec" else - # here we disassemble the kernel and count the number of occurrences of the LFENCE opcode - # in non-patched kernels, this has been empirically determined as being around 40-50 - # in patched kernels, this is more around 70-80, sometimes way higher (100+) - # v0.13: 68 found in a 3.10.23-xxxx-std-ipv6-64 (with lots of modules compiled-in directly), which doesn't have the LFENCE patches, - # so let's push the threshold to 70. - nb_lfence=$(objdump -d "$vmlinux" | grep -wc lfence) - if [ "$nb_lfence" -lt 70 ]; then - msg="only $nb_lfence opcodes found, should be >= 70, heuristic to be improved when official patches become available" - status=VULN - pstatus red NO + perl -ne '/\x3b\x82..\x00\x00\x73.\x19\xd2\x21\xd0/ and $found++; END { exit($found) }' "$kernel"; ret=$? + if [ $ret -gt 0 ]; then + pstatus green YES "$ret occurrence(s) found of 32 bits array_index_mask_nospec()" + v1_mask_nospec="32 bits array_index_mask_nospec" else - msg="$nb_lfence opcodes found, which is >= 70, heuristic to be improved when official patches become available" - status=OK - pstatus green YES + pstatus yellow NO fi fi fi - fi - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 1; then - # override status & msg in case CPU is not vulnerable after all - msg="your CPU vendor reported your CPU model as not vulnerable" - status=OK + _info_nol "* Kernel has the Red Hat/Ubuntu patch: " + check_redhat_canonical_spectre + if [ "$redhat_canonical_spectre" = -1 ]; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}strings' tool, please install it, usually it's in the binutils package" + elif [ "$redhat_canonical_spectre" = -2 ]; then + pstatus yellow UNKNOWN "couldn't check ($kernel_err)" + elif [ "$redhat_canonical_spectre" = 1 ]; then + pstatus green YES + elif [ "$redhat_canonical_spectre" = 2 ]; then + pstatus green YES "but without IBRS" + else + pstatus yellow NO + fi + + _info_nol "* Kernel has mask_nospec64 (arm): " + #.macro mask_nospec64, idx, limit, tmp + #sub \tmp, \idx, \limit + #bic \tmp, \tmp, \idx + #and \idx, \idx, \tmp, asr #63 + #csdb + #.endm + #$ aarch64-linux-gnu-objdump -d vmlinux | grep -w bic -A1 -B1 | grep -w sub -A2 | grep -w and -B2 + #ffffff8008082e44: cb190353 sub x19, x26, x25 + #ffffff8008082e48: 8a3a0273 bic x19, x19, x26 + #ffffff8008082e4c: 8a93ff5a and x26, x26, x19, asr #63 + #ffffff8008082e50: d503229f hint #0x14 + # /!\ can also just be "csdb" instead of "hint #0x14" for native objdump + # + # if we have v1_mask_nospec or redhat_canonical_spectre>0, don't bother disassembling the kernel, the answer is no. + if [ -n "$v1_mask_nospec" ] || [ "$redhat_canonical_spectre" -gt 0 ]; then + pstatus yellow NO + elif [ -n "$kernel_err" ]; then + pstatus yellow UNKNOWN "couldn't check ($kernel_err)" + elif ! which perl >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing 'perl' binary, please install it" + elif ! which "${opt_arch_prefix}objdump" >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}objdump' tool, please install it, usually it's in the binutils package" + else + "${opt_arch_prefix}objdump" -d "$kernel" | perl -ne 'push @r, $_; /\s(hint|csdb)\s/ && $r[0]=~/\ssub\s+(x\d+)/ && $r[1]=~/\sbic\s+$1,\s+$1,/ && $r[2]=~/\sand\s/ && exit(9); shift @r if @r>3'; ret=$? + if [ "$ret" -eq 9 ]; then + pstatus green YES "mask_nospec64 macro is present and used" + v1_mask_nospec="arm mask_nospec64" + else + pstatus yellow NO + fi + fi + + + if [ "$opt_verbose" -ge 2 ] || ( [ -z "$v1_mask_nospec" ] && [ "$redhat_canonical_spectre" != 1 ] && [ "$redhat_canonical_spectre" != 2 ] ); then + # this is a slow heuristic and we don't need it if we already know the kernel is patched + # but still show it in verbose mode + _info_nol "* Checking count of LFENCE instructions following a jump in kernel... " + if [ -n "$kernel_err" ]; then + pstatus yellow UNKNOWN "couldn't check ($kernel_err)" + else + if ! which "${opt_arch_prefix}objdump" >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}objdump' tool, please install it, usually it's in the binutils package" + else + # here we disassemble the kernel and count the number of occurrences of the LFENCE opcode + # in non-patched kernels, this has been empirically determined as being around 40-50 + # in patched kernels, this is more around 70-80, sometimes way higher (100+) + # v0.13: 68 found in a 3.10.23-xxxx-std-ipv6-64 (with lots of modules compiled-in directly), which doesn't have the LFENCE patches, + # so let's push the threshold to 70. + # v0.33+: now only count lfence opcodes after a jump, way less error-prone + # non patched kernel have between 0 and 20 matches, patched ones have at least 40-45 + nb_lfence=$("${opt_arch_prefix}objdump" -d "$kernel" 2>/dev/null | grep -w -B1 lfence | grep -Ewc 'jmp|jne|je') + if [ "$nb_lfence" -lt 30 ]; then + pstatus yellow NO "only $nb_lfence jump-then-lfence instructions found, should be >= 30 (heuristic)" + else + v1_lfence=1 + pstatus green YES "$nb_lfence jump-then-lfence instructions found, which is >= 30 (heuristic)" + fi + fi + fi + fi + + else + # we have no sysfs but were asked to use it only! + msg="/sys vulnerability interface use forced, but it's not available!" + status=UNK fi # report status - pvulnstatus CVE-2017-5753 "$status" "$msg" + cve='CVE-2017-5753' + + if ! is_cpu_vulnerable 1; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ -z "$msg" ]; then + # if msg is empty, sysfs check didn't fill it, rely on our own test + if [ -n "$v1_mask_nospec" ]; then + pvulnstatus $cve OK "Kernel source has been patched to mitigate the vulnerability ($v1_mask_nospec)" + elif [ "$redhat_canonical_spectre" = 1 ] || [ "$redhat_canonical_spectre" = 2 ]; then + pvulnstatus $cve OK "Kernel source has been patched to mitigate the vulnerability (Red Hat/Ubuntu patch)" + elif [ "$v1_lfence" = 1 ]; then + pvulnstatus $cve OK "Kernel source has PROBABLY been patched to mitigate the vulnerability (jump-then-lfence instructions heuristic)" + elif [ "$kernel_err" ]; then + pvulnstatus $cve UNK "Couldn't find kernel image or tools missing to execute the checks" + explain "Re-run this script with root privileges, after installing the missing tools indicated above" + else + pvulnstatus $cve VULN "Kernel source needs to be patched to mitigate the vulnerability" + explain "Your kernel is too old to have the mitigation for Variant 1, you should upgrade to a newer kernel. If you're using a Linux distro and didn't compile the kernel yourself, you should upgrade your distro to get a newer kernel." + fi + else + if [ "$msg" = "Vulnerable" ] && [ -n "$v1_mask_nospec" ]; then + pvulnstatus $cve OK "Kernel source has been patched to mitigate the vulnerability (silent backport of array_index_mask_nospec)" + else + if [ "$msg" = "Vulnerable" ]; then + msg="Kernel source needs to be patched to mitigate the vulnerability" + _explain="Your kernel is too old to have the mitigation for Variant 1, you should upgrade to a newer kernel. If you're using a Linux distro and didn't compile the kernel yourself, you should upgrade your distro to get a newer kernel." + fi + pvulnstatus $cve "$status" "$msg" + [ -n "$_explain" ] && explain "$_explain" + unset _explain + fi + fi } +check_variant1_bsd() +{ + cve='CVE-2017-5753' + if ! is_cpu_vulnerable 1; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + else + pvulnstatus $cve VULN "no mitigation for BSD yet" + fi +} + + ################### # SPECTRE VARIANT 2 check_variant2() { _info "\033[1;34mCVE-2017-5715 [branch target injection] aka 'Spectre Variant 2'\033[0m" + if [ "$os" = Linux ]; then + check_variant2_linux + elif echo "$os" | grep -q BSD; then + check_variant2_bsd + else + _warn "Unsupported OS ($os)" + fi +} +check_variant2_linux() +{ status=UNK sys_interface_available=0 msg='' if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then # this kernel has the /sys interface, trust it over everything sys_interface_available=1 - else + fi + if [ "$opt_sysfs_only" != 1 ]; then _info "* Mitigation 1" - _info_nol "* Hardware (CPU microcode) support for mitigation: " - if [ ! -e /dev/cpu/0/msr ]; then - # try to load the module ourselves (and remember it so we can rmmod it afterwards) - modprobe msr 2>/dev/null && insmod_msr=1 - _debug "attempted to load module msr, ret=$insmod_msr" - fi - if [ ! -e /dev/cpu/0/msr ]; then - pstatus yellow UNKNOWN "couldn't read /dev/cpu/0/msr, is msr support enabled in your kernel?" - else - # the new MSR 'SPEC_CTRL' is at offset 0x48 - # here we use dd, it's the same as using 'rdmsr 0x48' but without needing the rdmsr tool - # if we get a read error, the MSR is not there - dd if=/dev/cpu/0/msr of=/dev/null bs=8 count=1 skip=9 2>/dev/null - if [ $? -eq 0 ]; then - pstatus green YES - else - pstatus red NO - fi - fi - if [ "$insmod_msr" = 1 ]; then - # if we used modprobe ourselves, rmmod the module - rmmod msr 2>/dev/null - _debug "attempted to unload module msr, ret=$?" - fi + ibrs_can_tell=0 + ibrs_supported='' + ibrs_enabled='' + ibpb_can_tell=0 + ibpb_supported='' + ibpb_enabled='' - _info_nol "* Kernel support for IBRS: " if [ "$opt_live" = 1 ]; then + # in live mode, we can check for the ibrs_enabled file in debugfs + # all versions of the patches have it (NOT the case of IBPB or KPTI) + ibrs_can_tell=1 mount_debugfs - for ibrs_file in \ - /sys/kernel/debug/ibrs_enabled \ - /sys/kernel/debug/x86/ibrs_enabled \ - /proc/sys/kernel/ibrs_enabled; do - if [ -e "$ibrs_file" ]; then + for dir in \ + /sys/kernel/debug \ + /sys/kernel/debug/x86 \ + /proc/sys/kernel; do + if [ -e "$dir/ibrs_enabled" ]; then # if the file is there, we have IBRS compiled-in # /sys/kernel/debug/ibrs_enabled: vanilla - # /sys/kernel/debug/x86/ibrs_enabled: RedHat (see https://access.redhat.com/articles/3311301) + # /sys/kernel/debug/x86/ibrs_enabled: Red Hat (see https://access.redhat.com/articles/3311301) # /proc/sys/kernel/ibrs_enabled: OpenSUSE tumbleweed - pstatus green YES - ibrs_supported=1 - ibrs_enabled=$(cat "$ibrs_file" 2>/dev/null) - _debug "ibrs: found $ibrs_file=$ibrs_enabled" + specex_knob_dir=$dir + ibrs_supported="$dir/ibrs_enabled exists" + ibrs_enabled=$(cat "$dir/ibrs_enabled" 2>/dev/null) + _debug "ibrs: found $dir/ibrs_enabled=$ibrs_enabled" + # if ibrs_enabled is there, ibpb_enabled will be in the same dir + if [ -e "$dir/ibpb_enabled" ]; then + # if the file is there, we have IBPB compiled-in (see note above for IBRS) + ibpb_supported="$dir/ibpb_enabled exists" + ibpb_enabled=$(cat "$dir/ibpb_enabled" 2>/dev/null) + _debug "ibpb: found $dir/ibpb_enabled=$ibpb_enabled" + else + _debug "ibpb: $dir/ibpb_enabled file doesn't exist" + fi break else - _debug "ibrs: file $ibrs_file doesn't exist" + _debug "ibrs: $dir/ibrs_enabled file doesn't exist" fi done + # on some newer kernels, the spec_ctrl_ibrs flag in "$procfs/cpuinfo" + # is set when ibrs has been administratively enabled (usually from cmdline) + # which in that case means ibrs is supported *and* enabled for kernel & user + # as per the ibrs patch series v3 + if [ -z "$ibrs_supported" ]; then + if grep ^flags "$procfs/cpuinfo" | grep -qw spec_ctrl_ibrs; then + _debug "ibrs: found spec_ctrl_ibrs flag in $procfs/cpuinfo" + ibrs_supported="spec_ctrl_ibrs flag in $procfs/cpuinfo" + # enabled=2 -> kernel & user + ibrs_enabled=2 + # XXX and what about ibpb ? + fi + fi + if [ -e "/sys/devices/system/cpu/vulnerabilities/spectre_v2" ]; then + # when IBPB is enabled on 4.15+, we can see it in sysfs + if grep -q ', IBPB' "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then + _debug "ibpb: found enabled in sysfs" + [ -z "$ibpb_supported" ] && ibpb_supported='IBPB found enabled in sysfs' + [ -z "$ibpb_enabled" ] && ibpb_enabled=1 + fi + # when IBRS_FW is enabled on 4.15+, we can see it in sysfs + if grep -q ', IBRS_FW' "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then + _debug "ibrs: found IBRS_FW in sysfs" + [ -z "$ibrs_supported" ] && ibrs_supported='found IBRS_FW in sysfs' + ibrs_fw_enabled=1 + fi + # when IBRS is enabled on 4.15+, we can see it in sysfs + if grep -q 'Indirect Branch Restricted Speculation' "/sys/devices/system/cpu/vulnerabilities/spectre_v2"; then + _debug "ibrs: found IBRS in sysfs" + [ -z "$ibrs_supported" ] && ibrs_supported='found IBRS in sysfs' + [ -z "$ibrs_enabled" ] && ibrs_enabled=3 + fi + fi + # in live mode, if ibrs or ibpb is supported and we didn't find these are enabled, then they are not + [ -n "$ibrs_supported" ] && [ -z "$ibrs_enabled" ] && ibrs_enabled=0 + [ -n "$ibpb_supported" ] && [ -z "$ibpb_enabled" ] && ibpb_enabled=0 fi - if [ "$ibrs_supported" != 1 -a -n "$opt_map" ]; then + if [ -z "$ibrs_supported" ]; then + check_redhat_canonical_spectre + if [ "$redhat_canonical_spectre" = 1 ]; then + ibrs_supported="Red Hat/Ubuntu variant" + ibpb_supported="Red Hat/Ubuntu variant" + fi + fi + if [ -z "$ibrs_supported" ] && [ -n "$kernel" ]; then + if ! which "${opt_arch_prefix}strings" >/dev/null 2>&1; then + : + else + ibrs_can_tell=1 + ibrs_supported=$("${opt_arch_prefix}strings" "$kernel" | grep -Fw -e ', IBRS_FW' | head -1) + if [ -n "$ibrs_supported" ]; then + _debug "ibrs: found ibrs evidence in kernel image ($ibrs_supported)" + ibrs_supported="found '$ibrs_supported' in kernel image" + fi + fi + fi + if [ -z "$ibrs_supported" ] && [ -n "$opt_map" ]; then + ibrs_can_tell=1 if grep -q spec_ctrl "$opt_map"; then - pstatus green YES - ibrs_supported=1 + ibrs_supported="found spec_ctrl in symbols file" _debug "ibrs: found '*spec_ctrl*' symbol in $opt_map" fi fi - if [ "$ibrs_supported" != 1 ]; then - pstatus red NO + # recent (4.15) vanilla kernels have IBPB but not IBRS, and without the debugfs tunables of Red Hat + # we can detect it directly in the image + if [ -z "$ibpb_supported" ] && [ -n "$kernel" ]; then + if ! which "${opt_arch_prefix}strings" >/dev/null 2>&1; then + : + else + ibpb_can_tell=1 + ibpb_supported=$("${opt_arch_prefix}strings" "$kernel" | grep -Fw -e 'ibpb' -e ', IBPB' | head -1) + if [ -n "$ibpb_supported" ]; then + _debug "ibpb: found ibpb evidence in kernel image ($ibpb_supported)" + ibpb_supported="found '$ibpb_supported' in kernel image" + fi + fi fi - _info_nol "* IBRS enabled for Kernel space: " + _info_nol " * Kernel is compiled with IBRS support: " + if [ -z "$ibrs_supported" ]; then + if [ "$ibrs_can_tell" = 1 ]; then + pstatus yellow NO + else + # if we're in offline mode without System.map, we can't really know + pstatus yellow UNKNOWN "in offline mode, we need the kernel image and System.map to be able to tell" + fi + else + if [ "$opt_verbose" -ge 2 ]; then + pstatus green YES "$ibrs_supported" + else + pstatus green YES + fi + fi + + _info_nol " * IBRS enabled and active: " if [ "$opt_live" = 1 ]; then - # 0 means disabled - # 1 is enabled only for kernel space - # 2 is enabled for kernel and user space - case "$ibrs_enabled" in - "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; - 0) pstatus red NO;; - 1 | 2) pstatus green YES;; - *) pstatus yellow UNKNOWN;; - esac + if [ "$ibpb_enabled" = 2 ]; then + # if ibpb=2, ibrs is forcefully=0 + pstatus blue NO "IBPB used instead of IBRS in all kernel entrypoints" + else + # 0 means disabled + # 1 is enabled only for kernel space + # 2 is enabled for kernel and user space + # 3 is enabled + case "$ibrs_enabled" in + 0) + if [ "$ibrs_fw_enabled" = 1 ]; then + pstatus blue YES "for firmware code only" + else + pstatus yellow NO + fi + ;; + 1) if [ "$ibrs_fw_enabled" = 1 ]; then pstatus green YES "for kernel space and firmware code"; else pstatus green YES "for kernel space"; fi;; + 2) if [ "$ibrs_fw_enabled" = 1 ]; then pstatus green YES "for kernel, user space, and firmware code" ; else pstatus green YES "for both kernel and user space"; fi;; + 3) if [ "$ibrs_fw_enabled" = 1 ]; then pstatus green YES "for kernel and firmware code"; else pstatus green YES; fi;; + *) pstatus yellow UNKNOWN;; + esac + fi else pstatus blue N/A "not testable in offline mode" fi - _info_nol "* IBRS enabled for User space: " + _info_nol " * Kernel is compiled with IBPB support: " + if [ -z "$ibpb_supported" ]; then + if [ "$ibpb_can_tell" = 1 ]; then + pstatus yellow NO + else + # if we're in offline mode without System.map, we can't really know + pstatus yellow UNKNOWN "in offline mode, we need the kernel image to be able to tell" + fi + else + if [ "$opt_verbose" -ge 2 ]; then + pstatus green YES "$ibpb_supported" + else + pstatus green YES + fi + fi + + _info_nol " * IBPB enabled and active: " if [ "$opt_live" = 1 ]; then - case "$ibrs_enabled" in - "") [ "$ibrs_supported" = 1 ] && pstatus yellow UNKNOWN || pstatus red NO;; - 0 | 1) pstatus red NO;; - 2) pstatus green YES;; + case "$ibpb_enabled" in + "") + if [ "$ibrs_supported" = 1 ]; then + pstatus yellow UNKNOWN + else + pstatus yellow NO + fi + ;; + 0) + pstatus yellow NO + ;; + 1) pstatus green YES;; + 2) pstatus green YES "IBPB used instead of IBRS in all kernel entrypoints";; *) pstatus yellow UNKNOWN;; esac else @@ -748,180 +2174,539 @@ check_variant2() fi _info "* Mitigation 2" - _info_nol "* Kernel compiled with retpoline option: " + _info_nol " * Kernel has branch predictor hardening (arm): " + if [ -r "$opt_config" ]; then + bp_harden_can_tell=1 + bp_harden=$(grep -w 'CONFIG_HARDEN_BRANCH_PREDICTOR=y' "$opt_config") + if [ -n "$bp_harden" ]; then + pstatus green YES + _debug "bp_harden: found '$bp_harden' in $opt_config" + fi + fi + if [ -z "$bp_harden" ] && [ -n "$opt_map" ]; then + bp_harden_can_tell=1 + bp_harden=$(grep -w bp_hardening_data "$opt_map") + if [ -n "$bp_harden" ]; then + pstatus green YES + _debug "bp_harden: found '$bp_harden' in $opt_map" + fi + fi + if [ -z "$bp_harden" ]; then + if [ "$bp_harden_can_tell" = 1 ]; then + pstatus yellow NO + else + pstatus yellow UNKNOWN + fi + fi + + _info_nol " * Kernel compiled with retpoline option: " # We check the RETPOLINE kernel options if [ -r "$opt_config" ]; then if grep -q '^CONFIG_RETPOLINE=y' "$opt_config"; then pstatus green YES retpoline=1 - _debug "retpoline: found "$(grep '^CONFIG_RETPOLINE' "$opt_config")" in $opt_config" + # shellcheck disable=SC2046 + _debug 'retpoline: found '$(grep '^CONFIG_RETPOLINE' "$opt_config")" in $opt_config" else - pstatus red NO + pstatus yellow NO fi else pstatus yellow UNKNOWN "couldn't read your kernel configuration" fi - _info_nol "* Kernel compiled with a retpoline-aware compiler: " - # Now check if the compiler used to compile the kernel knows how to insert retpolines in generated asm - # For gcc, this is -mindirect-branch=thunk-extern (detected by the kernel makefiles) - # See gcc commit https://github.com/hjl-tools/gcc/commit/23b517d4a67c02d3ef80b6109218f2aadad7bd79 - # In latest retpoline LKML patches, the noretpoline_setup symbol exists only if CONFIG_RETPOLINE is set - # *AND* if the compiler is retpoline-compliant, so look for that symbol - if [ -n "$opt_map" ]; then - # look for the symbol - if grep -qw noretpoline_setup "$opt_map"; then - retpoline_compiler=1 - pstatus green YES "noretpoline_setup symbol found in System.map" - else - pstatus red NO - fi - elif [ -n "$vmlinux" ]; then - # look for the symbol - if which nm >/dev/null 2>&1; then - # the proper way: use nm and look for the symbol - if nm "$vmlinux" 2>/dev/null | grep -qw 'noretpoline_setup'; then - retpoline_compiler=1 - pstatus green YES "noretpoline_setup found in vmlinux symbols" - else - pstatus red NO + if [ "$retpoline" = 1 ]; then + # Now check if the compiler used to compile the kernel knows how to insert retpolines in generated asm + # For gcc, this is -mindirect-branch=thunk-extern (detected by the kernel makefiles) + # See gcc commit https://github.com/hjl-tools/gcc/commit/23b517d4a67c02d3ef80b6109218f2aadad7bd79 + # In latest retpoline LKML patches, the noretpoline_setup symbol exists only if CONFIG_RETPOLINE is set + # *AND* if the compiler is retpoline-compliant, so look for that symbol + # + # if there is "retpoline" in the file and NOT "minimal", then it's full retpoline + # (works for vanilla and Red Hat variants) + if [ "$opt_live" = 1 ] && [ -e "/sys/devices/system/cpu/vulnerabilities/spectre_v2" ]; then + if grep -qwi retpoline /sys/devices/system/cpu/vulnerabilities/spectre_v2; then + if grep -qwi minimal /sys/devices/system/cpu/vulnerabilities/spectre_v2; then + retpoline_compiler=0 + retpoline_compiler_reason="kernel reports minimal retpoline compilation" + else + retpoline_compiler=1 + retpoline_compiler_reason="kernel reports full retpoline compilation" + fi + fi + elif [ -n "$opt_map" ]; then + # look for the symbol + if grep -qw noretpoline_setup "$opt_map"; then + retpoline_compiler=1 + retpoline_compiler_reason="noretpoline_setup symbol found in System.map" + fi + elif [ -n "$kernel" ]; then + # look for the symbol + if which "${opt_arch_prefix}nm" >/dev/null 2>&1; then + # the proper way: use nm and look for the symbol + if "${opt_arch_prefix}nm" "$kernel" 2>/dev/null | grep -qw 'noretpoline_setup'; then + retpoline_compiler=1 + retpoline_compiler_reason="noretpoline_setup found in kernel symbols" + fi + elif grep -q noretpoline_setup "$kernel"; then + # if we don't have nm, nevermind, the symbol name is long enough to not have + # any false positive using good old grep directly on the binary + retpoline_compiler=1 + retpoline_compiler_reason="noretpoline_setup found in kernel" fi - elif grep -q noretpoline_setup "$vmlinux"; then - # if we don't have nm, nevermind, the symbol name is long enough to not have - # any false positive using good old grep directly on the binary - retpoline_compiler=1 - pstatus green YES "noretpoline_setup found in vmlinux" - else - pstatus red NO fi + if [ -n "$retpoline_compiler" ]; then + _info_nol " * Kernel compiled with a retpoline-aware compiler: " + if [ "$retpoline_compiler" = 1 ]; then + if [ -n "$retpoline_compiler_reason" ]; then + pstatus green YES "$retpoline_compiler_reason" + else + pstatus green YES + fi + else + if [ -n "$retpoline_compiler_reason" ]; then + pstatus red NO "$retpoline_compiler_reason" + else + pstatus red NO + fi + fi + fi + fi + + # only Red Hat has a tunable to disable it on runtime + if [ "$opt_live" = 1 ]; then + if [ -e "$specex_knob_dir/retp_enabled" ]; then + retp_enabled=$(cat "$specex_knob_dir/retp_enabled" 2>/dev/null) + _debug "retpoline: found $specex_knob_dir/retp_enabled=$retp_enabled" + _info_nol " * Retpoline is enabled: " + if [ "$retp_enabled" = 1 ]; then + pstatus green YES + else + pstatus yellow NO + fi + fi + fi + + # only for information, in verbose mode + if [ "$opt_verbose" -ge 2 ]; then + _info_nol " * Local gcc is retpoline-aware: " + if which gcc >/dev/null 2>&1; then + if [ -n "$(gcc -mindirect-branch=thunk-extern --version 2>&1 >/dev/null)" ]; then + pstatus blue NO + else + pstatus green YES + fi + else + pstatus blue NO "gcc is not installed" + fi + fi + + if is_skylake_cpu || [ "$opt_verbose" -ge 2 ]; then + _info_nol " * Kernel supports RSB filling: " + if ! which "${opt_arch_prefix}strings" >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}strings' tool, please install it, usually it's in the binutils package" + elif [ -z "$kernel" ]; then + pstatus yellow UNKNOWN "kernel image missing" + else + rsb_filling=$("${opt_arch_prefix}strings" "$kernel" | grep -w 'Filling RSB on context switch') + if [ -n "$rsb_filling" ]; then + pstatus green YES + else + pstatus yellow NO + fi + fi + fi + + elif [ "$sys_interface_available" = 0 ]; then + # we have no sysfs but were asked to use it only! + msg="/sys vulnerability interface use forced, but it's not available!" + status=UNK + fi + + cve='CVE-2017-5715' + if ! is_cpu_vulnerable 2; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + else + if [ "$retpoline" = 1 ] && [ "$retpoline_compiler" = 1 ] && [ "$retp_enabled" != 0 ] && [ -n "$ibpb_enabled" ] && [ "$ibpb_enabled" -ge 1 ] && ( ! is_skylake_cpu || [ -n "$rsb_filling" ] ); then + pvulnstatus $cve OK "Full retpoline + IBPB are mitigating the vulnerability" + elif [ "$retpoline" = 1 ] && [ "$retpoline_compiler" = 1 ] && [ "$retp_enabled" != 0 ] && [ "$opt_paranoid" = 0 ] && ( ! is_skylake_cpu || [ -n "$rsb_filling" ] ); then + pvulnstatus $cve OK "Full retpoline is mitigating the vulnerability" + if [ -n "$cpuid_ibpb" ]; then + _warn "You should enable IBPB to complete retpoline as a Variant 2 mitigation" + else + _warn "IBPB is considered as a good addition to retpoline for Variant 2 mitigation, but your CPU microcode doesn't support it" + fi + elif [ -n "$ibrs_enabled" ] && [ -n "$ibpb_enabled" ] && [ "$ibrs_enabled" -ge 1 ] && [ "$ibpb_enabled" -ge 1 ]; then + pvulnstatus $cve OK "IBRS + IBPB are mitigating the vulnerability" + elif [ "$ibpb_enabled" = 2 ] && ! is_cpu_smt_enabled; then + pvulnstatus $cve OK "Full IBPB is mitigating the vulnerability" + elif [ -n "$bp_harden" ]; then + pvulnstatus $cve OK "Branch predictor hardening mitigates the vulnerability" + elif [ -z "$bp_harden" ] && [ "$cpu_vendor" = ARM ]; then + pvulnstatus $cve VULN "Branch predictor hardening is needed to mitigate the vulnerability" + explain "Your kernel has not been compiled with the CONFIG_UNMAP_KERNEL_AT_EL0 option, recompile it with this option enabled." + elif [ "$opt_live" != 1 ]; then + if [ "$retpoline" = 1 ] && [ -n "$ibpb_supported" ]; then + pvulnstatus $cve OK "offline mode: kernel supports retpoline + IBPB to mitigate the vulnerability" + elif [ -n "$ibrs_supported" ] && [ -n "$ibpb_supported" ]; then + pvulnstatus $cve OK "offline mode: kernel supports IBRS + IBPB to mitigate the vulnerability" + elif [ "$ibrs_can_tell" != 1 ]; then + pvulnstatus $cve UNK "offline mode: not enough information" + explain "Re-run this script with root privileges, and give it the kernel image (--kernel), the kernel configuration (--config) and the System.map file (--map) corresponding to the kernel you would like to inspect." + fi + fi + + # if we arrive here and didn't already call pvulnstatus, then it's VULN, let's explain why + if [ "$pvulnstatus_last_cve" != "$cve" ]; then + # explain what's needed for this CPU + if is_skylake_cpu; then + pvulnstatus $cve VULN "IBRS+IBPB or retpoline+IBPB+RBS filling, is needed to mitigate the vulnerability" + explain "To mitigate this vulnerability, you need either IBRS + IBPB, both requiring hardware support from your CPU microcode in addition to kernel support, or a kernel compiled with retpoline and IBPB, with retpoline requiring a retpoline-aware compiler (re-run this script with -v to know if your version of gcc is retpoline-aware) and IBPB requiring hardware support from your CPU microcode. You also need a recent-enough kernel that supports RSB filling if you plan to use retpoline. For Skylake+ CPUs, the IBRS + IBPB approach is generally preferred as it guarantees complete protection, and the performance impact is not as high as with older CPUs in comparison with retpoline. More information about how to enable the missing bits for those two possible mitigations on your system follow. You only need to take one of the two approaches." + elif is_zen_cpu; then + pvulnstatus $cve VULN "retpoline+IBPB is needed to mitigate the vulnerability" + explain "To mitigate this vulnerability, You need a kernel compiled with retpoline + IBPB support, with retpoline requiring a retpoline-aware compiler (re-run this script with -v to know if your version of gcc is retpoline-aware) and IBPB requiring hardware support from your CPU microcode." + elif is_intel || is_amd; then + pvulnstatus $cve VULN "IBRS+IBPB or retpoline+IBPB is needed to mitigate the vulnerability" + explain "To mitigate this vulnerability, you need either IBRS + IBPB, both requiring hardware support from your CPU microcode in addition to kernel support, or a kernel compiled with retpoline and IBPB, with retpoline requiring a retpoline-aware compiler (re-run this script with -v to know if your version of gcc is retpoline-aware) and IBPB requiring hardware support from your CPU microcode. The retpoline + IBPB approach is generally preferred as the performance impact is lower. More information about how to enable the missing bits for those two possible mitigations on your system follow. You only need to take one of the two approaches." + else + # in that case, we might want to trust sysfs if it's there + if [ -n "$msg" ]; then + [ "$msg" = Vulnerable ] && msg="no known mitigation exists for your CPU vendor ($cpu_vendor)" + pvulnstatus $cve $status "$msg" + else + pvulnstatus $cve VULN "no known mitigation exists for your CPU vendor ($cpu_vendor)" + fi + fi + fi + + # if we are in live mode, we can check for a lot more stuff and explain further + if [ "$opt_live" = 1 ] && [ "$vulnstatus" != "OK" ]; then + _explain_hypervisor="An updated CPU microcode will have IBRS/IBPB capabilities indicated in the Hardware Check section above. If you're running under an hypervisor (KVM, Xen, VirtualBox, VMware, ...), the hypervisor needs to be up to date to be able to export the new host CPU flags to the guest. You can run this script on the host to check if the host CPU is IBRS/IBPB. If it is, and it doesn't show up in the guest, upgrade the hypervisor." + # IBPB (amd & intel) + if ( [ -z "$ibpb_enabled" ] || [ "$ibpb_enabled" = 0 ] ) && ( is_intel || is_amd ); then + if [ -z "$cpuid_ibpb" ]; then + explain "The microcode of your CPU needs to be upgraded to be able to use IBPB. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section). $_explain_hypervisor" + fi + if [ -z "$ibpb_supported" ]; then + explain "Your kernel doesn't have IBPB support, so you need to either upgrade your kernel (if you're using a distro) or recompiling a more recent kernel." + fi + if [ -n "$cpuid_ibpb" ] && [ -n "$ibpb_supported" ]; then + if [ -e "$specex_knob_dir/ibpb_enabled" ]; then + # newer (April 2018) Red Hat kernels have ibpb_enabled as ro, and automatically enables it with retpoline + if [ ! -w "$specex_knob_dir/ibpb_enabled" ] && [ -e "$specex_knob_dir/retp_enabled" ]; then + explain "Both your CPU and your kernel have IBPB support, but it is currently disabled. You kernel should enable IBPB automatically if you enable retpoline. You may enable it with \`echo 1 > $specex_knob_dir/retp_enabled\`." + else + explain "Both your CPU and your kernel have IBPB support, but it is currently disabled. You may enable it with \`echo 1 > $specex_knob_dir/ibpb_enabled\`." + fi + else + explain "Both your CPU and your kernel have IBPB support, but it is currently disabled. You may enable it. Check in your distro's documentation on how to do this." + fi + fi + elif [ "$ibpb_enabled" = 2 ] && is_cpu_smt_enabled; then + explain "You have ibpb_enabled set to 2, but it only offers sufficient protection when simultaneous multi-threading (aka SMT or HyperThreading) is disabled. You should reboot your system with the kernel parameter \`nosmt\`." + fi + # /IBPB + + # IBRS (amd & intel) + if ( [ -z "$ibrs_enabled" ] || [ "$ibrs_enabled" = 0 ] ) && ( is_intel || is_amd ); then + if [ -z "$cpuid_ibrs" ]; then + explain "The microcode of your CPU needs to be upgraded to be able to use IBRS. This is usually done at boot time by your kernel (the upgrade is not persistent across reboots which is why it's done at each boot). If you're using a distro, make sure you are up to date, as microcode updates are usually shipped alongside with the distro kernel. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section). $_explain_hypervisor" + fi + if [ -z "$ibrs_supported" ]; then + explain "Your kernel doesn't have IBRS support, so you need to either upgrade your kernel (if you're using a distro) or recompiling a more recent kernel." + fi + if [ -n "$cpuid_ibrs" ] && [ -n "$ibrs_supported" ]; then + if [ -e "$specex_knob_dir/ibrs_enabled" ]; then + explain "Both your CPU and your kernel have IBRS support, but it is currently disabled. You may enable it with \`echo 1 > $specex_knob_dir/ibrs_enabled\`." + else + explain "Both your CPU and your kernel have IBRS support, but it is currently disabled. You may enable it. Check in your distro's documentation on how to do this." + fi + fi + fi + # /IBRS + unset _explain_hypervisor + + # RETPOLINE (amd & intel) + if is_amd || is_intel; then + if [ "$retpoline" = 0 ]; then + explain "Your kernel is not compiled with retpoline support, so you need to either upgrade your kernel (if you're using a distro) or recompile your kernel with the CONFIG_RETPOLINE option enabled. You also need to compile your kernel with a retpoline-aware compiler (re-run this script with -v to know if your version of gcc is retpoline-aware)." + elif [ "$retpoline" = 1 ] && [ "$retpoline_compiler" = 0 ]; then + explain "Your kernel is compiled with retpoline, but without a retpoline-aware compiler (re-run this script with -v to know if your version of gcc is retpoline-aware)." + elif [ "$retpoline" = 1 ] && [ "$retpoline_compiler" = 1 ] && [ "$retp_enabled" = 0 ]; then + explain "Your kernel has retpoline support and has been compiled with a retpoline-aware compiler, but retpoline is disabled. You should enable it with \`echo 1 > $specex_knob_dir/retp_enabled\`." + fi + fi + # /RETPOLINE + fi + fi + # sysfs msgs: + #1 "Vulnerable" + #2 "Vulnerable: Minimal generic ASM retpoline" + #2 "Vulnerable: Minimal AMD ASM retpoline" + # "Mitigation: Full generic retpoline" + # "Mitigation: Full AMD retpoline" + # $MITIGATION + ", IBPB" + # $MITIGATION + ", IBRS_FW" + #5 $MITIGATION + " - vulnerable module loaded" + # Red Hat only: + #2 "Vulnerable: Minimal ASM retpoline", + #3 "Vulnerable: Retpoline without IBPB", + #4 "Vulnerable: Retpoline on Skylake+", + #5 "Vulnerable: Retpoline with unsafe module(s)", + # "Mitigation: Full retpoline", + # "Mitigation: Full retpoline and IBRS (user space)", + # "Mitigation: IBRS (kernel)", + # "Mitigation: IBRS (kernel and user space)", + # "Mitigation: IBP disabled", +} + +check_variant2_bsd() +{ + _info "* Mitigation 1" + _info_nol " * Kernel supports IBRS: " + ibrs_disabled=$(sysctl -n hw.ibrs_disable 2>/dev/null) + if [ -z "$ibrs_disabled" ]; then + pstatus yellow NO + else + pstatus green YES + fi + + _info_nol " * IBRS enabled and active: " + ibrs_active=$(sysctl -n hw.ibrs_active 2>/dev/null) + if [ "$ibrs_active" = 1 ]; then + pstatus green YES + else + pstatus yellow NO + fi + + _info "* Mitigation 2" + _info_nol " * Kernel compiled with RETPOLINE: " + if [ -n "$kernel_err" ]; then + pstatus yellow UNKNOWN "couldn't check ($kernel_err)" + else + if ! which "${opt_arch_prefix}readelf" >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}readelf' tool, please install it, usually it's in the binutils package" else - pstatus yellow UNKNOWN "couldn't find your kernel image or System.map" + nb_thunks=$("${opt_arch_prefix}readelf" -s "$kernel" | grep -c -e __llvm_retpoline_ -e __llvm_external_retpoline_ -e __x86_indirect_thunk_) + if [ "$nb_thunks" -gt 0 ]; then + retpoline=1 + pstatus green YES "found $nb_thunks thunk(s)" + else + pstatus yellow NO + fi fi fi - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 2; then + cve='CVE-2017-5715' + if ! is_cpu_vulnerable 2; then # override status & msg in case CPU is not vulnerable after all - pvulnstatus CVE-2017-5715 OK "your CPU vendor reported your CPU model as not vulnerable" - elif [ -z "$msg" ]; then - # if msg is empty, sysfs check didn't fill it, rely on our own test - if [ "$retpoline" = 1 -a "$retpoline_compiler" = 1 ]; then - pvulnstatus CVE-2017-5715 OK "retpoline mitigate the vulnerability" - elif [ "$opt_live" = 1 ]; then - if [ "$ibrs_enabled" = 1 -o "$ibrs_enabled" = 2 ]; then - pvulnstatus CVE-2017-5715 OK "IBRS mitigates the vulnerability" - else - pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" - fi - else - if [ "$ibrs_supported" = 1 ]; then - pvulnstatus CVE-2017-5715 OK "offline mode: IBRS will mitigate the vulnerability if enabled at runtime" - else - pvulnstatus CVE-2017-5715 VULN "IBRS hardware + kernel support OR kernel with retpoline are needed to mitigate the vulnerability" - fi - fi + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ "$retpoline" = 1 ]; then + pvulnstatus $cve OK "Retpoline mitigates the vulnerability" + elif [ "$ibrs_active" = 1 ]; then + pvulnstatus $cve OK "IBRS mitigates the vulnerability" + elif [ "$ibrs_disabled" = 0 ]; then + pvulnstatus $cve VULN "IBRS is supported by your kernel but your CPU microcode lacks support" + explain "The microcode of your CPU needs to be upgraded to be able to use IBRS. Availability of a microcode update for you CPU model depends on your CPU vendor. You can usually find out online if a microcode update is available for your CPU by searching for your CPUID (indicated in the Hardware Check section). To do a microcode update, you can search the ports for the \`cpupdate\` tool. Microcode updates done this way are not reboot-proof, so be sure to do it every time the system boots up." + elif [ "$ibrs_disabled" = 1 ]; then + pvulnstatus $cve VULN "IBRS is supported but administratively disabled on your system" + explain "To enable IBRS, use \`sysctl hw.ibrs_disable=0\`" else - pvulnstatus CVE-2017-5715 "$status" "$msg" + pvulnstatus $cve VULN "IBRS is needed to mitigate the vulnerability but your kernel is missing support" + explain "You need to either upgrade your kernel or recompile yourself a more recent version having IBRS support" fi } ######################## # MELTDOWN aka VARIANT 3 + +# no security impact but give a hint to the user in verbose mode +# about PCID/INVPCID cpuid features that must be present to avoid +# too big a performance impact with PTI +# refs: +# https://marc.info/?t=151532047900001&r=1&w=2 +# https://groups.google.com/forum/m/#!topic/mechanical-sympathy/L9mHTbeQLNU +pti_performance_check() +{ + _info_nol " * Reduced performance impact of PTI: " + if [ -e "$procfs/cpuinfo" ] && grep ^flags "$procfs/cpuinfo" | grep -qw pcid; then + cpu_pcid=1 + else + read_cpuid 0x1 $ECX 17 1 1; ret=$? + [ $ret -eq 0 ] && cpu_pcid=1 + fi + + if [ -e "$procfs/cpuinfo" ] && grep ^flags "$procfs/cpuinfo" | grep -qw invpcid; then + cpu_invpcid=1 + else + read_cpuid 0x7 $EBX 10 1 1; ret=$? + [ $ret -eq 0 ] && cpu_invpcid=1 + fi + + if [ "$cpu_invpcid" = 1 ]; then + pstatus green YES 'CPU supports INVPCID, performance impact of PTI will be greatly reduced' + elif [ "$cpu_pcid" = 1 ]; then + pstatus green YES 'CPU supports PCID, performance impact of PTI will be reduced' + else + pstatus blue NO 'PCID/INVPCID not supported, performance impact of PTI will be significant' + fi +} + check_variant3() { _info "\033[1;34mCVE-2017-5754 [rogue data cache load] aka 'Meltdown' aka 'Variant 3'\033[0m" + if [ "$os" = Linux ]; then + check_variant3_linux + elif echo "$os" | grep -q BSD; then + check_variant3_bsd + else + _warn "Unsupported OS ($os)" + fi +} +check_variant3_linux() +{ status=UNK sys_interface_available=0 msg='' if sys_interface_check "/sys/devices/system/cpu/vulnerabilities/meltdown"; then # this kernel has the /sys interface, trust it over everything sys_interface_available=1 - else + fi + if [ "$opt_sysfs_only" != 1 ]; then _info_nol "* Kernel supports Page Table Isolation (PTI): " - kpti_support=0 + kpti_support='' kpti_can_tell=0 if [ -n "$opt_config" ]; then kpti_can_tell=1 - if grep -Eq '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config"; then - _debug "kpti_support: found option "$(grep -E '^(CONFIG_PAGE_TABLE_ISOLATION|CONFIG_KAISER)=y' "$opt_config")" in $opt_config" - kpti_support=1 + kpti_support=$(grep -w -e CONFIG_PAGE_TABLE_ISOLATION=y -e CONFIG_KAISER=y -e CONFIG_UNMAP_KERNEL_AT_EL0=y "$opt_config") + if [ -n "$kpti_support" ]; then + _debug "kpti_support: found option '$kpti_support' in $opt_config" fi fi - if [ "$kpti_support" = 0 -a -n "$opt_map" ]; then + if [ -z "$kpti_support" ] && [ -n "$opt_map" ]; then # it's not an elif: some backports don't have the PTI config but still include the patch # so we try to find an exported symbol that is part of the PTI patch in System.map + # parse_kpti: arm kpti_can_tell=1 - if grep -qw kpti_force_enabled "$opt_map"; then - _debug "kpti_support: found kpti_force_enabled in $opt_map" - kpti_support=1 + kpti_support=$(grep -w -e kpti_force_enabled -e parse_kpti "$opt_map") + if [ -n "$kpti_support" ]; then + _debug "kpti_support: found '$kpti_support' in $opt_map" fi fi - if [ "$kpti_support" = 0 -a -n "$vmlinux" ]; then - # same as above but in case we don't have System.map and only vmlinux, look for the + if [ -z "$kpti_support" ] && [ -n "$kernel" ]; then + # same as above but in case we don't have System.map and only kernel, look for the # nopti option that is part of the patch (kernel command line option) + # 'kpti=': arm kpti_can_tell=1 - if ! which strings >/dev/null 2>&1; then - pstatus yellow UNKNOWN "missing 'strings' tool, please install it, usually it's in the binutils package" + if ! which "${opt_arch_prefix}strings" >/dev/null 2>&1; then + pstatus yellow UNKNOWN "missing '${opt_arch_prefix}strings' tool, please install it, usually it's in the binutils package" else - if strings "$vmlinux" | grep -qw nopti; then - _debug "kpti_support: found nopti string in $vmlinux" - kpti_support=1 + kpti_support=$("${opt_arch_prefix}strings" "$kernel" | grep -w -e nopti -e kpti=) + if [ -n "$kpti_support" ]; then + _debug "kpti_support: found '$kpti_support' in $kernel" fi fi fi - if [ "$kpti_support" = 1 ]; then - pstatus green YES + if [ -n "$kpti_support" ]; then + if [ "$opt_verbose" -ge 2 ]; then + pstatus green YES "found '$kpti_support'" + else + pstatus green YES + fi elif [ "$kpti_can_tell" = 1 ]; then - pstatus red NO + pstatus yellow NO else pstatus yellow UNKNOWN "couldn't read your kernel configuration nor System.map file" fi mount_debugfs - _info_nol "* PTI enabled and active: " + _info_nol " * PTI enabled and active: " if [ "$opt_live" = 1 ]; then dmesg_grep="Kernel/User page tables isolation: enabled" dmesg_grep="$dmesg_grep|Kernel page table isolation enabled" dmesg_grep="$dmesg_grep|x86/pti: Unmapping kernel while in userspace" - if grep ^flags /proc/cpuinfo | grep -qw pti; then + if grep ^flags "$procfs/cpuinfo" | grep -qw pti; then # vanilla PTI patch sets the 'pti' flag in cpuinfo - _debug "kpti_enabled: found 'pti' flag in /proc/cpuinfo" + _debug "kpti_enabled: found 'pti' flag in $procfs/cpuinfo" kpti_enabled=1 - elif grep ^flags /proc/cpuinfo | grep -qw kaiser; then + elif grep ^flags "$procfs/cpuinfo" | grep -qw kaiser; then # kernel line 4.9 sets the 'kaiser' flag in cpuinfo - _debug "kpti_enabled: found 'kaiser' flag in /proc/cpuinfo" + _debug "kpti_enabled: found 'kaiser' flag in $procfs/cpuinfo" kpti_enabled=1 elif [ -e /sys/kernel/debug/x86/pti_enabled ]; then - # RedHat Backport creates a dedicated file, see https://access.redhat.com/articles/3311301 + # Red Hat Backport creates a dedicated file, see https://access.redhat.com/articles/3311301 kpti_enabled=$(cat /sys/kernel/debug/x86/pti_enabled 2>/dev/null) _debug "kpti_enabled: file /sys/kernel/debug/x86/pti_enabled exists and says: $kpti_enabled" - elif dmesg | grep -Eq "$dmesg_grep"; then - # if we can't find the flag, grep dmesg output - _debug "kpti_enabled: found hint in dmesg: "$(dmesg | grep -E "$dmesg_grep") - kpti_enabled=1 - elif [ -r /var/log/dmesg ] && grep -Eq "$dmesg_grep" /var/log/dmesg; then - # if we can't find the flag in dmesg output, grep in /var/log/dmesg when readable - _debug "kpti_enabled: found hint in /var/log/dmesg: "$(grep -E "$dmesg_grep" /var/log/dmesg) - kpti_enabled=1 - else + fi + if [ -z "$kpti_enabled" ]; then + dmesg_grep "$dmesg_grep"; ret=$? + if [ $ret -eq 0 ]; then + _debug "kpti_enabled: found hint in dmesg: $dmesg_grepped" + kpti_enabled=1 + elif [ $ret -eq 2 ]; then + _debug "kpti_enabled: dmesg truncated" + kpti_enabled=-1 + fi + fi + if [ -z "$kpti_enabled" ]; then _debug "kpti_enabled: couldn't find any hint that PTI is enabled" kpti_enabled=0 fi if [ "$kpti_enabled" = 1 ]; then pstatus green YES + elif [ "$kpti_enabled" = -1 ]; then + pstatus yellow UNKNOWN "dmesg truncated, please reboot and relaunch this script" else - pstatus red NO + pstatus yellow NO fi else - pstatus blue N/A "can't verify if PTI is enabled in offline mode" + pstatus blue N/A "not testable in offline mode" + fi + + pti_performance_check + + elif [ "$sys_interface_available" = 0 ]; then + # we have no sysfs but were asked to use it only! + msg="/sys vulnerability interface use forced, but it's not available!" + status=UNK + fi + + + # Test if the current host is a Xen PV Dom0 / DomU + if [ -d "/proc/xen" ]; then + # XXX do we have a better way that relying on dmesg? + dmesg_grep 'Booting paravirtualized kernel on Xen$'; ret=$? + if [ $ret -eq 2 ]; then + _warn "dmesg truncated, Xen detection will be unreliable. Please reboot and relaunch this script" + elif [ $ret -eq 0 ]; then + if [ -e /proc/xen/capabilities ] && grep -q "control_d" /proc/xen/capabilities; then + xen_pv_domo=1 + else + xen_pv_domu=1 + fi + # PVHVM guests also print 'Booting paravirtualized kernel', so we need this check. + dmesg_grep 'Xen HVM callback vector for event delivery is enabled$'; ret=$? + if [ $ret -eq 0 ]; then + xen_pv_domu=0 + fi + fi + fi + + if [ "$opt_live" = 1 ]; then + # checking whether we're running under Xen PV 64 bits. If yes, we are affected by variant3 + # (unless we are a Dom0) + _info_nol "* Running as a Xen PV DomU: " + if [ "$xen_pv_domu" = 1 ]; then + pstatus yellow YES + else + pstatus blue NO fi fi - # if we have the /sys interface, don't even check is_cpu_vulnerable ourselves, the kernel already does it cve='CVE-2017-5754' - if [ "$sys_interface_available" = 0 ] && ! is_cpu_vulnerable 3; then + if ! is_cpu_vulnerable 3; then # override status & msg in case CPU is not vulnerable after all pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" elif [ -z "$msg" ]; then @@ -929,54 +2714,142 @@ check_variant3() if [ "$opt_live" = 1 ]; then if [ "$kpti_enabled" = 1 ]; then pvulnstatus $cve OK "PTI mitigates the vulnerability" + elif [ "$xen_pv_domo" = 1 ]; then + pvulnstatus $cve OK "Xen Dom0s are safe and do not require PTI" + elif [ "$xen_pv_domu" = 1 ]; then + pvulnstatus $cve VULN "Xen PV DomUs are vulnerable and need to be run in HVM, PVHVM, PVH mode, or the Xen hypervisor must have the Xen's own PTI patch" + explain "Go to https://blog.xenproject.org/2018/01/22/xen-project-spectre-meltdown-faq-jan-22-update/ for more information" + elif [ "$kpti_enabled" = -1 ]; then + pvulnstatus $cve UNK "couldn't find any clue of PTI activation due to a truncated dmesg, please reboot and relaunch this script" else pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + if [ -n "$kpti_support" ]; then + if [ -e "/sys/kernel/debug/x86/pti_enabled" ]; then + explain "Your kernel supports PTI but it's disabled, you can enable it with \`echo 1 > /sys/kernel/debug/x86/pti_enabled\`" + elif grep -q -w nopti -w pti=off /proc/cmdline; then + explain "Your kernel supports PTI but it has been disabled on command-line, remove the nopti or pti=off option from your bootloader configuration" + else + explain "Your kernel supports PTI but it has been disabled, check \`dmesg\` right after boot to find clues why the system disabled it" + fi + else + explain "If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel with the CONFIG_PAGE_TABLE_ISOLATION option (named CONFIG_KAISER for some kernels), or the CONFIG_UNMAP_KERNEL_AT_EL0 option (for ARM64)" + fi fi else - if [ "$kpti_support" = 1 ]; then + if [ -n "$kpti_support" ]; then pvulnstatus $cve OK "offline mode: PTI will mitigate the vulnerability if enabled at runtime" - else + elif [ "$kpti_can_tell" = 1 ]; then pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + explain "If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel with the CONFIG_PAGE_TABLE_ISOLATION option (named CONFIG_KAISER for some kernels), or the CONFIG_UNMAP_KERNEL_AT_EL0 option (for ARM64)" + else + pvulnstatus $cve UNK "offline mode: not enough information" + explain "Re-run this script with root privileges, and give it the kernel image (--kernel), the kernel configuration (--config) and the System.map file (--map) corresponding to the kernel you would like to inspect." fi fi else + if [ "$xen_pv_domo" = 1 ]; then + msg="Xen Dom0s are safe and do not require PTI" + status="OK" + elif [ "$xen_pv_domu" = 1 ]; then + msg="Xen PV DomUs are vulnerable and need to be run in HVM, PVHVM, PVH mode, or the Xen hypervisor must have the Xen's own PTI patch" + status="VULN" + _explain="Go to https://blog.xenproject.org/2018/01/22/xen-project-spectre-meltdown-faq-jan-22-update/ for more information" + elif [ "$msg" = "Vulnerable" ]; then + msg="PTI is needed to mitigate the vulnerability" + _explain="If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel with the CONFIG_PAGE_TABLE_ISOLATION option (named CONFIG_KAISER for some kernels), or the CONFIG_UNMAP_KERNEL_AT_EL0 option (for ARM64)" + fi pvulnstatus $cve "$status" "$msg" + [ -z "$_explain" ] && [ "$msg" = "Vulnerable" ] && _explain="If you're using a distro kernel, upgrade your distro to get the latest kernel available. Otherwise, recompile the kernel with the CONFIG_PAGE_TABLE_ISOLATION option (named CONFIG_KAISER for some kernels), or the CONFIG_UNMAP_KERNEL_AT_EL0 option (for ARM64)" + [ -n "$_explain" ] && explain "$_explain" + unset _explain + fi + + # Warn the user about XSA-254 recommended mitigations + if [ "$xen_pv_domo" = 1 ]; then + _warn + _warn "This host is a Xen Dom0. Please make sure that you are running your DomUs" + _warn "in HVM, PVHVM or PVH mode to prevent any guest-to-host / host-to-guest attacks." + _warn + _warn "See https://blog.xenproject.org/2018/01/22/xen-project-spectre-meltdown-faq-jan-22-update/ and XSA-254 for details." fi } +check_variant3_bsd() +{ + _info_nol "* Kernel supports Page Table Isolation (PTI): " + kpti_enabled=$(sysctl -n vm.pmap.pti 2>/dev/null) + if [ -z "$kpti_enabled" ]; then + pstatus yellow NO + else + pstatus green YES + fi + + _info_nol " * PTI enabled and active: " + if [ "$kpti_enabled" = 1 ]; then + pstatus green YES + else + pstatus yellow NO + fi + + pti_performance_check + + cve='CVE-2017-5754' + if ! is_cpu_vulnerable 3; then + # override status & msg in case CPU is not vulnerable after all + pvulnstatus $cve OK "your CPU vendor reported your CPU model as not vulnerable" + elif [ "$kpti_enabled" = 1 ]; then + pvulnstatus $cve OK "PTI mitigates the vulnerability" + elif [ -n "$kpti_enabled" ]; then + pvulnstatus $cve VULN "PTI is supported but disabled on your system" + else + pvulnstatus $cve VULN "PTI is needed to mitigate the vulnerability" + fi +} + +if [ "$opt_no_hw" = 0 ] && [ -z "$opt_arch_prefix" ]; then + check_cpu + check_cpu_vulnerabilities + _info +fi + # now run the checks the user asked for -if [ "$opt_variant1" = 1 -o "$opt_allvariants" = 1 ]; then +if [ "$opt_variant1" = 1 ] || [ "$opt_allvariants" = 1 ]; then check_variant1 _info fi -if [ "$opt_variant2" = 1 -o "$opt_allvariants" = 1 ]; then +if [ "$opt_variant2" = 1 ] || [ "$opt_allvariants" = 1 ]; then check_variant2 _info fi -if [ "$opt_variant3" = 1 -o "$opt_allvariants" = 1 ]; then +if [ "$opt_variant3" = 1 ] || [ "$opt_allvariants" = 1 ]; then check_variant3 _info fi +_vars=$(set | grep -Ev '^[A-Z_[:space:]]' | sort | tr "\n" '|') +_debug "variables at end of script: $_vars" + _info "A false sense of security is worse than no security at all, see --disclaimer" -# this'll umount only if we mounted debugfs ourselves -umount_debugfs - -# cleanup the temp decompressed config -[ -n "$dumped_config" ] && rm -f "$dumped_config" - -if [ "$opt_batch" = 1 -a "$opt_batch_format" = "nrpe" ]; then +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "nrpe" ]; then if [ ! -z "$nrpe_vuln" ]; then echo "Vulnerable:$nrpe_vuln" else echo "OK" fi - [ "$nrpe_critical" = 1 ] && exit 2 # critical - [ "$nrpe_unknown" = 1 ] && exit 3 # unknown - exit 0 # ok fi -if [ "$opt_batch" = 1 -a "$opt_batch_format" = "json" ]; then - _echo 0 ${json_output%?}] +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "json" ]; then + _echo 0 "${json_output%?}]" fi + +if [ "$opt_batch" = 1 ] && [ "$opt_batch_format" = "prometheus" ]; then + echo "# TYPE specex_vuln_status untyped" + echo "# HELP specex_vuln_status Exposure of system to speculative execution vulnerabilities" + echo "$prometheus_output" +fi + +# exit with the proper exit code +[ "$global_critical" = 1 ] && exit 2 # critical +[ "$global_unknown" = 1 ] && exit 3 # unknown +exit 0 # ok From ae89e38d56e12d6efeb2a6bd7fabeaa4f2453882 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 10 May 2018 19:13:31 +0200 Subject: [PATCH 0811/1066] [fix] some services are marked as None --- src/yunohost/service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 1852259ad..ef31afda0 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -574,6 +574,12 @@ def _get_services(): except: return {} else: + # some services are marked as None to remove them from YunoHost + # filter this + for key, value in services.items(): + if value is None: + del services[key] + return services From e8412ae0c8097c234233d07ab2fce889c542f290 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 10 May 2018 19:44:05 +0200 Subject: [PATCH 0812/1066] [mod] more debug information --- src/yunohost/service.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 1852259ad..7e52ec9c5 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -75,6 +75,7 @@ def service_add(name, status=None, log=None, runlevel=None): try: _save_services(services) except: + # we'll get a logger.warning with more details in _save_services raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', service=name)) logger.success(m18n.n('service_added', service=name)) @@ -98,6 +99,7 @@ def service_remove(name): try: _save_services(services) except: + # we'll get a logger.warning with more details in _save_services raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', service=name)) logger.success(m18n.n('service_removed', service=name)) @@ -585,9 +587,12 @@ def _save_services(services): services -- A dict of managed services with their parameters """ - # TODO: Save to custom services.yml - with open('/etc/yunohost/services.yml', 'w') as f: - yaml.safe_dump(services, f, default_flow_style=False) + try: + with open('/etc/yunohost/services.yml', 'w') as f: + yaml.safe_dump(services, f, default_flow_style=False) + except Exception as e: + logger.warning('Error while saving services, exception: %s', e, exc_info=1) + raise def _tail(file, n, offset=None): From 061b6bbcce7131e01198f39bdcaedf43a5d26fe1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 10 May 2018 21:51:36 +0200 Subject: [PATCH 0813/1066] [enh] display service journalctl logs on failed to start/stop/enable/disable error --- locales/en.json | 8 ++++---- src/yunohost/service.py | 25 +++++++++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0f45c7a8b..d237333d3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -334,9 +334,9 @@ "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", - "service_disable_failed": "Unable to disable service '{service:s}'", + "service_disable_failed": "Unable to disable service '{service:s}'\n\nRecent service logs:{logs:s}", "service_disabled": "The service '{service:s}' has been disabled", - "service_enable_failed": "Unable to enable service '{service:s}'", + "service_enable_failed": "Unable to enable service '{service:s}'\n\nRecent service logs:{logs:s}", "service_enabled": "The service '{service:s}' has been enabled", "service_no_log": "No log to display for service '{service:s}'", "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", @@ -344,10 +344,10 @@ "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", "service_remove_failed": "Unable to remove service '{service:s}'", "service_removed": "The service '{service:s}' has been removed", - "service_start_failed": "Unable to start service '{service:s}'", + "service_start_failed": "Unable to start service '{service:s}'\n\nRecent service logs:{logs:s}", "service_started": "The service '{service:s}' has been started", "service_status_failed": "Unable to determine status of service '{service:s}'", - "service_stop_failed": "Unable to stop service '{service:s}'", + "service_stop_failed": "Unable to stop service '{service:s}'\n\nRecent service logs:{logs:s}", "service_stopped": "The service '{service:s}' has been stopped", "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 7e52ec9c5..13256c324 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -115,13 +115,16 @@ def service_start(names): """ if isinstance(names, str): names = [names] + for name in names: if _run_service_command('start', name): logger.success(m18n.n('service_started', service=name)) else: if service_status(name)['status'] != 'running': raise MoulinetteError(errno.EPERM, - m18n.n('service_start_failed', service=name)) + m18n.n('service_start_failed', + service=name, + logs=_get_journalctl_logs(name))) logger.info(m18n.n('service_already_started', service=name)) @@ -141,7 +144,9 @@ def service_stop(names): else: if service_status(name)['status'] != 'inactive': raise MoulinetteError(errno.EPERM, - m18n.n('service_stop_failed', service=name)) + m18n.n('service_stop_failed', + service=name, + logs=_get_journalctl_logs(name))) logger.info(m18n.n('service_already_stopped', service=name)) @@ -160,7 +165,9 @@ def service_enable(names): logger.success(m18n.n('service_enabled', service=name)) else: raise MoulinetteError(errno.EPERM, - m18n.n('service_enable_failed', service=name)) + m18n.n('service_enable_failed', + service=name, + logs=_get_journalctl_logs(name))) def service_disable(names): @@ -178,7 +185,9 @@ def service_disable(names): logger.success(m18n.n('service_disabled', service=name)) else: raise MoulinetteError(errno.EPERM, - m18n.n('service_disable_failed', service=name)) + m18n.n('service_disable_failed', + service=name, + logs=_get_journalctl_logs(name))) def service_status(names=[]): @@ -798,3 +807,11 @@ def manually_modified_files(): output.append(filename) return output + + +def _get_journalctl_logs(service): + try: + return subprocess.check_output("journalctl -xn -u %s" % service, shell=True) + except: + import traceback + return "error while get services logs from journalctl:\n%s" % traceback.format_exc() From 4420cc300a915fcb149d1065b8477a48f0ca7ee4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 10 May 2018 22:10:35 +0200 Subject: [PATCH 0814/1066] [mod] remove unused variable --- src/yunohost/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 13256c324..8485a9f2c 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -229,8 +229,8 @@ def service_status(names=[]): # Retrieve service status try: - ret = subprocess.check_output(status, stderr=subprocess.STDOUT, - shell=True) + subprocess.check_output(status, stderr=subprocess.STDOUT, + shell=True) except subprocess.CalledProcessError as e: if 'usage:' in e.output.lower(): logger.warning(m18n.n('service_status_failed', service=name)) From 2843ce923d53403934995d4a6d48120746d68506 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 22:37:27 +0200 Subject: [PATCH 0815/1066] Have a specific upgrade for nginx-common because some people edit /etc/nginx/nginx.conf --- locales/en.json | 1 + .../data_migrations/0003_migrate_to_stretch.py | 16 ++++++++++++++-- src/yunohost/service.py | 11 +++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0f45c7a8b..ff1616c79 100644 --- a/locales/en.json +++ b/locales/en.json @@ -230,6 +230,7 @@ "migration_0003_patching_sources_list": "Patching the sources.lists ...", "migration_0003_main_upgrade": "Starting main upgrade ...", "migration_0003_fail2ban_upgrade": "Starting the fail2ban upgrade ...", + "migration_0003_nginx_upgrade": "Starting the nginx-common upgrade ...", "migration_0003_yunohost_upgrade": "Starting the yunohost package upgrade ... The migration will end, but the actual upgrade will happen right after. After the operation is complete, you might have to re-log on the webadmin.", "migration_0003_not_jessie": "The current debian distribution is not Jessie !", "migration_0003_system_not_fully_up_to_date": "Your system is not fully up to date. Please perform a regular upgrade before running the migration to stretch.", diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index ab4974e2e..a15d786df 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -15,7 +15,10 @@ from moulinette.utils.filesystem import read_file from yunohost.tools import Migration from yunohost.app import unstable_apps -from yunohost.service import _run_service_command, service_regen_conf, manually_modified_files +from yunohost.service import (_run_service_command, + service_regen_conf, + manually_modified_files, + manually_modified_files_compared_to_debian_default) from yunohost.utils.filesystem import free_space_in_directory from yunohost.utils.packages import get_installed_version @@ -47,7 +50,7 @@ class MyMigration(Migration): self.apt_update() apps_packages = self.get_apps_equivs_packages() self.unhold(["metronome"]) - self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban"]) + self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban", "nginx-common"]) # Main dist-upgrade logger.warning(m18n.n("migration_0003_main_upgrade")) @@ -68,6 +71,11 @@ class MyMigration(Migration): self.apt_dist_upgrade(conf_flags=["new", "miss", "def"]) _run_service_command("restart", "fail2ban") + # Specific upgrade for nginx-common... + logger.warning(m18n.n("migration_0003_nginx_upgrade")) + self.unhold(["nginx-common"]) + self.apt_dist_upgrade(conf_flags=["new", "def"]) + # Clean the mess os.system("apt autoremove --assume-yes") os.system("apt clean --assume-yes") @@ -129,6 +137,10 @@ class MyMigration(Migration): # Manually modified files ? (c.f. yunohost service regen-conf) modified_files = manually_modified_files() + # We also have a specific check for nginx.conf which some people + # modified and needs to be upgraded... + if "/etc/nginx/nginx.conf" in manually_modified_files_compared_to_debian_default(): + modified_files.append("/etc/nginx/nginx.conf") modified_files = "".join(["\n - "+f for f in modified_files ]) message = m18n.n("migration_0003_general_warning") diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 1852259ad..6c8acfbca 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -793,3 +793,14 @@ def manually_modified_files(): output.append(filename) return output + + +def manually_modified_files_compared_to_debian_default(): + + # from https://serverfault.com/a/90401 + r = subprocess.check_output("dpkg-query -W -f='${Conffiles}\n' '*' \ + | awk 'OFS=\" \"{print $2,$1}' \ + | md5sum -c 2>/dev/null \ + | awk -F': ' '$2 !~ /OK/{print $1}'", shell=True) + return r.strip().split("\n") + From e848524912a5846cbb4fb376aa7836dd5935e5fe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 22:58:28 +0200 Subject: [PATCH 0816/1066] Unused imports, PEP8 --- .../0003_migrate_to_stretch.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index a15d786df..41ab6b4b2 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -1,10 +1,5 @@ import glob import os -import requests -import base64 -import time -import json -import errno from shutil import copy2 from moulinette import m18n, msettings @@ -16,7 +11,6 @@ from moulinette.utils.filesystem import read_file from yunohost.tools import Migration from yunohost.app import unstable_apps from yunohost.service import (_run_service_command, - service_regen_conf, manually_modified_files, manually_modified_files_compared_to_debian_default) from yunohost.utils.filesystem import free_space_in_directory @@ -24,7 +18,8 @@ from yunohost.utils.packages import get_installed_version logger = getActionLogger('yunohost.migration') -YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat" ] +YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] + class MyMigration(Migration): "Upgrade the system to Debian Stretch and Yunohost 3.0" @@ -103,7 +98,7 @@ class MyMigration(Migration): # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... if not self.debian_major_version() == 8 \ - and not self.yunohost_major_version() == 2: + and not self.yunohost_major_version() == 2: raise MoulinetteError(m18n.n("migration_0003_not_jessie")) # Have > 1 Go free space on /var/ ? @@ -113,7 +108,7 @@ class MyMigration(Migration): # Check system is up to date # (but we don't if 'stretch' is already in the sources.list ... # which means maybe a previous upgrade crashed and we're re-running it) - if not " stretch " in read_file("/etc/apt/sources.list"): + if " stretch " not in read_file("/etc/apt/sources.list"): self.apt_update() apt_list_upgradable = check_output("apt list --upgradable -a") if "upgradable" in apt_list_upgradable: @@ -128,12 +123,12 @@ class MyMigration(Migration): # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... if not self.debian_major_version() == 8 \ - and not self.yunohost_major_version() == 2: + and not self.yunohost_major_version() == 2: return None # Get list of problematic apps ? I.e. not official or community+working problematic_apps = unstable_apps() - problematic_apps = "".join(["\n - "+app for app in problematic_apps ]) + problematic_apps = "".join(["\n - " + app for app in problematic_apps]) # Manually modified files ? (c.f. yunohost service regen-conf) modified_files = manually_modified_files() @@ -141,7 +136,7 @@ class MyMigration(Migration): # modified and needs to be upgraded... if "/etc/nginx/nginx.conf" in manually_modified_files_compared_to_debian_default(): modified_files.append("/etc/nginx/nginx.conf") - modified_files = "".join(["\n - "+f for f in modified_files ]) + modified_files = "".join(["\n - " + f for f in modified_files]) message = m18n.n("migration_0003_general_warning") @@ -232,7 +227,6 @@ class MyMigration(Migration): os.system(command) - def apt_dist_upgrade(self, conf_flags): # Make apt-get happy @@ -264,7 +258,6 @@ class MyMigration(Migration): # enabled if the user explicitly add --verbose ... os.system(command) - # Those are files that should be kept and restored before the final switch # to yunohost 3.x... They end up being modified by the various dist-upgrades # (or need to be taken out momentarily), which then blocks the regen-conf @@ -304,4 +297,3 @@ class MyMigration(Migration): for f in self.files_to_keep: dest_file = f.strip('/').replace("/", "_") copy2(os.path.join(tmp_dir, dest_file), f) - From be35b491602b0f74ca7aaa4ba1beb71e265578ed Mon Sep 17 00:00:00 2001 From: Quenti Date: Mon, 7 May 2018 09:41:19 +0000 Subject: [PATCH 0817/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 1.5% (6 of 385 strings) --- locales/oc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 35a9e8342..42b437cca 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -3,5 +3,6 @@ "admin_password_change_failed": "Impossible de cambiar lo senhal", "admin_password_changed": "Lo senhal d'administracion es ben estat cambiat", "app_already_installed": "{app:s} es ja installat", - "app_already_up_to_date": "{app:s} es ja a jorn" + "app_already_up_to_date": "{app:s} es ja a jorn", + "installation_complete": "Installacion acabada" } From aab608b8d3de3c127d8aa8006677d69f50896847 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Mon, 7 May 2018 00:22:49 +0000 Subject: [PATCH 0818/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 29.0% (112 of 385 strings) --- locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index 88690249c..ee94b6352 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -167,5 +167,6 @@ "app_already_up_to_date": "{app:s} já está atualizado", "app_argument_choice_invalid": "Escolha inválida para o argumento '{name:s}', deve ser um dos {choices:s}", "app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}", - "app_argument_required": "O argumento '{name:s}' é obrigatório" + "app_argument_required": "O argumento '{name:s}' é obrigatório", + "app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors:s}" } From 62e9c9f04d3b03d36f543b3bdb21f6e7d9d2aa8e Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 9 May 2018 14:05:46 +0000 Subject: [PATCH 0819/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 93.2% (359 of 385 strings) --- locales/ar.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 9d2cfe54a..e4594bc81 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -5,7 +5,7 @@ "admin_password_changed": "تم تعديل الكلمة السرية الإدارية", "app_already_installed": "{app:s} تم تنصيبه مِن قبل", "app_already_installed_cant_change_url": "", - "app_already_up_to_date": "", + "app_already_up_to_date": "{app:s} تم تحديثه مِن قَبل", "app_argument_choice_invalid": "", "app_argument_invalid": "", "app_argument_required": "", @@ -222,7 +222,7 @@ "migrate_tsig_wait_4": "30 ثانية …", "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", "migrations_backward": "Migrating backward.", - "migrations_bad_value_for_target": "Invalide number for target argument, available migrations numbers are 0 or {}", + "migrations_bad_value_for_target": "", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", "migrations_current_target": "Migration target is {}", "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", From ee319ef525f9a61b6c8660dd25f1e285da45c99a Mon Sep 17 00:00:00 2001 From: Quenti Date: Thu, 10 May 2018 11:02:36 +0000 Subject: [PATCH 0820/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 10.3% (40 of 385 strings) --- locales/oc.json | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 42b437cca..27c0c4d7d 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -4,5 +4,39 @@ "admin_password_changed": "Lo senhal d'administracion es ben estat cambiat", "app_already_installed": "{app:s} es ja installat", "app_already_up_to_date": "{app:s} es ja a jorn", - "installation_complete": "Installacion acabada" + "installation_complete": "Installacion acabada", + "app_id_invalid": "Id d’aplicacion incorrècte", + "app_install_files_invalid": "Fichièrs d’installacion incorrèctes", + "app_no_upgrade": "Pas cap d’aplicacion de metre a jorn", + "app_not_correctly_installed": "{app:s} sembla pas ben installat", + "app_not_installed": "{app:s} es pas installat", + "app_not_properly_removed": "{app:s} es pas estat corrèctament suprimit", + "app_removed": "{app:s} es estat suprimit", + "app_unknown": "Aplicacion desconeguda", + "app_upgrade_app_name": "Mesa a jorn de l’aplicacion {app}...", + "app_upgrade_failed": "Impossible de metre a jorn {app:s}", + "app_upgrade_some_app_failed": "D’aplicacions se pòdon pas metre a jorn", + "app_upgraded": "{app:s} es estat mes a jorn", + "appslist_fetched": "Recuperacion de la lista d’aplicacions {appslist:s} corrèctament realizada", + "appslist_migrating": "Migracion de la lista d’aplicacion{appslist:s}…", + "appslist_name_already_tracked": "I a ja una lista d’aplicacion enregistrada amb lo nom {name:s}.", + "appslist_removed": "Supression de la lista d’aplicacions {appslist:s} corrèctament realizada", + "appslist_retrieve_bad_format": "Lo fichièr recuperat per la lista d’aplicacions {appslist:s} es pas valid", + "appslist_unknown": "La lista d’aplicacions {appslist:s} es desconeguda.", + "appslist_url_already_tracked": "I a ja una lista d’aplicacions enregistrada amb l’URL {url:s}.", + "ask_current_admin_password": "Senhal administrator actual", + "ask_email": "Adreça de corrièl", + "ask_firstname": "Prenom", + "ask_lastname": "Nom", + "ask_list_to_remove": "Lista de suprimir", + "ask_main_domain": "Domeni màger", + "ask_new_admin_password": "Nòu senhal administrator", + "ask_password": "Senhal", + "ask_path": "Camin", + "backup_action_required": "Devètz precisar çò que cal salvagardar", + "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app:s} »", + "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…", + "backup_applying_method_tar": "Creacion de l’archiu tar de la salvagarda…", + "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja", + "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut" } From 606d04981ae53e5740c67b8f67fd8432166cbdb6 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Thu, 10 May 2018 21:11:49 +0000 Subject: [PATCH 0821/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 93.2% (360 of 386 strings) --- locales/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index e4594bc81..740ce0fcc 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -222,7 +222,7 @@ "migrate_tsig_wait_4": "30 ثانية …", "migrate_tsig_not_needed": "You do not appear to use a dyndns domain, so no migration is needed !", "migrations_backward": "Migrating backward.", - "migrations_bad_value_for_target": "", + "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", "migrations_current_target": "Migration target is {}", "migrations_error_failed_to_load_migration": "ERROR: failed to load migration {number} {name}", From d769f539ab3e07e8629d8d0c2c80ad31029c6b71 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 May 2018 23:46:08 +0200 Subject: [PATCH 0822/1066] apt and hold packages are fucking stupid, so we need to handle this the dirty way --- .../data_migrations/0003_migrate_to_stretch.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 41ab6b4b2..faf324303 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -45,12 +45,16 @@ class MyMigration(Migration): self.apt_update() apps_packages = self.get_apps_equivs_packages() self.unhold(["metronome"]) - self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban", "nginx-common"]) + self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban"]) + if "/etc/nginx/nginx.conf" in manually_modified_files_compared_to_debian_default() \ + and os.path.exists("/etc/nginx/nginx.conf"): + os.system("mv /etc/nginx/nginx.conf \ + /home/yunohost.conf/backup/nginx.conf.bkp_before_stretch") # Main dist-upgrade logger.warning(m18n.n("migration_0003_main_upgrade")) _run_service_command("stop", "mysql") - self.apt_dist_upgrade(conf_flags=["old", "def"]) + self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") if self.debian_major_version() == 8: raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) @@ -66,11 +70,6 @@ class MyMigration(Migration): self.apt_dist_upgrade(conf_flags=["new", "miss", "def"]) _run_service_command("restart", "fail2ban") - # Specific upgrade for nginx-common... - logger.warning(m18n.n("migration_0003_nginx_upgrade")) - self.unhold(["nginx-common"]) - self.apt_dist_upgrade(conf_flags=["new", "def"]) - # Clean the mess os.system("apt autoremove --assume-yes") os.system("apt clean --assume-yes") From 7ea53f2ff7023762d3a6ae671f15661c28574f85 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 01:25:41 +0200 Subject: [PATCH 0823/1066] [fix] metldown script can now returns 2/3 return code --- src/yunohost/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 80f7bcaa5..f3c56a6c7 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -666,7 +666,7 @@ def _check_if_vulnerable_to_meltdown(): stderr=subprocess.STDOUT) output, _ = call.communicate() - assert call.returncode == 0 + assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode CVEs = json.loads(output) assert len(CVEs) == 1 From 8186f044dced4bd91069ef39ab799359f24de0ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 11 May 2018 03:32:54 +0200 Subject: [PATCH 0824/1066] Replace the nginx.conf thing with a function called at the beginning of the migration that restore the original file if it was modified --- locales/en.json | 2 +- .../0003_migrate_to_stretch.py | 62 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/locales/en.json b/locales/en.json index ff1616c79..978465b67 100644 --- a/locales/en.json +++ b/locales/en.json @@ -230,7 +230,7 @@ "migration_0003_patching_sources_list": "Patching the sources.lists ...", "migration_0003_main_upgrade": "Starting main upgrade ...", "migration_0003_fail2ban_upgrade": "Starting the fail2ban upgrade ...", - "migration_0003_nginx_upgrade": "Starting the nginx-common upgrade ...", + "migration_0003_restoring_origin_nginx_conf": "Your file /etc/nginx/nginx.conf was edited somehow. The migration is going to reset back to its original state first... The previous file will be available as {backup_dest}.", "migration_0003_yunohost_upgrade": "Starting the yunohost package upgrade ... The migration will end, but the actual upgrade will happen right after. After the operation is complete, you might have to re-log on the webadmin.", "migration_0003_not_jessie": "The current debian distribution is not Jessie !", "migration_0003_system_not_fully_up_to_date": "Your system is not fully up to date. Please perform a regular upgrade before running the migration to stretch.", diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index faf324303..b2fcd08ac 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -39,6 +39,8 @@ class MyMigration(Migration): logger.warning(m18n.n("migration_0003_start", logfile=self.logfile)) # Preparing the upgrade + self.restore_original_nginx_conf_if_needed() + logger.warning(m18n.n("migration_0003_patching_sources_list")) self.patch_apt_sources_list() self.backup_files_to_keep() @@ -46,10 +48,6 @@ class MyMigration(Migration): apps_packages = self.get_apps_equivs_packages() self.unhold(["metronome"]) self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban"]) - if "/etc/nginx/nginx.conf" in manually_modified_files_compared_to_debian_default() \ - and os.path.exists("/etc/nginx/nginx.conf"): - os.system("mv /etc/nginx/nginx.conf \ - /home/yunohost.conf/backup/nginx.conf.bkp_before_stretch") # Main dist-upgrade logger.warning(m18n.n("migration_0003_main_upgrade")) @@ -296,3 +294,59 @@ class MyMigration(Migration): for f in self.files_to_keep: dest_file = f.strip('/').replace("/", "_") copy2(os.path.join(tmp_dir, dest_file), f) + + # On some setups, /etc/nginx/nginx.conf got edited. But this file needs + # to be upgraded because of the way the new module system works for nginx. + # (in particular, having the line that include the modules at the top) + # + # So here, if it got edited, we force the restore of the original conf + # *before* starting the actual upgrade... + # + # An alternative strategy that was attempted was to hold the nginx-common + # package and have a specific upgrade for it like for fail2ban, but that + # leads to apt complaining about not being able to upgrade for shitty + # reasons >.> + def restore_original_nginx_conf_if_needed(self): + if "/etc/nginx/nginx.conf" not in manually_modified_files_compared_to_debian_default(): + return + + if not os.path.exists("/etc/nginx/nginx.conf"): + return + + # If stretch is in the sources.list, we already started migrating on + # stretch so we don't re-do this + if " stretch " in read_file("/etc/apt/sources.list"): + return + + backup_dest = "/home/yunohost.conf/backup/nginx.conf.bkp_before_stretch" + + logger.warning(m18n.n("migration_0003_restoring_origin_nginx_conf", + backup_dest=backup_dest)) + + os.system("mv /etc/nginx/nginx.conf %s" % backup_dest) + + command = "" + command += " DEBIAN_FRONTEND=noninteractive" + command += " APT_LISTCHANGES_FRONTEND=none" + command += " apt-get" + command += " --fix-broken --show-upgraded --assume-yes" + command += ' -o Dpkg::Options::="--force-confmiss"' + command += " install --reinstall" + command += " nginx-common" + + logger.debug("Running apt command :\n{}".format(command)) + + command += " 2>&1 | tee -a {}".format(self.logfile) + + is_api = msettings.get('interface') == 'api' + if is_api: + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + call_async_output(command, callbacks, shell=True) + else: + # We do this when running from the cli to have the output of the + # command showing in the terminal, since 'info' channel is only + # enabled if the user explicitly add --verbose ... + os.system(command) From 642de6dc330d789b4bcbb81e69592ec8115f7680 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 11 May 2018 02:09:06 +0000 Subject: [PATCH 0825/1066] Update changelog for 2.7.13.2 release --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 1fa1544ad..da5428e22 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (2.7.13.2) testing; urgency=low + + * [fix] Fix an error with services marked as None (#466) + * [fix] Issue with nginx not upgrading correctly /etc/nginx/nginx.conf if it was manually modified + + -- Alexandre Aubin Fri, 11 May 2018 02:06:42 +0000 + yunohost (2.7.13.1) testing; urgency=low * [fix] Misc fixes on stretch migration following feedback From d32ab073ba2719170bb3d5254d772d8100be9445 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:21:24 +0200 Subject: [PATCH 0826/1066] [mod] removes status key from /etc/yunohost/service.yml for generic approach --- data/hooks/conf_regen/01-yunohost | 18 ++++++++++++++++++ data/templates/yunohost/services.yml | 25 +++++-------------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index e1daa7c3d..faf041110 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -61,11 +61,17 @@ do_pre_regen() { _update_services() { sudo python2 - << EOF import yaml + + with open('services.yml') as f: new_services = yaml.load(f) + with open('/etc/yunohost/services.yml') as f: services = yaml.load(f) + updated = False + + for service, conf in new_services.items(): # remove service with empty conf if conf is None: @@ -73,20 +79,32 @@ for service, conf in new_services.items(): print("removing '{0}' from services".format(service)) del services[service] updated = True + # add new service elif not services.get(service, None): print("adding '{0}' to services".format(service)) services[service] = conf updated = True + # update service conf else: conffiles = services[service].pop('conffiles', {}) + + # status need to be removed + if "status" not in conf and "status" in services[service]: + print("update '{0}' service status access".format(service)) + del services[service]["status"] + updated = True + if services[service] != conf: print("update '{0}' service".format(service)) services[service].update(conf) updated = True + if conffiles: services[service]['conffiles'] = conffiles + + if updated: with open('/etc/yunohost/services.yml-new', 'w') as f: yaml.safe_dump(services, f, default_flow_style=False) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index fb8c076f9..b4c9ab362 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -1,57 +1,42 @@ nginx: - status: service log: /var/log/nginx avahi-daemon: - status: service log: /var/log/daemon.log dnsmasq: - status: service log: /var/log/daemon.log fail2ban: - status: service log: /var/log/fail2ban.log dovecot: - status: service log: [/var/log/mail.log,/var/log/mail.err] postfix: - status: service log: [/var/log/mail.log,/var/log/mail.err] rmilter: - status: systemctl status rmilter.service log: /var/log/mail.log rspamd: - status: systemctl status rspamd.service log: /var/log/mail.log redis-server: - status: service log: /var/log/redis/redis-server.log mysql: - status: service log: [/var/log/mysql.log,/var/log/mysql.err] -glances: - status: service +glances: {} ssh: - status: service log: /var/log/auth.log +ssl: + status: null metronome: - status: metronomectl status log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err] slapd: - status: service log: /var/log/syslog php5-fpm: - status: service log: /var/log/php5-fpm.log yunohost-api: - status: service log: /var/log/yunohost/yunohost-api.log yunohost-firewall: - status: service need_lock: true nslcd: - status: service log: /var/log/syslog -nsswitch: {} +nsswitch: + status: null bind9: null tahoe-lafs: null memcached: null From 312e9bb22ef2b1ee50eecdebb1c940099a2fca1a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:21:53 +0200 Subject: [PATCH 0827/1066] [mod] respect yaml indentation --- data/templates/yunohost/services.yml | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index b4c9ab362..0c12b9e60 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -1,40 +1,40 @@ nginx: - log: /var/log/nginx + log: /var/log/nginx avahi-daemon: - log: /var/log/daemon.log + log: /var/log/daemon.log dnsmasq: - log: /var/log/daemon.log + log: /var/log/daemon.log fail2ban: - log: /var/log/fail2ban.log + log: /var/log/fail2ban.log dovecot: - log: [/var/log/mail.log,/var/log/mail.err] + log: [/var/log/mail.log,/var/log/mail.err] postfix: - log: [/var/log/mail.log,/var/log/mail.err] + log: [/var/log/mail.log,/var/log/mail.err] rmilter: - log: /var/log/mail.log + log: /var/log/mail.log rspamd: - log: /var/log/mail.log + log: /var/log/mail.log redis-server: - log: /var/log/redis/redis-server.log + log: /var/log/redis/redis-server.log mysql: - log: [/var/log/mysql.log,/var/log/mysql.err] + log: [/var/log/mysql.log,/var/log/mysql.err] glances: {} ssh: - log: /var/log/auth.log + log: /var/log/auth.log ssl: status: null metronome: - log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err] + log: [/var/log/metronome/metronome.log,/var/log/metronome/metronome.err] slapd: - log: /var/log/syslog + log: /var/log/syslog php5-fpm: - log: /var/log/php5-fpm.log + log: /var/log/php5-fpm.log yunohost-api: - log: /var/log/yunohost/yunohost-api.log + log: /var/log/yunohost/yunohost-api.log yunohost-firewall: need_lock: true nslcd: - log: /var/log/syslog + log: /var/log/syslog nsswitch: status: null bind9: null From f4df8c8ec83fc30ca1b79f0f032812eb8b70b03b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:35:28 +0200 Subject: [PATCH 0828/1066] [enh] move to dbus to retreive service status --- debian/control | 2 +- src/yunohost/service.py | 63 +++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/debian/control b/debian/control index d1505994a..17961b83d 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) , python-psutil, python-requests, python-dnspython, python-openssl - , python-apt, python-miniupnpc + , python-apt, python-miniupnpc, python-dbus , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ef61154bd..a5a97d67c 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -202,46 +202,47 @@ def service_status(names=[]): raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name)) - status = None - if services[name].get('status') == 'service': - status = 'service %s status' % name - elif "status" in services[name]: - status = str(services[name]['status']) - else: + # this "service" isn't a service actually so we skip it + # + # the historical reason is because regenconf has been hacked into the + # service part of YunoHost will in some situation we need to regenconf + # for things that aren't services + # the hack was to add fake services... + # we need to extract regenconf from service at some point, also because + # some app would really like to use it + if "status" in services[name] and services[name]["status"] is None: continue - runlevel = 5 - if 'runlevel' in services[name].keys(): - runlevel = int(services[name]['runlevel']) + status = _get_service_information_from_systemd(name) - result[name] = {'status': 'unknown', 'loaded': 'unknown'} - - # Retrieve service status - try: - ret = subprocess.check_output(status, stderr=subprocess.STDOUT, - shell=True) - except subprocess.CalledProcessError as e: - if 'usage:' in e.output.lower(): - logger.warning(m18n.n('service_status_failed', service=name)) - else: - result[name]['status'] = 'inactive' - else: - result[name]['status'] = 'running' - - # Retrieve service loading - rc_path = glob.glob("/etc/rc%d.d/S[0-9][0-9]%s" % (runlevel, name)) - if len(rc_path) == 1 and os.path.islink(rc_path[0]): - result[name]['loaded'] = 'enabled' - elif os.path.isfile("/etc/init.d/%s" % name): - result[name]['loaded'] = 'disabled' - else: - result[name]['loaded'] = 'not-found' + result[name] = { + 'status': str(status.get("SubState", "unknown")), + 'loaded': str(status.get("LoadState", "unknown")), + } if len(names) == 1: return result[names[0]] return result +def _get_service_information_from_systemd(service): + "this is the equivalent of 'systemctl status $service'" + import dbus + + d = dbus.SystemBus() + + systemd = d.get_object('org.freedesktop.systemd1','/org/freedesktop/systemd1') + manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') + + service_path = manager.GetUnit(service + ".service") + service_proxy = d.get_object('org.freedesktop.systemd1', service_path) + + # unit_proxy = dbus.Interface(service_proxy, 'org.freedesktop.systemd1.Unit',) + properties_interface = dbus.Interface(service_proxy, 'org.freedesktop.DBus.Properties') + + return properties_interface.GetAll('org.freedesktop.systemd1.Unit') + + def service_log(name, number=50): """ Log every log files of a service From 07139e7bc1de5f89cfc3b1a0ac4974444881b38b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:40:35 +0200 Subject: [PATCH 0829/1066] [enh] display service description on status --- src/yunohost/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index a5a97d67c..7284f95b8 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -218,6 +218,7 @@ def service_status(names=[]): result[name] = { 'status': str(status.get("SubState", "unknown")), 'loaded': str(status.get("LoadState", "unknown")), + 'description': str(status.get("Description", "")), } if len(names) == 1: From 929515ba940f5a439a2dab02eb21e1476323a22d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:49:20 +0200 Subject: [PATCH 0830/1066] [mod] remove useless import --- src/yunohost/service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 7284f95b8..fed7ff3be 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -27,7 +27,6 @@ import os import time import yaml import json -import glob import subprocess import errno import shutil From 1b774527d87ac8a3bafd10a0e6a982adf59f6fc4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:49:43 +0200 Subject: [PATCH 0831/1066] [enh] get active status of service --- src/yunohost/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index fed7ff3be..31ed54a5d 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -217,6 +217,7 @@ def service_status(names=[]): result[name] = { 'status': str(status.get("SubState", "unknown")), 'loaded': str(status.get("LoadState", "unknown")), + 'active': str(status.get("ActiveState", "unknown")), 'description': str(status.get("Description", "")), } From 0011c4ab56a6b4b4cd49f82097eb5a81ae69f76b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:49:52 +0200 Subject: [PATCH 0832/1066] [enh] get since how much time a service is running --- src/yunohost/service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 31ed54a5d..b5fd3a9ed 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -31,7 +31,9 @@ import subprocess import errno import shutil import hashlib + from difflib import unified_diff +from datetime import datetime from moulinette import m18n from moulinette.core import MoulinetteError @@ -218,6 +220,10 @@ def service_status(names=[]): 'status': str(status.get("SubState", "unknown")), 'loaded': str(status.get("LoadState", "unknown")), 'active': str(status.get("ActiveState", "unknown")), + 'active_at': { + "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), + "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), + }, 'description': str(status.get("Description", "")), } From 2bef98a519b4a7635b16e9285ac18797264b2e5d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 04:51:28 +0200 Subject: [PATCH 0833/1066] [enh] add path to service file in service status --- src/yunohost/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b5fd3a9ed..e73da3430 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -225,6 +225,7 @@ def service_status(names=[]): "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), }, 'description': str(status.get("Description", "")), + 'service_file_path': str(status.get("FragmentPath", "unknown")), } if len(names) == 1: From 54dffa5f94722841b1a55001bf105d62ad152e28 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 05:08:02 +0200 Subject: [PATCH 0834/1066] [mod] refactor service_log to make is readable --- src/yunohost/service.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ef61154bd..617115360 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -256,21 +256,33 @@ def service_log(name, number=50): if name not in services.keys(): raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name)) - if 'log' in services[name]: - log_list = services[name]['log'] - result = {} - if not isinstance(log_list, list): - log_list = [log_list] - - for log_path in log_list: - if os.path.isdir(log_path): - for log in [f for f in os.listdir(log_path) if os.path.isfile(os.path.join(log_path, f)) and f[-4:] == '.log']: - result[os.path.join(log_path, log)] = _tail(os.path.join(log_path, log), int(number)) - else: - result[log_path] = _tail(log_path, int(number)) - else: + if 'log' not in services[name]: raise MoulinetteError(errno.EPERM, m18n.n('service_no_log', service=name)) + log_list = services[name]['log'] + + if not isinstance(log_list, list): + log_list = [log_list] + + result = {} + + for log_path in log_list: + # log is a file, read it + if not os.path.isdir(log_path): + result[log_path] = _tail(log_path, int(number)) + continue + + for log_file in os.listdir(log_path): + log_file_path = os.path.join(log_path, log_file) + # not a file : skip + if not os.path.isfile(log_file_path): + continue + + if not log_file.endswith(".log"): + continue + + result[log_file_path] = _tail(log_file_path, int(number)) + return result From 1f6a7b2ee59a6f901ca053fb7b749da6dbb72781 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 11 May 2018 16:52:28 +0200 Subject: [PATCH 0835/1066] [fix] Untrusted TLS connection established to --- data/templates/postfix/main.cf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index bdd364250..b96bb4860 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -45,6 +45,10 @@ smtp_tls_exclude_ciphers = $smtpd_tls_exclude_ciphers smtp_tls_mandatory_ciphers= $smtpd_tls_mandatory_ciphers smtp_tls_loglevel=1 +# Fix "Untrusted TLS connection established to" message in log +smtpd_tls_CAfile = /etc/ssl/certs/ca-certificates.crt +smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt + # See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for # information on enabling SSL in the smtp client. From bd28c244795719851c5b186825997a26f2fbc713 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 05:16:07 +0200 Subject: [PATCH 0836/1066] [mod] blank lines to make code more readable --- src/yunohost/service.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ef61154bd..f9bc14a03 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -323,12 +323,15 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # create the pending conf directory for the service service_pending_path = os.path.join(PENDING_CONF_DIR, name) filesystem.mkdir(service_pending_path, 0755, True, uid='admin') + # return the arguments to pass to the script return pre_args + [service_pending_path, ] + pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call) # Update the services name names = pre_result['succeed'].keys() + if not names: raise MoulinetteError(errno.EIO, m18n.n('service_regenconf_failed', @@ -386,6 +389,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, 'service_conf_file_manually_removed', conf=system_path)) conf_status = 'removed' + # -> system conf is not managed yet elif not saved_hash: logger.debug("> system conf is not managed yet") @@ -409,6 +413,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, logger.warning(m18n.n('service_conf_file_kept_back', conf=system_path, service=service)) conf_status = 'unmanaged' + # -> system conf has not been manually modified elif system_hash == saved_hash: if to_remove: @@ -421,6 +426,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, logger.debug("> system conf is already up-to-date") os.remove(pending_path) continue + else: logger.debug("> system conf has been manually modified") if system_hash == new_hash: @@ -457,6 +463,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, 'service_conf_updated' if not dry_run else 'service_conf_would_be_updated', service=service)) + if succeed_regen and not dry_run: _update_conf_hashes(service, conf_hashes) @@ -480,6 +487,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, else: regen_conf_files = '' return post_args + [regen_conf_files, ] + hook_callback('conf_regen', names, pre_callback=_pre_call) return result @@ -678,25 +686,33 @@ def _get_pending_conf(services=[]): """ result = {} + if not os.path.isdir(PENDING_CONF_DIR): return result + if not services: services = os.listdir(PENDING_CONF_DIR) + for name in services: service_pending_path = os.path.join(PENDING_CONF_DIR, name) + if not os.path.isdir(service_pending_path): continue + path_index = len(service_pending_path) service_conf = {} + for root, dirs, files in os.walk(service_pending_path): for filename in files: pending_path = os.path.join(root, filename) service_conf[pending_path[path_index:]] = pending_path + if service_conf: result[name] = service_conf else: # remove empty directory shutil.rmtree(service_pending_path, ignore_errors=True) + return result @@ -708,9 +724,11 @@ def _get_conf_hashes(service): if service not in services: logger.debug("Service %s is not in services.yml yet.", service) return {} + elif services[service] is None or 'conffiles' not in services[service]: logger.debug("No configuration files for service %s.", service) return {} + else: return services[service]['conffiles'] @@ -743,11 +761,14 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format( system_conf.lstrip('/'), time.strftime("%Y%m%d.%H%M%S"))) backup_dir = os.path.dirname(backup_path) + if not os.path.isdir(backup_dir): filesystem.mkdir(backup_dir, 0755, True) + shutil.copy2(system_conf, backup_path) logger.info(m18n.n('service_conf_file_backed_up', conf=system_conf, backup=backup_path)) + try: if not new_conf: os.remove(system_conf) @@ -755,8 +776,10 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): conf=system_conf)) else: system_dir = os.path.dirname(system_conf) + if not os.path.isdir(system_dir): filesystem.mkdir(system_dir, 0755, True) + shutil.copyfile(new_conf, system_conf) logger.info(m18n.n('service_conf_file_updated', conf=system_conf)) @@ -766,6 +789,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): conf=system_conf), exc_info=1) return False + elif new_conf: try: copy_succeed = os.path.samefile(system_conf, new_conf) @@ -777,8 +801,10 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): conf=system_conf, new=new_conf), exc_info=1) return False + return True + def manually_modified_files(): # We do this to have --quiet, i.e. don't throw a whole bunch of logs From 1fab47cbeab4e1a2ddea080090a76094992b6b52 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 22:19:54 +0200 Subject: [PATCH 0837/1066] [mod] reduce indentation level --- src/yunohost/service.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f9bc14a03..527d7a2c7 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -292,14 +292,19 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) - if with_diff: - for service, conf_files in pending_conf.items(): - for system_path, pending_path in conf_files.items(): - pending_conf[service][system_path] = { - 'pending_conf': pending_path, - 'diff': _get_files_diff( - system_path, pending_path, True), - } + + if not with_diff: + return pending_conf + + for service, conf_files in pending_conf.items(): + for system_path, pending_path in conf_files.items(): + + pending_conf[service][system_path] = { + 'pending_conf': pending_path, + 'diff': _get_files_diff( + system_path, pending_path, True), + } + return pending_conf # Clean pending conf directory From 7435cfdea391e6793735174ffe83a9b407610dfc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 22:20:07 +0200 Subject: [PATCH 0838/1066] [mod] simplify code, give more verbose debug output --- src/yunohost/service.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 527d7a2c7..f965343d4 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -511,11 +511,11 @@ def _run_service_command(action, service): if service not in services.keys(): raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service)) - cmd = None - if action in ['start', 'stop', 'restart', 'reload', 'enable', 'disable']: - cmd = 'systemctl %s %s' % (action, service) - else: - raise ValueError("Unknown action '%s'" % action) + possible_actions = ['start', 'stop', 'restart', 'reload', 'enable', 'disable'] + if action not in possible_actions: + raise ValueError("Unknown action '%s', available actions are: %s" % (action, ", ".join(possible_actions))) + + cmd = 'systemctl %s %s' % (action, service) need_lock = services[service].get('need_lock', False) \ and action in ['start', 'stop', 'restart', 'reload'] From 1d59738085628b49952b3e8efc5cb3bf5b875f56 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 22:57:37 +0200 Subject: [PATCH 0839/1066] [fix] always remove lock if needed --- src/yunohost/service.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index f965343d4..91b060a57 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -530,14 +530,17 @@ def _run_service_command(action, service): PID = _give_lock(action, service, p) # Wait for the command to complete p.communicate() - # Remove the lock if one was given - if need_lock and PID != 0: - _remove_lock(PID) except subprocess.CalledProcessError as e: # TODO: Log output? logger.warning(m18n.n('service_cmd_exec_failed', command=' '.join(e.cmd))) return False + + finally: + # Remove the lock if one was given + if need_lock and PID != 0: + _remove_lock(PID) + return True From ebe5cab0999cf51e99ab66bbbcb2482795c55223 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 11 May 2018 23:27:24 +0200 Subject: [PATCH 0840/1066] [mod] add warning comment about unconcurrency safe _remove_lock function --- src/yunohost/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 91b060a57..20819ae98 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -573,6 +573,7 @@ def _give_lock(action, service, p): return son_PID def _remove_lock(PID_to_remove): + # FIXME ironically not concurrency safe because it's not atomic... PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n") PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ] From 14c270cebdc67a1123331237cee6dba347ee7623 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 00:26:22 +0200 Subject: [PATCH 0841/1066] [mod] offset is never used --- src/yunohost/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 20819ae98..9b4b8348e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -613,7 +613,7 @@ def _save_services(services): yaml.safe_dump(services, f, default_flow_style=False) -def _tail(file, n, offset=None): +def _tail(file, n): """ Reads a n lines from f with an offset of offset lines. The return value is a tuple in the form ``(lines, has_more)`` where `has_more` is @@ -621,7 +621,7 @@ def _tail(file, n, offset=None): """ avg_line_length = 74 - to_read = n + (offset or 0) + to_read = n try: with open(file, 'r') as f: @@ -635,7 +635,7 @@ def _tail(file, n, offset=None): pos = f.tell() lines = f.read().splitlines() if len(lines) >= to_read or pos == 0: - return lines[-to_read:offset and -offset or None] + return lines[-to_read] avg_line_length *= 1.3 except IOError: From ac6a14055dc48faa67b147b07ab6ab79cedee03b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 02:22:26 +0200 Subject: [PATCH 0842/1066] [mod] lisibility --- src/yunohost/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 9b4b8348e..b5908741f 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -632,10 +632,13 @@ def _tail(file, n): # woops. apparently file is smaller than what we want # to step back, go to the beginning instead f.seek(0) + pos = f.tell() lines = f.read().splitlines() + if len(lines) >= to_read or pos == 0: return lines[-to_read] + avg_line_length *= 1.3 except IOError: From b7e946517b998fa58ce0cf839421ace8da19adb1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 02:22:38 +0200 Subject: [PATCH 0843/1066] [mod] more debug output --- src/yunohost/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b5908741f..e37cd0062 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -641,7 +641,8 @@ def _tail(file, n): avg_line_length *= 1.3 - except IOError: + except IOError as e: + logger.warning("Error while tailing file '%s': %s", file, e, exc_info=1) return [] From 3155def9dd13e3ccbc3e76862e754aea563dbf8d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 02:55:08 +0200 Subject: [PATCH 0844/1066] [mod] simplify code --- src/yunohost/service.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index e37cd0062..0f5c1beb4 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -654,25 +654,25 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): header can also be removed if skip_header is True. """ - contents = [[], []] - for i, path in enumerate((orig_file, new_file)): - try: - with open(path, 'r') as f: - contents[i] = f.readlines() - except IOError: - pass + with open(orig_file, 'r') as orig_file: + orig_file = orig_file.readlines() + + with open(new_file, 'r') as new_file: + new_file.readlines() # Compare files and format output - diff = unified_diff(contents[0], contents[1]) + diff = unified_diff(orig_file, new_file) + if skip_header: - for i in range(2): - try: - next(diff) - except: - break + try: + next(diff) + next(diff) + except: + pass + if as_string: - result = ''.join(line for line in diff) - return result.rstrip() + return ''.join(diff).rstrip() + return diff From d15ca90eedb656258f6b1cad69464123e66b5d3b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 02:59:09 +0200 Subject: [PATCH 0845/1066] [mod] add more debug output --- src/yunohost/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 0f5c1beb4..ee9ff33f1 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -679,11 +679,14 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): def _calculate_hash(path): """Calculate the MD5 hash of a file""" hasher = hashlib.md5() + try: with open(path, 'rb') as f: hasher.update(f.read()) return hasher.hexdigest() - except IOError: + + except IOError as e: + logger.warning("Error while calculating file '%s' hash: %s", path, e, exc_info=1) return None From 249994785f2837114a688feca492dd93173c7cbb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 03:12:52 +0200 Subject: [PATCH 0846/1066] [mod] more debug output --- src/yunohost/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ee9ff33f1..99726b720 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -799,7 +799,8 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): shutil.copyfile(new_conf, system_conf) logger.info(m18n.n('service_conf_file_updated', conf=system_conf)) - except: + except Exception as e: + logger.warning("Exception while trying to regenerate conf '%s': %s", system_conf, e, exc_info=1) if not new_conf and os.path.exists(system_conf): logger.warning(m18n.n('service_conf_file_remove_failed', conf=system_conf), From 5f2f262c57aaabe914781a8679e8ec1e3497cefb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 12 May 2018 03:13:03 +0200 Subject: [PATCH 0847/1066] [doc] add comment explaining situation --- src/yunohost/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 99726b720..27b7f378b 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -809,6 +809,9 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): elif new_conf: try: + # From documentation: + # Raise an exception if an os.stat() call on either pathname fails. + # (os.stats returns a series of information from a file like type, size...) copy_succeed = os.path.samefile(system_conf, new_conf) except: copy_succeed = False From 4686e48dbc9551438a1875b639417302a39225da Mon Sep 17 00:00:00 2001 From: Quenti Date: Fri, 11 May 2018 22:09:33 +0000 Subject: [PATCH 0848/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 17.3% (67 of 386 strings) --- locales/oc.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 27c0c4d7d..ff86b107e 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -38,5 +38,32 @@ "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…", "backup_applying_method_tar": "Creacion de l’archiu tar de la salvagarda…", "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja", - "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut" + "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut", + "action_invalid": "Accion « {action:s} » incorrècte", + "app_argument_choice_invalid": "Causida invalida pel paramètre « {name:s} », cal que siá un de {choices:s}", + "app_argument_invalid": "Valor invalida pel paramètre « {name:s} » : {error:s}", + "app_argument_required": "Lo paramètre « {name:s} » es requesit", + "app_change_url_failed_nginx_reload": "La reaviada de nginx a fracassat. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}", + "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain:s}{path:s}, pas res a far.", + "app_change_url_success": "L’URL de l’aplicacion {app:s} a cambiat per {domain:s}{path:s}", + "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' es obsolèt ! Utilizatz 'app register-url' a la plaça !", + "app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla", + "app_incompatible": "L’aplicacion {app} es pas compatibla amb vòstra version de YunoHost", + "app_location_already_used": "L’aplicacion « {app} » es ja installada a aqueste emplaçament ({path})", + "app_manifest_invalid": "Manifest d’aplicacion incorrècte : {error}", + "app_package_need_update": "Lo paquet de l’aplicacion {app} deu èsser mes a jorn per seguir los cambiaments de YunoHost", + "app_requirements_checking": "Verificacion dels paquets requesida per {app}...", + "app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla", + "app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat", + "appslist_retrieve_error": "Impossible de recuperar la lista d’aplicacions alonhadas {appslist:s} : {error:s}", + "backup_archive_app_not_found": "L’aplicacion « {app:s} » es pas estada trobada dins l’archiu de la salvagarda", + "backup_archive_broken_link": "Impossible d‘accedir a l’archiu de salvagarda (ligam invalid cap a {path:s})", + "backup_archive_mount_failed": "Lo montatge de l’archiu de salvagarda a fracassat", + "backup_archive_open_failed": "Impossible de dobrir l’archiu de salvagarda", + "backup_archive_system_part_not_available": "La part « {part:s} » del sistèma es pas disponibla dins aquesta salvagarda", + "backup_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda", + "backup_copying_to_organize_the_archive": "Còpia de {size:s} Mio per organizar l’archiu", + "backup_created": "Salvagarda acabada", + "backup_creating_archive": "Creacion de l’archiu de salvagarda...", + "backup_creation_failed": "Impossible de crear la salvagarda" } From 644c927d854cb00cc168edc9b9517710ea71df90 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sat, 12 May 2018 08:45:47 +0000 Subject: [PATCH 0849/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (386 of 386 strings) --- locales/fr.json | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 84601dcb5..ff1278369 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -200,9 +200,9 @@ "service_configuration_conflict": "Le fichier {file:s} a été modifié depuis sa dernière génération. Veuillez y appliquer les modifications manuellement ou utiliser l’option --force (ce qui écrasera toutes les modifications effectuées sur le fichier).", "service_configured": "La configuration du service « {service:s} » a été générée avec succès", "service_configured_all": "La configuration de tous les services a été générée avec succès", - "service_disable_failed": "Impossible de désactiver le service « {service:s} »", + "service_disable_failed": "Impossible de désactiver le service « {service:s} »\n\nJournaux récents : {logs:s}", "service_disabled": "Le service « {service:s} » a été désactivé", - "service_enable_failed": "Impossible d'activer le service « {service:s} »", + "service_enable_failed": "Impossible d’activer le service « {service:s} »\n\nJournaux récents : {logs:s}", "service_enabled": "Le service « {service:s} » a été activé", "service_no_log": "Aucun journal à afficher pour le service « {service:s} »", "service_regenconf_dry_pending_applying": "Vérification des configurations en attentes qui pourraient être appliquées pour le service « {service} »…", @@ -210,10 +210,10 @@ "service_regenconf_pending_applying": "Application des configurations en attentes pour le service « {service} »…", "service_remove_failed": "Impossible d'enlever le service « {service:s} »", "service_removed": "Le service « {service:s} » a été enlevé", - "service_start_failed": "Impossible de démarrer le service « {service:s} »", + "service_start_failed": "Impossible de démarrer le service « {service:s} »\n\nJournaux récents : {logs:s}", "service_started": "Le service « {service:s} » a été démarré", "service_status_failed": "Impossible de déterminer le statut du service « {service:s} »", - "service_stop_failed": "Impossible d'arrêter le service « {service:s} »", + "service_stop_failed": "Impossible d’arrêter le service « {service:s} »\n\nJournaux récents : {logs:s}", "service_stopped": "Le service « {service:s} » a été arrêté", "service_unknown": "Service « {service:s} » inconnu", "services_configured": "La configuration a été générée avec succès", @@ -379,5 +379,24 @@ "migrate_tsig_wait_3": "1 minute…", "migrate_tsig_wait_4": "30 secondes…", "migrate_tsig_not_needed": "Il ne semble pas que vous utilisez un domaine dyndns, donc aucune migration n’est nécessaire !", - "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' est obsolète ! Utilisez 'app register-url' en remplacement !" + "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' est obsolète ! Utilisez 'app register-url' en remplacement !", + "migration_description_0001_change_cert_group_to_sslcert": "Change les permissions de groupe des certificats de « metronome » à « ssl-cert »", + "migration_description_0002_migrate_to_tsig_sha256": "Améliore la sécurité de DynDNDS TSIG en utilisant SHA512 au lieu de MD5", + "migration_description_0003_migrate_to_stretch": "Mise à niveau du système vers Debian Stretch et YunoHost 3.0", + "migration_0003_backward_impossible": "La migration Stretch n’est pas réversible.", + "migration_0003_start": "Démarrage de la migration vers Stretch. Les journaux seront disponibles dans {logfile}.", + "migration_0003_patching_sources_list": "Modification de sources.lists…", + "migration_0003_main_upgrade": "Démarrage de la mise à niveau principale…", + "migration_0003_fail2ban_upgrade": "Démarrage de la mise à niveau de fail2ban…", + "migration_0003_restoring_origin_nginx_conf": "Votre fichier /etc/nginx/nginx.conf a été modifié d’une manière ou d’une autre. La migration va d’abords le réinitialiser à son état initial… Le fichier précédent sera disponible en tant que {backup_dest}.", + "migration_0003_yunohost_upgrade": "Démarrage de la mise à niveau du paquet YunoHost… La migration terminera, mais la mise à jour réelle aura lieu immédiatement après. Après cette opération terminée, vous pourriez avoir à vous reconnecter à l’administration web.", + "migration_0003_not_jessie": "La distribution Debian actuelle n’est pas Jessie !", + "migration_0003_system_not_fully_up_to_date": "Votre système n’est pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer à migration à Stretch.", + "migration_0003_still_on_jessie_after_main_upgrade": "Quelque chose s’est ma passé pendant la mise à niveau principale : le système est toujours sur Jessie ?!? Pour investiguer le problème, veuillez regarder {log} 🙁…", + "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu'à quelques heures pour que tout soit à niveau.", + "migration_0003_problematic_apps_warning": "Veuillez noter que les applications suivantes, éventuellement problématiques, ont été détectées. Il semble qu’elles n’aient pas été installées depuis une liste d’application ou qu’elles ne soit pas marquées «working ». En conséquence, nous ne pouvons pas garantir qu’elles fonctionneront après la mise à niveau : {problematic_apps}", + "migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}", + "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", + "migrations_to_be_ran_manually": "La migration {number} {name} doit être lancée manuellement. Veuillez aller dans Outils > Migration dans l’interface admin, ou lancer `yunohost tools migrations migrate`.", + "migrations_need_to_accept_disclaimer": "Pour lancer la migration {number} {name}, vous devez accepter cette clause de non-responsabilité :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer." } From 676d26fb2c596d35130999cb2894754e486511d5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 12 May 2018 19:22:39 +0200 Subject: [PATCH 0850/1066] Convert old comments in php pools conf files --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 90023bcd4..b6a36e44b 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -48,6 +48,11 @@ class MyMigration(Migration): c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) os.system(c) + # Some old comments starting with '#' instead of ';' are not + # compatible in php7 + c = "sed -i 's/^#/;#/g' {}".format(dest) + os.system(c) + # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") os.system("systemctl stop php5-fpm") From 2dcd274adef06af55fd6b080e645e06e9bca901e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 12 May 2018 19:31:15 +0200 Subject: [PATCH 0851/1066] Update changelog for beta1.3 --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index a00fcceed..cb77689e1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (3.0.0~beta1.3) testing; urgency=low + + * Use mariadb 10.1 now + * Convert old php comment starting with # for php5->7 migration + + -- Alexandre Aubin Sat, 12 May 2018 19:26:00 +0000 + yunohost (3.0.0~beta1.2) testing; urgency=low Removing http2 also from yunohost_admin.conf since there still are some From 82b598b5ca33b5230c1f509767ecd87191a3565a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Sat, 12 May 2018 21:31:12 +0200 Subject: [PATCH 0852/1066] Fix from comment PR #462 --- data/helpers.d/backend | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index a912cd337..8dce2df06 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -134,7 +134,7 @@ ynh_remove_systemd_config () { # __PORT_2__ by $port_2 # ynh_add_nginx_config () { - local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" + finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" local others_var=${1:-} ynh_backup_if_checksum_is_different "$finalnginxconf" sudo cp ../conf/nginx.conf "$finalnginxconf" @@ -161,14 +161,18 @@ ynh_add_nginx_config () { fi # Replace all other variable given as arguments - for v in $others_var + for var_to_replace in $others_var do - ynh_replace_string "__${v^^}__" "${!v}" "$finalnginxconf" + # ${var_to_replace^^} make the content of the variable on upper-cases + # ${!var_to_replace} get the content of the variable named $var_to_replace + ynh_replace_string "__${var_to_replace^^}__" "${!var_to_replace}" "$finalnginxconf" done if [ "${path_url:-}" != "/" ] then ynh_replace_string "^#sub_path_only" "" "$finalnginxconf" + else + ynh_replace_string "^#root_path_only" "" "$finalnginxconf" fi ynh_store_file_checksum "$finalnginxconf" From 6461b3ec111de5d5c3b1f1ee8348dca5af88d79b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 May 2018 17:52:49 +0200 Subject: [PATCH 0853/1066] Update comment about certificates --- data/templates/postfix/main.cf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index b96bb4860..7d7589c66 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -45,7 +45,8 @@ smtp_tls_exclude_ciphers = $smtpd_tls_exclude_ciphers smtp_tls_mandatory_ciphers= $smtpd_tls_mandatory_ciphers smtp_tls_loglevel=1 -# Fix "Untrusted TLS connection established to" message in log +# Configure Root CA certificates +# (for example, avoids getting "Untrusted TLS connection established to" messages in logs) smtpd_tls_CAfile = /etc/ssl/certs/ca-certificates.crt smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt From a897ec94f8b6a2265f5c2827ecaacbc9bd4f2000 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 15 May 2018 19:40:04 +0200 Subject: [PATCH 0854/1066] Remove a line which I added for test / debug --- bin/yunoprompt | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index bb71e96b9..ab9068d26 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -47,9 +47,6 @@ ${fingerprint[4]} EOF ) - -echo "$LOGO_AND_FINGERPRINTS" > /etc/issue.net - if [[ ! -f /etc/yunohost/installed ]] then if [[ ! -f /etc/yunohost/from_script ]] From 2bc5e82b9cdc7e3c3b55d635f7fc0466012db9ad Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 15 May 2018 19:51:13 +0200 Subject: [PATCH 0855/1066] [enh] We need a fucking log Die quiet ! Die !!! --- data/helpers.d/package | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index a1d29651e..e360fad4c 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -30,7 +30,7 @@ ynh_package_version() { # # usage: ynh_apt update ynh_apt() { - DEBIAN_FRONTEND=noninteractive sudo apt-get -y -qq $@ + DEBIAN_FRONTEND=noninteractive sudo apt-get -y $@ } # Update package index files @@ -163,4 +163,4 @@ EOF ynh_remove_app_dependencies () { local dep_app=${app//_/-} # Replace all '_' by '-' ynh_package_autopurge ${dep_app}-ynh-deps # Remove the fake package and its dependencies if they not still used. -} \ No newline at end of file +} From 8b0295aa0423b72180ed74cae119432072b066b2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 17 May 2018 04:52:55 +0200 Subject: [PATCH 0856/1066] [fix] keep backward compatibility --- src/yunohost/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 7adecce39..d02adc083 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -229,7 +229,7 @@ def service_status(names=[]): result[name] = { 'status': str(status.get("SubState", "unknown")), - 'loaded': str(status.get("LoadState", "unknown")), + 'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")), 'active': str(status.get("ActiveState", "unknown")), 'active_at': { "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), From 2a4713f785c205a8ad13ba8d136da63779024756 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 17 May 2018 22:23:18 +0200 Subject: [PATCH 0857/1066] Alternative logo + misc tweak / cleaning --- bin/yunoprompt | 64 ++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index ab9068d26..5a7992789 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -16,15 +16,13 @@ done # LOGO=$(cat << 'EOF' - '. ' '' -d. - /M+ h- .shh/ // /NMy- hMdosso - 'MN' /' '. -' :+ N: .Nmyym yo .MN' omNds: :mN' .sydMMMNds+ - sMh:/dN: :M' m: oMh' .M: dy h' MM: 'Mo oMh:-sMh /ddNdyyNM' - .sMMy' /M' /M- sMMd/sM- -Ms +M+ MM: +M/ mM' -Md 'NM. hM. - mM .M- :NN yMMMMMM: .dMNNMd' -/oMMmhmMMh /msosNM/ ::oMM. +M: - 'MN sMNMM+ mN:.+mM+ -+o/ :hMMm+- 'oN- :oyo- 'yho. - - hy /yy: :- -. -Nh ' - . + .--------------. + | \\ / _ _ | + | | |_|| |(_) | + | | + | |_| _ __|_ | + | | |(_)_\\ | | + '______________' EOF ) @@ -35,15 +33,17 @@ EOF # LOGO_AND_FINGERPRINTS=$(cat << EOF -"$LOGO" -IP: ${ip} -SSH fingerprints: -${fingerprint[0]} -${fingerprint[1]} -${fingerprint[2]} -${fingerprint[3]} -${fingerprint[4]} +$LOGO + + IP: ${ip} + SSH fingerprints: + ${fingerprint[0]} + ${fingerprint[1]} + ${fingerprint[2]} + ${fingerprint[3]} + ${fingerprint[4]} + EOF ) @@ -55,25 +55,23 @@ then chvt 2 echo "$LOGO_AND_FINGERPRINTS" echo -e "\e[m Post-installation \e[0m" - echo "Congratulations! YunoHost has been successfully installed. - Two more steps are required to activate the services of your server." + echo "Congratulations! YunoHost has been successfully installed.\nTwo more steps are required to activate the services of your server." read -p "Proceed to post-installation? (y/n) " -n 1 - RESULT=1 - while [ $RESULT -gt 0 ]; do - if [[ $REPLY =~ ^[Nn]$ ]]; then + RESULT=1 + while [ $RESULT -gt 0 ]; do + if [[ $REPLY =~ ^[Nn]$ ]]; then chvt 1 - exit 0 - fi - echo -e "\n" - /usr/bin/yunohost tools postinstall - let RESULT=$? - if [ $RESULT -gt 0 ]; then - echo -e "\n" - read -p "Retry? (y/n) " -n 1 - fi - done + exit 0 + fi + echo -e "\n" + /usr/bin/yunohost tools postinstall + let RESULT=$? + if [ $RESULT -gt 0 ]; then + echo -e "\n" + read -p "Retry? (y/n) " -n 1 + fi + done fi else # YunoHost is already post-installed echo "$LOGO_AND_FINGERPRINTS" > /etc/issue fi - From 9ed8d9cdbcccfb1dea8cac23fc295b8dcfd7b5a4 Mon Sep 17 00:00:00 2001 From: Quenti Date: Sun, 13 May 2018 18:02:39 +0000 Subject: [PATCH 0858/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 22.2% (86 of 386 strings) --- locales/oc.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index ff86b107e..e0d476cf2 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -65,5 +65,24 @@ "backup_copying_to_organize_the_archive": "Còpia de {size:s} Mio per organizar l’archiu", "backup_created": "Salvagarda acabada", "backup_creating_archive": "Creacion de l’archiu de salvagarda...", - "backup_creation_failed": "Impossible de crear la salvagarda" + "backup_creation_failed": "Impossible de crear la salvagarda", + "app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.", + "app_change_no_change_url_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, poiretz aver de la metre a jorn.", + "app_change_url_no_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, benlèu que vos cal la metre a jorn.", + "app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}", + "app_location_install_failed": "Impossible d’installar l’aplicacion a aqueste emplaçament per causa de conflicte amb l’aplicacion {other_app} qu’es ja installada sus {other_path}", + "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta", + "appslist_corrupted_json": "Cargament impossible de la lista d’aplicacion. Sembla que {filename:s} siá gastat.", + "backup_delete_error": "Impossible de suprimir « {path:s} »", + "backup_deleted": "La salvagarda es estada suprimida", + "backup_hook_unknown": "Script de salvagarda « {hook:s} » desconegut", + "backup_invalid_archive": "Archiu de salvagarda incorrècte", + "backup_method_borg_finished": "La salvagarda dins Borg es acabada", + "backup_method_copy_finished": "La còpia de salvagarda es acabada", + "backup_method_tar_finished": "L’archiu tar de la salvagarda es estat creat", + "backup_output_directory_not_empty": "Lo dorsièr de sortida es pas void", + "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", + "backup_running_app_script": "Lançament de l’escript de salvagarda de l’aplicacion « {app:s} »...", + "backup_running_hooks": "Execucion dels scripts de salvagarda...", + "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma" } From da27a0c9476da5ca24c197bb55d92794839accb5 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 17 May 2018 16:40:30 +0000 Subject: [PATCH 0859/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 93.5% (361 of 386 strings) --- locales/ar.json | 53 +++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 740ce0fcc..d2bc735f2 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -24,11 +24,11 @@ "app_location_unavailable": "This url is not available or conflicts with an already installed app", "app_manifest_invalid": "Invalid app manifest: {error}", "app_no_upgrade": "البرمجيات لا تحتاج إلى تحديث", - "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", - "app_not_installed": "{app:s} is not installed", + "app_not_correctly_installed": "يبدو أن التطبيق {app:s} لم يتم تنصيبه بشكل صحيح", + "app_not_installed": "إنّ التطبيق {app:s} غير مُنصَّب", "app_not_properly_removed": "{app:s} has not been properly removed", "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", - "app_removed": "{app:s} has been removed", + "app_removed": "تمت إزالة تطبيق {app:s}", "app_requirements_checking": "Checking required packages for {app}...", "app_requirements_failed": "Unable to meet requirements for {app}: {error}", "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", @@ -38,7 +38,7 @@ "app_upgrade_app_name": "جارٍ تحديث برنامج {app}...", "app_upgrade_failed": "تعذرت عملية ترقية {app:s}", "app_upgrade_some_app_failed": "تعذرت عملية ترقية بعض البرمجيات", - "app_upgraded": "{app:s} has been upgraded", + "app_upgraded": "تم تحديث التطبيق {app:s}", "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", "appslist_fetched": "The application list {appslist:s} has been fetched", @@ -115,8 +115,8 @@ "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain:s}!", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", - "certmanager_cert_renew_success": "Successfully renewed Let's Encrypt certificate for domain {domain:s}!", - "certmanager_cert_signing_failed": "Signing the new certificate failed", + "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق {domain:s} !", + "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge: the nginx configuration file {filepath:s} is conflicting and should be removed first", "certmanager_couldnt_fetch_intermediate_cert": "Timed out when trying to fetch intermediate certificate from Let's Encrypt. Certificate installation/renewal aborted - please try again later.", @@ -139,13 +139,13 @@ "diagnosis_monitor_disk_error": "Can't monitor disks: {error}", "diagnosis_monitor_network_error": "Can't monitor network: {error}", "diagnosis_monitor_system_error": "Can't monitor system: {error}", - "diagnosis_no_apps": "No installed application", + "diagnosis_no_apps": "لم تقم بتنصيب أية تطبيقات بعد", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", "domain_cannot_remove_main": "Cannot remove main domain. Set a new main domain first", "domain_cert_gen_failed": "Unable to generate certificate", - "domain_created": "The domain has been created", - "domain_creation_failed": "Unable to create domain", - "domain_deleted": "The domain has been deleted", + "domain_created": "تم إنشاء النطاق", + "domain_creation_failed": "تعذرت عملية إنشاء النطاق", + "domain_deleted": "تم حذف النطاق", "domain_deletion_failed": "Unable to delete domain", "domain_dns_conf_is_just_a_recommendation": "This command shows you what is the *recommended* configuration. It does not actually set up the DNS configuration for you. It is your responsability to configure your DNS zone in your registrar according to this recommendation.", "domain_dyndns_already_subscribed": "You've already subscribed to a DynDNS domain", @@ -257,18 +257,18 @@ "package_not_installed": "Package '{pkgname}' is not installed", "package_unexpected_error": "An unexpected error occurred processing the package '{pkgname}'", "package_unknown": "Unknown package '{pkgname}'", - "packages_no_upgrade": "There is no package to upgrade", + "packages_no_upgrade": "لا يوجد هناك أية حزمة بحاجة إلى تحديث", "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", "path_removal_failed": "Unable to remove path {:s}", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", - "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", - "pattern_email": "Must be a valid email address (e.g. someone@domain.org)", + "pattern_domain": "يتوجب أن يكون إسم نطاق صالح (مثل my-domain.org)", + "pattern_email": "يتوجب أن يكون عنوان بريد إلكتروني صالح (مثل someone@domain.org)", "pattern_firstname": "Must be a valid first name", "pattern_lastname": "Must be a valid last name", "pattern_listname": "Must be alphanumeric and underscore characters only", "pattern_mailbox_quota": "Must be a size with b/k/M/G/T suffix or 0 to disable the quota", - "pattern_password": "Must be at least 3 characters long", + "pattern_password": "يتوجب أن تكون مكونة من 3 حروف على الأقل", "pattern_port": "يجب أن يكون رقم منفذ صالح (مثال 0-65535)", "pattern_port_or_range": "Must be a valid port number (i.e. 0-65535) or range of ports (e.g. 100:200)", "pattern_positive_number": "يجب أن يكون عددا إيجابيا", @@ -283,22 +283,22 @@ "restore_cleaning_failed": "Unable to clean-up the temporary restoration directory", "restore_complete": "Restore complete", "restore_confirm_yunohost_installed": "Do you really want to restore an already installed system? [{answers:s}]", - "restore_extracting": "Extracting needed files from the archive...", + "restore_extracting": "فك الضغط عن الملفات التي نحتاجها من النسخة الإحتياطية ...", "restore_failed": "Unable to restore the system", "restore_hook_unavailable": "Restoration script for '{part:s}' not available on your system and not in the archive either", "restore_may_be_not_enough_disk_space": "Your system seems not to have enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", - "restore_mounting_archive": "Mounting archive into '{path:s}'", + "restore_mounting_archive": "تنصيب النسخة الإحتياطية على المسار '{path:s}'", "restore_not_enough_disk_space": "Not enough disk space (freespace: {free_space:d} B, needed space: {needed_space:d} B, security margin: {margin:d} B)", "restore_nothings_done": "Nothing has been restored", "restore_removing_tmp_dir_failed": "Unable to remove an old temporary directory", "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", - "server_shutdown": "The server will shutdown", - "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", + "server_shutdown": "سوف ينطفئ الخادوم", + "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers:s}]", "server_reboot": "The server will reboot", "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", - "service_add_failed": "Unable to add service '{service:s}'", + "service_add_failed": "تعذرت إضافة خدمة '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", "service_already_stopped": "Service '{service:s}' has already been stopped", @@ -315,9 +315,9 @@ "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", - "service_disable_failed": "Unable to disable service '{service:s}'", + "service_disable_failed": "", "service_disabled": "The service '{service:s}' has been disabled", - "service_enable_failed": "Unable to enable service '{service:s}'", + "service_enable_failed": "", "service_enabled": "The service '{service:s}' has been enabled", "service_no_log": "No log to display for service '{service:s}'", "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", @@ -325,10 +325,10 @@ "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", "service_remove_failed": "Unable to remove service '{service:s}'", "service_removed": "The service '{service:s}' has been removed", - "service_start_failed": "Unable to start service '{service:s}'", + "service_start_failed": "", "service_started": "The service '{service:s}' has been started", "service_status_failed": "Unable to determine status of service '{service:s}'", - "service_stop_failed": "Unable to stop service '{service:s}'", + "service_stop_failed": "", "service_stopped": "The service '{service:s}' has been stopped", "service_unknown": "Unknown service '{service:s}'", "ssowat_conf_generated": "The SSOwat configuration has been generated", @@ -364,5 +364,10 @@ "yunohost_ca_creation_success": "تم إنشاء هيئة الشهادات المحلية.", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "عملية تنصيب يونوهوست جارية …", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'" + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", + "migration_description_0003_migrate_to_stretch": "تحديث النظام إلى ديبيان ستريتش و واي يونوهوست 3.0", + "migration_0003_patching_sources_list": "عملية تعديل ملف المصادر sources.lists جارية ...", + "migration_0003_main_upgrade": "بداية عملية التحديث الأساسية ...", + "migration_0003_fail2ban_upgrade": "بداية عملية تحديث fail2ban ...", + "migration_0003_not_jessie": "إن توزيعة ديبيان الحالية تختلف عن جيسي !" } From 02842b311d19e1ef403c5b501896ae619628896c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 17 May 2018 06:00:04 +0200 Subject: [PATCH 0860/1066] [enh] add service descriptions to en.json --- locales/en.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/locales/en.json b/locales/en.json index 86f36749b..3e64b1973 100644 --- a/locales/en.json +++ b/locales/en.json @@ -335,6 +335,24 @@ "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", + "service_description_avahi-daemon": "Avahi mDNS/DNS-SD Stack", + "service_description_dnsmasq": "dnsmasq - A lightweight DHCP and caching DNS server", + "service_description_dovecot": "Dovecot IMAP/POP3 email server", + "service_description_fail2ban": "Start/stop fail2ban", + "service_description_glances": "Starts and daemonize Glances server", + "service_description_metronome": "Metronome XMPP Server", + "service_description_mysql": "Start and stop the mysql database server daemon", + "service_description_nginx": "A high performance web server and a reverse proxy server", + "service_description_nslcd": "LDAP connection daemon", + "service_description_php5-fpm": "The PHP FastCGI Process Manager", + "service_description_postfix": "Postfix Mail Transport Agent", + "service_description_redis-server": "Advanced key-value store", + "service_description_rmilter": "Another sendmail milter for different mail checks", + "service_description_rspamd": "rapid spam filtering system", + "service_description_slapd": "OpenLDAP standalone server (Lightweight Directory Access Protocol)", + "service_description_ssh": "OpenBSD Secure Shell server", + "service_description_yunohost-api": "YunoHost API Server", + "service_description_yunohost-firewall": "YunoHost Firewall", "service_disable_failed": "Unable to disable service '{service:s}'\n\nRecent service logs:{logs:s}", "service_disabled": "The service '{service:s}' has been disabled", "service_enable_failed": "Unable to enable service '{service:s}'\n\nRecent service logs:{logs:s}", From 357be33583f6b23a50853fd3f85dc83a81b11114 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 17 May 2018 06:20:01 +0200 Subject: [PATCH 0861/1066] [enh] uses services descriptions translatables --- src/yunohost/service.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index d02adc083..96fd8ff92 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -227,6 +227,15 @@ def service_status(names=[]): status = _get_service_information_from_systemd(name) + translation_key = "service_description_%s" % name + description = m18n.n(translation_key) + + # that mean that we don't have a translation for this string + # that's the only way to test for that for now + # if we don't have it, uses the one provide by systemd + if description == translation_key: + description = str(status.get("Description", "")) + result[name] = { 'status': str(status.get("SubState", "unknown")), 'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")), @@ -235,7 +244,7 @@ def service_status(names=[]): "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), }, - 'description': str(status.get("Description", "")), + 'description': description, 'service_file_path': str(status.get("FragmentPath", "unknown")), } From 72bd201cb2efc5f67df29a2eb6384e2a5770fc3f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 18 May 2018 04:31:09 +0200 Subject: [PATCH 0862/1066] [enh] import services descriptions from https://pad.aquilenet.fr/p/8Cg5Miv6Of Thanks to everyone who contributed <3 Co-authored-by: ariasuni Co-authored-by: Haelwenn (lanodan) Monnier --- locales/en.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/locales/en.json b/locales/en.json index 3e64b1973..e7bfd2ded 100644 --- a/locales/en.json +++ b/locales/en.json @@ -335,24 +335,24 @@ "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", - "service_description_avahi-daemon": "Avahi mDNS/DNS-SD Stack", - "service_description_dnsmasq": "dnsmasq - A lightweight DHCP and caching DNS server", - "service_description_dovecot": "Dovecot IMAP/POP3 email server", - "service_description_fail2ban": "Start/stop fail2ban", - "service_description_glances": "Starts and daemonize Glances server", - "service_description_metronome": "Metronome XMPP Server", - "service_description_mysql": "Start and stop the mysql database server daemon", - "service_description_nginx": "A high performance web server and a reverse proxy server", - "service_description_nslcd": "LDAP connection daemon", - "service_description_php5-fpm": "The PHP FastCGI Process Manager", - "service_description_postfix": "Postfix Mail Transport Agent", - "service_description_redis-server": "Advanced key-value store", - "service_description_rmilter": "Another sendmail milter for different mail checks", - "service_description_rspamd": "rapid spam filtering system", - "service_description_slapd": "OpenLDAP standalone server (Lightweight Directory Access Protocol)", - "service_description_ssh": "OpenBSD Secure Shell server", - "service_description_yunohost-api": "YunoHost API Server", - "service_description_yunohost-firewall": "YunoHost Firewall", + "service_description_avahi-daemon": "allows to reach your server using yunohost.local on your local network", + "service_description_dnsmasq": "handles domain name resolution (DNS)", + "service_description_dovecot": "allows e-mail client to access/fetch email (via IMAP and POP3)", + "service_description_fail2ban": "protects against bruteforce and other kind of attacks from the Internet", + "service_description_glances": "monitors system information on your server", + "service_description_metronome": "manage XMPP instant messaging accounts", + "service_description_mysql": "stores applications data (SQL database)", + "service_description_nginx": "serves or provides access to all the websites hosted on your server", + "service_description_nslcd": "handles YunoHost user shell connection", + "service_description_php5-fpm": "runs applications written in PHP with nginx", + "service_description_postfix": "used to send and receive emails", + "service_description_redis-server": "a specialized database used for rapid data access, task queue and communication between programs", + "service_description_rmilter": "checks various parameters in emails", + "service_description_rspamd": "filters spam, and other email-related features", + "service_description_slapd": "stores users, domains and related information", + "service_description_ssh": "allows you to connect remotely to your server via a terminal (SSH protocol)", + "service_description_yunohost-api": "manages interactions between the YunoHost web interface and the system", + "service_description_yunohost-firewall": "manages open and close connexion ports to services", "service_disable_failed": "Unable to disable service '{service:s}'\n\nRecent service logs:{logs:s}", "service_disabled": "The service '{service:s}' has been disabled", "service_enable_failed": "Unable to enable service '{service:s}'\n\nRecent service logs:{logs:s}", From 23474558fcca8234d7ea3c18c80e63427df6cdc0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 18 May 2018 04:38:11 +0200 Subject: [PATCH 0863/1066] [mod] anglish --- src/yunohost/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 96fd8ff92..69b2bb9fc 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -232,7 +232,7 @@ def service_status(names=[]): # that mean that we don't have a translation for this string # that's the only way to test for that for now - # if we don't have it, uses the one provide by systemd + # if we don't have it, uses the one provided by systemd if description == translation_key: description = str(status.get("Description", "")) From 53dde3a6c7f32d11f7e141eb2084e7e4edb79e4b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 19 May 2018 21:59:52 +0200 Subject: [PATCH 0864/1066] We need to set status:null for yunohost pseudo-service --- data/templates/yunohost/services.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index 0c12b9e60..ba568760e 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -37,6 +37,8 @@ nslcd: log: /var/log/syslog nsswitch: status: null +yunohost: + status: null bind9: null tahoe-lafs: null memcached: null From d77c616d71eb7a48f6e613b49129f8494830dfb7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 May 2018 21:09:18 +0200 Subject: [PATCH 0865/1066] Return None directly if we try to calculate the hash of a file that does not exists --- src/yunohost/service.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index d02adc083..866fab414 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -713,6 +713,10 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): def _calculate_hash(path): """Calculate the MD5 hash of a file""" + + if not os.path.exists(path): + return None + hasher = hashlib.md5() try: @@ -889,7 +893,7 @@ def _get_journalctl_logs(service): import traceback return "error while get services logs from journalctl:\n%s" % traceback.format_exc() - + def manually_modified_files_compared_to_debian_default(): # from https://serverfault.com/a/90401 From c0be9676e37c9a6b13c48070b29897e1efefc1a7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 May 2018 00:46:48 +0200 Subject: [PATCH 0866/1066] Rework condition (and remove the 'from_script' flag) --- bin/yunoprompt | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 5a7992789..b92f2d209 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -47,31 +47,28 @@ $LOGO EOF ) -if [[ ! -f /etc/yunohost/installed ]] +if [[ -f /etc/yunohost/installed ]] then - if [[ ! -f /etc/yunohost/from_script ]] - then - sleep 5 - chvt 2 - echo "$LOGO_AND_FINGERPRINTS" - echo -e "\e[m Post-installation \e[0m" - echo "Congratulations! YunoHost has been successfully installed.\nTwo more steps are required to activate the services of your server." - read -p "Proceed to post-installation? (y/n) " -n 1 - RESULT=1 - while [ $RESULT -gt 0 ]; do - if [[ $REPLY =~ ^[Nn]$ ]]; then - chvt 1 - exit 0 - fi - echo -e "\n" - /usr/bin/yunohost tools postinstall - let RESULT=$? - if [ $RESULT -gt 0 ]; then - echo -e "\n" - read -p "Retry? (y/n) " -n 1 - fi - done - fi -else # YunoHost is already post-installed echo "$LOGO_AND_FINGERPRINTS" > /etc/issue +else + sleep 5 + chvt 2 + echo "$LOGO_AND_FINGERPRINTS" + echo -e "\e[m Post-installation \e[0m" + echo "Congratulations! YunoHost has been successfully installed.\nTwo more steps are required to activate the services of your server." + read -p "Proceed to post-installation? (y/n) " -n 1 + RESULT=1 + while [ $RESULT -gt 0 ]; do + if [[ $REPLY =~ ^[Nn]$ ]]; then + chvt 1 + exit 0 + fi + echo -e "\n" + /usr/bin/yunohost tools postinstall + let RESULT=$? + if [ $RESULT -gt 0 ]; then + echo -e "\n" + read -p "Retry? (y/n) " -n 1 + fi + done fi From eb76ca3fafd71f15f3096c928c614eccc0c4d483 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 May 2018 00:48:39 +0200 Subject: [PATCH 0867/1066] Change logo again ('Modular' font from patorjk.com) --- bin/yunoprompt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index b92f2d209..de05dd6fa 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -16,13 +16,13 @@ done # LOGO=$(cat << 'EOF' - .--------------. - | \\ / _ _ | - | | |_|| |(_) | - | | - | |_| _ __|_ | - | | |(_)_\\ | | - '______________' + __ __ __ __ __ _ _______ __ __ _______ _______ _______ + | | | || | | || | | || || | | || || || | + | |_| || | | || |_| || _ || |_| || _ || _____||_ _| + | || |_| || || | | || || | | || |_____ | | + |_ _|| || _ || |_| || _ || |_| ||_____ | | | + | | | || | | || || | | || | _____| | | | + |___| |_______||_| |__||_______||__| |__||_______||_______| |___| EOF ) From e7a4b6df2c27027fd4e0a6cf22f85e450652df9e Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 21 May 2018 01:28:08 +0200 Subject: [PATCH 0868/1066] [enh] Add postgresql helpers (#238) * [enh] Add postgresql helpers * Updated from experimental helpers repo * Update postgresql helpers, c.f. Experimental_helpers/#13 --- data/helpers.d/psql | 150 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 data/helpers.d/psql diff --git a/data/helpers.d/psql b/data/helpers.d/psql new file mode 100644 index 000000000..ddacbef8c --- /dev/null +++ b/data/helpers.d/psql @@ -0,0 +1,150 @@ +# Create a master password and set up global settings +# Please always call this script in install and restore scripts +# +# usage: ynh_psql_test_if_first_run +ynh_psql_test_if_first_run() { + if [ -f /etc/yunohost/psql ]; + then + echo "PostgreSQL is already installed, no need to create master password" + else + pgsql=$(ynh_string_random) + pg_hba="" + echo "$pgsql" >> /etc/yunohost/psql + + if [ -e /etc/postgresql/9.4/ ] + then + pg_hba=/etc/postgresql/9.4/main/pg_hba.conf + elif [ -e /etc/postgresql/9.6/ ] + then + pg_hba=/etc/postgresql/9.6/main/pg_hba.conf + else + ynh_die "postgresql shoud be 9.4 or 9.6" + fi + + systemctl start postgresql + sudo --login --user=postgres psql -c"ALTER user postgres WITH PASSWORD '$pgsql'" postgres + + # force all user to connect to local database using passwords + # https://www.postgresql.org/docs/current/static/auth-pg-hba-conf.html#EXAMPLE-PG-HBA.CONF + # Note: we can't use peer since YunoHost create users with nologin + # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user + sed -i '/local\s*all\s*all\s*peer/i \ + local all all password' "$pg_hba" + systemctl enable postgresql + systemctl reload postgresql + fi +} + +# Open a connection as a user +# +# example: ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;" +# example: ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql +# +# usage: ynh_psql_connect_as user pwd [db] +# | arg: user - the user name to connect as +# | arg: pwd - the user password +# | arg: db - the database to connect to +ynh_psql_connect_as() { + user="$1" + pwd="$2" + db="$3" + sudo --login --user=postgres PGUSER="$user" PGPASSWORD="$pwd" psql "$db" +} + +# # Execute a command as root user +# +# usage: ynh_psql_execute_as_root sql [db] +# | arg: sql - the SQL command to execute +# | arg: db - the database to connect to +ynh_psql_execute_as_root () { + sql="$1" + sudo --login --user=postgres psql <<< "$sql" +} + +# Execute a command from a file as root user +# +# usage: ynh_psql_execute_file_as_root file [db] +# | arg: file - the file containing SQL commands +# | arg: db - the database to connect to +ynh_psql_execute_file_as_root() { + file="$1" + db="$2" + sudo --login --user=postgres psql "$db" < "$file" +} + +# Create a database, an user and its password. Then store the password in the app's config +# +# After executing this helper, the password of the created database will be available in $db_pwd +# It will also be stored as "psqlpwd" into the app settings. +# +# usage: ynh_psql_setup_db user name [pwd] +# | arg: user - Owner of the database +# | arg: name - Name of the database +# | arg: pwd - Password of the database. If not given, a password will be generated +ynh_psql_setup_db () { + db_user="$1" + db_name="$2" + new_db_pwd=$(ynh_string_random) # Generate a random password + # If $3 is not given, use new_db_pwd instead for db_pwd. + db_pwd="${3:-$new_db_pwd}" + ynh_psql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database + ynh_app_setting_set "$app" psqlpwd "$db_pwd" # Store the password in the app's config +} + +# Create a database and grant privilegies to a user +# +# usage: ynh_psql_create_db db [user [pwd]] +# | arg: db - the database name to create +# | arg: user - the user to grant privilegies +# | arg: pwd - the user password +ynh_psql_create_db() { + db="$1" + user="$2" + pwd="$3" + ynh_psql_create_user "$user" "$pwd" + sudo --login --user=postgres createdb --owner="$user" "$db" +} + +# Drop a database +# +# usage: ynh_psql_drop_db db +# | arg: db - the database name to drop +# | arg: user - the user to drop +ynh_psql_remove_db() { + db="$1" + user="$2" + sudo --login --user=postgres dropdb "$db" + ynh_psql_drop_user "$user" +} + +# Dump a database +# +# example: ynh_psql_dump_db 'roundcube' > ./dump.sql +# +# usage: ynh_psql_dump_db db +# | arg: db - the database name to dump +# | ret: the psqldump output +ynh_psql_dump_db() { + db="$1" + sudo --login --user=postgres pg_dump "$db" +} + + +# Create a user +# +# usage: ynh_psql_create_user user pwd [host] +# | arg: user - the user name to create +ynh_psql_create_user() { + user="$1" + pwd="$2" + sudo --login --user=postgres psql -c"CREATE USER $user WITH PASSWORD '$pwd'" postgres +} + +# Drop a user +# +# usage: ynh_psql_drop_user user +# | arg: user - the user name to drop +ynh_psql_drop_user() { + user="$1" + sudo --login --user=postgres dropuser "$user" +} From 17bbe6f4b25b33e760d7cfaba47360f1adcec163 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 20:06:51 +0200 Subject: [PATCH 0869/1066] [fix] Index error on description --- src/yunohost/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c73269ba0..91d53541a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -111,7 +111,8 @@ def log_display(file_name, number=50): m18n.n('log_does_exists', log=file_name)) infos = {} if not base_path.startswith(OPERATIONS_PATH): - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if len(operation) > 2: + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename if os.path.exists(md_path): From 440e96b4955047a58c3ebe9017b02f1046bd21d8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 21:07:18 +0200 Subject: [PATCH 0870/1066] [enh] Allow to ask fo .err or .warn log file --- src/yunohost/log.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 91d53541a..94671321b 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -91,28 +91,28 @@ def log_display(file_name, number=50): number """ - if file_name.endswith(METADATA_FILE_EXT): - base_filename = file_name[:-len(METADATA_FILE_EXT)] - elif file_name.endswith(LOG_FILE_EXT): - base_filename = file_name[:-len(LOG_FILE_EXT)] - else: - base_filename = file_name + abs_path = file_name + log_path = None + if not file_name.startswith('/'): + abs_path = os.path.join(OPERATIONS_PATH, file_name) - base_path = base_filename - if not base_filename.startswith('/'): - base_path = os.path.join(OPERATIONS_PATH, base_filename) + if os.path.exists(abs_path) and not file_name.endswith(METADATA_FILE_EXT) : + log_path = abs_path + base_path = os.path.splitext(abs_path)[0] + base_filename = os.path.basename(base_path) md_path = base_path + METADATA_FILE_EXT - log_path = base_path + LOG_FILE_EXT - operation = base_filename.split("-") + if log_path is None: + log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, m18n.n('log_does_exists', log=file_name)) + infos = {} - if not base_path.startswith(OPERATIONS_PATH): - if len(operation) > 2: - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if base_path.startswith(OPERATIONS_PATH): + operation = base_filename.split("-") + infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename if os.path.exists(md_path): From 24bd5f9c835b3ba1f6ec41fd03b3f9037e1228de Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 21:18:59 +0200 Subject: [PATCH 0871/1066] [fix] Manage yaml corrupted error --- locales/en.json | 1 + src/yunohost/log.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 930722a9e..1b97fd90f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -205,6 +205,7 @@ "invalid_url_format": "Invalid URL format", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", + "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_removelist": "Remove an application list", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 94671321b..7ad52720f 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -123,8 +123,12 @@ def log_display(file_name, number=50): infos['metadata'] = metadata if 'log_path' in metadata: log_path = metadata['log_path'] - except yaml.YAMLError as exc: - print(exc) + except yaml.YAMLError: + error = m18n.n('log_corrupted_md_file', file=md_path) + if os.path.exists(log_path): + logger.warning(error) + else: + raise MoulinetteError(errno.EINVAL, error) if os.path.exists(log_path): from yunohost.service import _tail From 2f5861a6a556e2760ed120135c94ff763bf6137b Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 22 May 2018 22:16:54 +0200 Subject: [PATCH 0872/1066] [enh] Add coments in log part --- src/yunohost/log.py | 87 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7ad52720f..5d5d1d93d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -84,13 +84,17 @@ def log_list(limit=None, full=False): def log_display(file_name, number=50): """ - Display full log or specific logs listed + Display a log file enriched with metadata if any. + + If the file_name is not an absolute path, it will try to search the file in + the unit operations log path (see OPERATIONS_PATH). Argument: file_name number """ + # Normalize log/metadata paths and filenames abs_path = file_name log_path = None if not file_name.startswith('/'): @@ -110,11 +114,14 @@ def log_display(file_name, number=50): m18n.n('log_does_exists', log=file_name)) infos = {} + + # If it's a unit operation, display the name and the description if base_path.startswith(OPERATIONS_PATH): operation = base_filename.split("-") infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), infos['name'] = base_filename + # Display metadata if exist if os.path.exists(md_path): with open(md_path, "r") as md_file: try: @@ -130,6 +137,7 @@ def log_display(file_name, number=50): else: raise MoulinetteError(errno.EINVAL, error) + # Display logs if exist if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) @@ -140,8 +148,34 @@ def log_display(file_name, number=50): return infos def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): + """ + Configure quickly a unit operation + + This decorator help you to configure quickly the record of a unit operations. + + Argument: + entities A list seperated by coma of entity types related to the unit + operation. The entity type is searched inside argument's names of the + decorated function. If something match, the argument value is added as + related entity. + + exclude Remove some arguments from the context. By default, arguments + called 'password' and 'auth' are removed. If an argument is an object, you + need to exclude it or create manually the unit operation without this + decorator. + + operation_key Key describing the unit operation. If you want to display a + well formed description you should add a translation key like this + "log_" + operation_key in locales files. + + auto If true, start the recording. If False, the unit operation object + created is given to the decorated function as the first argument and you can + start recording at the good time. + """ def decorate(func): def func_wrapper(*args, **kwargs): + # For a strange reason we can't use directly the arguments from + # is_unit_operation function. We need to store them in a var before. entities_list = entities.split(',') exclude_list = exclude.split(',') op_key = operation_key @@ -150,6 +184,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if op_key is None: op_key = func.__name__ + # Search related entity in arguments of the decorated function for entity in entities_list: entity = entity.split(':') entity_type = entity[-1] @@ -162,10 +197,15 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password related_to.append({entity_type: kwargs[x]}) context = kwargs.copy() + + # Exclude unappropriate data from the context for field in exclude_list: if field in context: context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) + + # Start to record or give the unit operation in argument to let the + # developper start the record itself if auto: uo.start() try: @@ -173,12 +213,22 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password args = (uo,) + args result = func(*args, **kwargs) finally: + # Close the unit operation if it hasn't been closed before uo.close(exc_info()[0]) return result return func_wrapper return decorate class UnitOperation(object): + """ + Instances of this class represents unit operation the yunohost admin as done. + + Each time an action of the yunohost cli/api change the system, one or several + unit operations should be registered. + + This class record logs and some metadata like context or start time/end time. + """ + def __init__(self, operation, related_to=None, **kwargs): # TODO add a way to not save password on app installation self.operation = operation @@ -194,12 +244,21 @@ class UnitOperation(object): os.makedirs(self.path) def start(self): + """ + Start to record logs that change the system + Until this start method is run, no unit operation will be registered. + """ + if self.started_at is None: self.started_at = datetime.now() self.flush() self._register_log() def _register_log(self): + """ + Register log with a handler connected on log system + """ + # TODO add a way to not save password on app installation filename = os.path.join(self.path, self.name + LOG_FILE_EXT) self.file_handler = FileHandler(filename) @@ -210,12 +269,20 @@ class UnitOperation(object): self.logger.addHandler(self.file_handler) def flush(self): + """ + Write or rewrite the metadata file with all metadata known + """ + filename = os.path.join(self.path, self.name + METADATA_FILE_EXT) with open(filename, 'w') as outfile: yaml.safe_dump(self.metadata, outfile, default_flow_style=False) @property def name(self): + """ + Name of the operation + This name is used as filename, so don't use space + """ name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: @@ -224,6 +291,10 @@ class UnitOperation(object): @property def metadata(self): + """ + Dictionnary of all metadata collected + """ + data = { 'started_at': self.started_at, 'operation': self.operation, @@ -240,12 +311,21 @@ class UnitOperation(object): return data def success(self): + """ + Declare the success end of the unit operation + """ self.close() def error(self, error): + """ + Declare the failure of the unit operation + """ self.close(error) def close(self, error=None): + """ + Close properly the unit operation + """ if self.ended_at is not None or self.started_at is None: return self.ended_at = datetime.now() @@ -256,5 +336,10 @@ class UnitOperation(object): self.flush() def __del__(self): + """ + Try to close the unit operation, if it's missing. + The missing of the message below could help to see an electrical + shortage. + """ self.error(m18n.n('log_operation_unit_unclosed_properly')) From 2cb0e7916ed33c4a1a1cbbbc8dbe83e7e07171a2 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 23 May 2018 01:17:32 +0200 Subject: [PATCH 0873/1066] [enh] Watch in compress log file too --- src/yunohost/service.py | 46 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 4e264e310..cc65fe237 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -596,13 +596,21 @@ def _tail(file, n, offset=None): value is a tuple in the form ``(lines, has_more)`` where `has_more` is an indicator that is `True` if there are more lines in the file. + This function works even with splitted logs (gz compression, log rotate...) """ avg_line_length = 74 to_read = n + (offset or 0) try: - with open(file, 'r') as f: - while 1: + if file.endswith(".gz"): + import gzip + f = gzip.open(file) + lines = f.read().splitlines() + else: + f = open(file) + pos = 1 + lines = [] + while len(lines) < to_read and pos > 0: try: f.seek(-(avg_line_length * to_read), 2) except IOError: @@ -611,13 +619,43 @@ def _tail(file, n, offset=None): f.seek(0) pos = f.tell() lines = f.read().splitlines() - if len(lines) >= to_read or pos == 0: - return lines[-to_read:offset and -offset or None] avg_line_length *= 1.3 + f.close() except IOError: return [] + if len(lines) < to_read: + previous_log_file = _find_previous_log_file(file) + if previous_log_file is not None: + lines = _tail(previous_log_file, to_read - len(lines)) + lines + return lines[-to_read:offset and -offset or None] + + +def _find_previous_log_file(file): + """ + Find the previous log file + """ + import re + + splitext = os.path.splitext(file) + if splitext[1] == '.gz': + file = splitext[0] + splitext = os.path.splitext(file) + ext = splitext[1] + i = re.findall(r'\.(\d+)', ext) + i = int(i[0]) + 1 if len(i) > 0 else 1 + + previous_file = file if i == 1 else splitext[0] + previous_file = previous_file + '.%d' % (i) + if os.path.exists(previous_file): + return previous_file + + previous_file = previous_file + ".gz" + if os.path.exists(previous_file): + return previous_file + + return None def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): """Compare two files and return the differences From 930567f54f77f2755b5677aab1fd232a186eec8e Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 25 May 2018 11:58:46 +0200 Subject: [PATCH 0874/1066] [enh] Replace file_name by path in log display cmd --- data/actionsmap/yunohost.yml | 6 +++--- src/yunohost/log.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 2150aa7d3..90067d627 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1625,10 +1625,10 @@ log: ### log_display() display: action_help: Display a log content - api: GET /logs/ + api: GET /logs/display arguments: - file_name: - help: Log filename which to display the content + path: + help: Log file which to display the content -n: full: --number help: Number of lines to display diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 5d5d1d93d..44a979075 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -82,7 +82,7 @@ def log_list(limit=None, full=False): return result -def log_display(file_name, number=50): +def log_display(path, number=50): """ Display a log file enriched with metadata if any. @@ -95,12 +95,12 @@ def log_display(file_name, number=50): """ # Normalize log/metadata paths and filenames - abs_path = file_name + abs_path = path log_path = None - if not file_name.startswith('/'): - abs_path = os.path.join(OPERATIONS_PATH, file_name) + if not path.startswith('/'): + abs_path = os.path.join(OPERATIONS_PATH, path) - if os.path.exists(abs_path) and not file_name.endswith(METADATA_FILE_EXT) : + if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path base_path = os.path.splitext(abs_path)[0] @@ -111,7 +111,7 @@ def log_display(file_name, number=50): if not os.path.exists(md_path) and not os.path.exists(log_path): raise MoulinetteError(errno.EINVAL, - m18n.n('log_does_exists', log=file_name)) + m18n.n('log_does_exists', log=path)) infos = {} From 59bb47de185352070c191274fd7a34341db6d886 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 26 May 2018 10:43:37 +0200 Subject: [PATCH 0875/1066] backup filename limit set to 50 --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 6fac16511..4cf0f8955 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -788,7 +788,7 @@ backup: help: Name of the backup archive extra: pattern: &pattern_backup_archive_name - - !!str ^[\w\-\._]{1,30}(? Date: Mon, 28 May 2018 02:24:34 +0200 Subject: [PATCH 0876/1066] Merge with unstable, update changelog --- debian/changelog | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/debian/changelog b/debian/changelog index da5428e22..60467eec7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,24 @@ +yunohost (2.7.13.3) testing; urgency=low + + * [enh] Add postgresql helpers (#238) + * [enh] Bring back the bootprompt (#363) + * [enh] Allow to disable the backup during the upgrade (#431) + * [fix] Remove warning from equivs (#439) + * [enh] Add SOURCE_EXTRACT (true/false) in ynh_setup_source (#460) + * [enh] More debug output in services.py (#468) + * [enh] Be able to use more variables in template for nginx conf (#462) + * [enh] Upgrade Meltdown / Spectre diagnosis (#464) + * [enh] Check services status via dbus (#469, #478, #479) + * [mod] Cleaning in services.py code (#470, #472) + * [enh] Improvate and translate service descriptions (#476) + * [fix] Fix "untrusted TLS connection" in mail logs (#471) + * [fix] Make apt-get helper not quiet so we can debug (#475) + * [i18n] Improve Occitan, Portuguese, Arabic, French translations + + Contributors : ljf, Maniack, Josue, Aleks, Bram, Quent-in, itxtoledo, ButterflyOfFire, Jibec, ariasuni, Haelwenn + + -- Alexandre Aubin Mon, 28 May 2018 02:23:00 +0000 + yunohost (2.7.13.2) testing; urgency=low * [fix] Fix an error with services marked as None (#466) From 5e452aa07df3aae8b0cf68b3ef466baacbb6a414 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 May 2018 02:31:05 +0200 Subject: [PATCH 0877/1066] Update changelog --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index dc6c6276b..a0992251a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (3.0.0~beta1.4) testing; urgency=low + + * Merge with jessie's branches + + -- Alexandre Aubin Mon, 28 May 2018 02:30:00 +0000 + yunohost (3.0.0~beta1.3) testing; urgency=low * Use mariadb 10.1 now From d0b9eb1bddf5152ffb0b23bf42ac5aaad5260ce7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 29 May 2018 08:31:44 +0200 Subject: [PATCH 0878/1066] [fix] handle grabbing services status from alternate names, fix yunohost/issues#1134 --- src/yunohost/service.py | 66 +++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b252f0873..65a6f6f2d 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -227,26 +227,47 @@ def service_status(names=[]): status = _get_service_information_from_systemd(name) - translation_key = "service_description_%s" % name - description = m18n.n(translation_key) + # try to get status using alternative version if they exists + # this is for mariadb/mysql but is generic in case of + alternates = services[name].get("alternates", []) + while status is None and alternates: + status = _get_service_information_from_systemd(alternates.pop()) - # that mean that we don't have a translation for this string - # that's the only way to test for that for now - # if we don't have it, uses the one provided by systemd - if description == translation_key: - description = str(status.get("Description", "")) + if status is None: + logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % name) + result[name] = { + 'status': "unknown", + 'loaded': "unknown", + 'active': "unknown", + 'active_at': { + "timestamp": "unknown", + "human": "unknown", + }, + 'description': "Error: failed to get information for this service, it doesn't exists for systemd", + 'service_file_path': "unknown", + } - result[name] = { - 'status': str(status.get("SubState", "unknown")), - 'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")), - 'active': str(status.get("ActiveState", "unknown")), - 'active_at': { - "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), - "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), - }, - 'description': description, - 'service_file_path': str(status.get("FragmentPath", "unknown")), - } + else: + translation_key = "service_description_%s" % name + description = m18n.n(translation_key) + + # that mean that we don't have a translation for this string + # that's the only way to test for that for now + # if we don't have it, uses the one provided by systemd + if description == translation_key: + description = str(status.get("Description", "")) + + result[name] = { + 'status': str(status.get("SubState", "unknown")), + 'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")), + 'active': str(status.get("ActiveState", "unknown")), + 'active_at': { + "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), + "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), + }, + 'description': description, + 'service_file_path': str(status.get("FragmentPath", "unknown")), + } if len(names) == 1: return result[names[0]] @@ -256,13 +277,20 @@ def service_status(names=[]): def _get_service_information_from_systemd(service): "this is the equivalent of 'systemctl status $service'" import dbus + from dbus.exceptions import DBusException d = dbus.SystemBus() systemd = d.get_object('org.freedesktop.systemd1','/org/freedesktop/systemd1') manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') - service_path = manager.GetUnit(service + ".service") + try: + service_path = manager.GetUnit(service + ".service") + except DBusException as exception: + if exception.get_dbus_name() == 'org.freedesktop.systemd1.NoSuchUnit': + return None + raise + service_proxy = d.get_object('org.freedesktop.systemd1', service_path) # unit_proxy = dbus.Interface(service_proxy, 'org.freedesktop.systemd1.Unit',) From 6ec5a916f3edb20427f758eabe3fd1ac4784f5fd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 29 May 2018 08:32:36 +0200 Subject: [PATCH 0879/1066] [fix] would have failed on status.get returning None --- src/yunohost/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 65a6f6f2d..ae37bd8c7 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -263,7 +263,7 @@ def service_status(names=[]): 'active': str(status.get("ActiveState", "unknown")), 'active_at': { "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), - "human": datetime.fromtimestamp(status.get("ActiveEnterTimestamp") / 1000000).strftime("%F %X"), + "human": datetime.fromtimestamp(status["ActiveEnterTimestamp"] / 1000000).strftime("%F %X") if "ActiveEnterTimestamp" in status else "unknown", }, 'description': description, 'service_file_path': str(status.get("FragmentPath", "unknown")), From 75b6fd87864509295c2e1afa002901e7b625e1b4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 29 May 2018 08:54:17 +0200 Subject: [PATCH 0880/1066] [mod] add mariadb as an alternates for mysql service --- data/templates/yunohost/services.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index ba568760e..47452c476 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -18,6 +18,7 @@ redis-server: log: /var/log/redis/redis-server.log mysql: log: [/var/log/mysql.log,/var/log/mysql.err] + alternates: ['mariadb'] glances: {} ssh: log: /var/log/auth.log From d3f7809fb43fa36e672cccaf14f2a33da6e82dd6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 29 May 2018 08:55:58 +0200 Subject: [PATCH 0881/1066] [fix] redo yolo logic in case file doesn't exist --- src/yunohost/service.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b252f0873..100150c21 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -698,11 +698,18 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): header can also be removed if skip_header is True. """ - with open(orig_file, 'r') as orig_file: - orig_file = orig_file.readlines() - with open(new_file, 'r') as new_file: - new_file.readlines() + if os.path.exists(orig_file): + with open(orig_file, 'r') as orig_file: + orig_file = orig_file.readlines() + else: + orig_file = [] + + if not os.path.exists(new_file): + with open(new_file, 'r') as new_file: + new_file.readlines() + else: + new_file = [] # Compare files and format output diff = unified_diff(orig_file, new_file) From 3a6f3c37323adbfdc8ed53dcbfde79f536a4da75 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 31 May 2018 09:32:29 +0200 Subject: [PATCH 0882/1066] [fix] check if log exists before tailing to avoid errors --- src/yunohost/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b252f0873..743efa6da 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -298,7 +298,7 @@ def service_log(name, number=50): for log_path in log_list: # log is a file, read it if not os.path.isdir(log_path): - result[log_path] = _tail(log_path, int(number)) + result[log_path] = _tail(log_path, int(number)) if os.path.exists(log_path) else [] continue for log_file in os.listdir(log_path): @@ -310,7 +310,7 @@ def service_log(name, number=50): if not log_file.endswith(".log"): continue - result[log_file_path] = _tail(log_file_path, int(number)) + result[log_file_path] = _tail(log_file_path, int(number)) if os.path.exists(log_file_path) else [] return result From 91483f38965d2b5a6ac678353ccdbd48a8187d09 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jun 2018 02:30:10 +0200 Subject: [PATCH 0883/1066] Reflag some messages as info or debug --- src/yunohost/app.py | 18 ++++++------- src/yunohost/backup.py | 16 ++++++------ src/yunohost/certificate.py | 22 ++++++++-------- .../0002_migrate_to_tsig_sha256.py | 14 +++++----- .../0003_migrate_to_stretch.py | 10 +++---- src/yunohost/domain.py | 2 +- src/yunohost/dyndns.py | 4 +-- src/yunohost/firewall.py | 10 +++---- src/yunohost/hook.py | 6 ++--- src/yunohost/service.py | 26 +++++++++---------- src/yunohost/tools.py | 6 ++--- 11 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 20997de77..a073baf95 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -566,7 +566,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.info("Upgrading apps %s", ", ".join(app)) for app_instance_name in apps: - logger.warning(m18n.n('app_upgrade_app_name', app=app_instance_name)) + logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name)) installed = _is_installed(app_instance_name) if not installed: raise MoulinetteError(errno.ENOPKG, @@ -1098,7 +1098,7 @@ def app_setting(app, key, value=None, delete=False): try: return app_settings[key] except: - logger.info("cannot get app setting '%s' for '%s'", key, app) + logger.debug("cannot get app setting '%s' for '%s'", key, app) return None else: if delete and key in app_settings: @@ -1449,7 +1449,7 @@ def _extract_app_from_file(path, remove=False): Dict manifest """ - logger.info(m18n.n('extracting')) + logger.debug(m18n.n('extracting')) if os.path.exists(APP_TMP_FOLDER): shutil.rmtree(APP_TMP_FOLDER) @@ -1490,7 +1490,7 @@ def _extract_app_from_file(path, remove=False): raise MoulinetteError(errno.EINVAL, m18n.n('app_manifest_invalid', error=e.strerror)) - logger.info(m18n.n('done')) + logger.debug(m18n.n('done')) manifest['remote'] = {'type': 'file', 'path': path} return manifest, extracted_app_folder @@ -1535,7 +1535,7 @@ def _fetch_app_from_git(app): if os.path.exists(app_tmp_archive): os.remove(app_tmp_archive) - logger.info(m18n.n('downloading')) + logger.debug(m18n.n('downloading')) if ('@' in app) or ('http://' in app) or ('https://' in app): url = app @@ -1586,7 +1586,7 @@ def _fetch_app_from_git(app): raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid', error=e.strerror)) else: - logger.info(m18n.n('done')) + logger.debug(m18n.n('done')) # Store remote repository info into the returned manifest manifest['remote'] = {'type': 'git', 'url': url, 'branch': branch} @@ -1643,7 +1643,7 @@ def _fetch_app_from_git(app): raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid', error=e.strerror)) else: - logger.info(m18n.n('done')) + logger.debug(m18n.n('done')) # Store remote repository info into the returned manifest manifest['remote'] = { @@ -1766,7 +1766,7 @@ def _check_manifest_requirements(manifest, app_instance_name): elif not requirements: return - logger.info(m18n.n('app_requirements_checking', app=app_instance_name)) + logger.debug(m18n.n('app_requirements_checking', app=app_instance_name)) # Retrieve versions of each required package try: @@ -1996,7 +1996,7 @@ def _migrate_appslist_system(): for cron_path in legacy_crons: appslist_name = os.path.basename(cron_path).replace("yunohost-applist-", "") - logger.info(m18n.n('appslist_migrating', appslist=appslist_name)) + logger.debug(m18n.n('appslist_migrating', appslist=appslist_name)) # Parse appslist url in cron cron_file_content = open(cron_path).read().strip() diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index f8176e79b..1fe67e406 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -577,7 +577,7 @@ class BackupManager(): if system_targets == []: return - logger.info(m18n.n('backup_running_hooks')) + logger.debug(m18n.n('backup_running_hooks')) # Prepare environnement env_dict = self._get_env_var() @@ -665,7 +665,7 @@ class BackupManager(): tmp_app_bkp_dir = env_dict["YNH_APP_BACKUP_DIR"] settings_dir = os.path.join(self.work_dir, 'apps', app, 'settings') - logger.info(m18n.n('backup_running_app_script', app=app)) + logger.debug(m18n.n('backup_running_app_script', app=app)) try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') @@ -722,9 +722,9 @@ class BackupManager(): """Apply backup methods""" for method in self.methods: - logger.info(m18n.n('backup_applying_method_' + method.method_name)) + logger.debug(m18n.n('backup_applying_method_' + method.method_name)) method.mount_and_backup(self) - logger.info(m18n.n('backup_method_' + method.method_name + '_finished')) + logger.debug(m18n.n('backup_method_' + method.method_name + '_finished')) def _compute_backup_size(self): """ @@ -1125,7 +1125,7 @@ class RestoreManager(): if system_targets == []: return - logger.info(m18n.n('restore_running_hooks')) + logger.debug(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() ret = hook_callback('restore', @@ -1210,7 +1210,7 @@ class RestoreManager(): self.targets.set_result("apps", app_instance_name, "Warning") return - logger.info(m18n.n('restore_running_app_script', app=app_instance_name)) + logger.debug(m18n.n('restore_running_app_script', app=app_instance_name)) try: # Restore app settings app_settings_new_path = os.path.join('/etc/yunohost/apps/', @@ -1582,7 +1582,7 @@ class BackupMethod(object): m18n.n('backup_unable_to_organize_files')) # Copy unbinded path - logger.info(m18n.n('backup_copying_to_organize_the_archive', + logger.debug(m18n.n('backup_copying_to_organize_the_archive', size=str(size))) for path in paths_needed_to_be_copied: dest = os.path.join(self.work_dir, path['dest']) @@ -1786,7 +1786,7 @@ class TarBackupMethod(BackupMethod): if ret != 0: logger.warning(m18n.n('backup_archive_mount_failed')) - logger.info(m18n.n("restore_extracting")) + logger.debug(m18n.n("restore_extracting")) tar = tarfile.open(self._archive_file, "r:gz") tar.extract('info.json', path=self.work_dir) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 775e726e9..6d70b9b0a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -210,7 +210,7 @@ def _certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError( errno.EIO, m18n.n('domain_cert_gen_failed')) else: - logger.info(out) + logger.debug(out) # Link the CA cert (not sure it's actually needed in practice though, # since we append it at the end of crt.pem. For instance for Let's @@ -485,11 +485,11 @@ location ^~ '/.well-known/acme-challenge' # Write the conf if os.path.exists(nginx_conf_file): - logger.info( + logger.debug( "Nginx configuration file for ACME challenge already exists for domain, skipping.") return - logger.info( + logger.debug( "Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: @@ -531,7 +531,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _regen_dnsmasq_if_needed() # Prepare certificate signing request - logger.info( + logger.debug( "Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) @@ -541,7 +541,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) # Sign the certificate - logger.info("Now using ACME Tiny to sign the certificate...") + logger.debug("Now using ACME Tiny to sign the certificate...") domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) @@ -579,7 +579,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) # Now save the key and signed certificate - logger.info("Saving the key and signed certificate...") + logger.debug("Saving the key and signed certificate...") # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") @@ -642,7 +642,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Save the request in tmp folder csr_file = output_folder + domain + ".csr" - logger.info("Saving to %s.", csr_file) + logger.debug("Saving to %s.", csr_file) with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) @@ -753,7 +753,7 @@ def _get_status(domain): def _generate_account_key(): - logger.info("Generating account key ...") + logger.debug("Generating account key ...") _generate_key(ACCOUNT_KEY_FILE) _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400) @@ -776,7 +776,7 @@ def _set_permissions(path, user, group, permissions): def _enable_certificate(domain, new_cert_folder): - logger.info("Enabling the certificate for domain %s ...", domain) + logger.debug("Enabling the certificate for domain %s ...", domain) live_link = os.path.join(CERT_FOLDER, domain) @@ -793,7 +793,7 @@ def _enable_certificate(domain, new_cert_folder): os.symlink(new_cert_folder, live_link) - logger.info("Restarting services...") + logger.debug("Restarting services...") for service in ("postfix", "dovecot", "metronome"): _run_service_command("restart", service) @@ -802,7 +802,7 @@ def _enable_certificate(domain, new_cert_folder): def _backup_current_cert(domain): - logger.info("Backuping existing certificate for domain %s", domain) + logger.debug("Backuping existing certificate for domain %s", domain) cert_folder_domain = os.path.join(CERT_FOLDER, domain) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 5d495b06c..5cbc4494f 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -30,10 +30,10 @@ class MyMigration(Migration): (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) assert "+157" in private_key_path except (MoulinetteError, AssertionError): - logger.warning(m18n.n("migrate_tsig_not_needed")) + logger.info(m18n.n("migrate_tsig_not_needed")) return - logger.warning(m18n.n('migrate_tsig_start', domain=domain)) + logger.info(m18n.n('migrate_tsig_start', domain=domain)) public_key_path = private_key_path.rsplit(".private", 1)[0] + ".key" public_key_md5 = open(public_key_path).read().strip().split(' ')[-1] @@ -77,15 +77,15 @@ class MyMigration(Migration): os.system("mv /etc/yunohost/dyndns/*+157* /tmp") # sleep to wait for dyndns cache invalidation - logger.warning(m18n.n('migrate_tsig_wait')) + logger.info(m18n.n('migrate_tsig_wait')) time.sleep(60) - logger.warning(m18n.n('migrate_tsig_wait_2')) + logger.info(m18n.n('migrate_tsig_wait_2')) time.sleep(60) - logger.warning(m18n.n('migrate_tsig_wait_3')) + logger.info(m18n.n('migrate_tsig_wait_3')) time.sleep(30) - logger.warning(m18n.n('migrate_tsig_wait_4')) + logger.info(m18n.n('migrate_tsig_wait_4')) time.sleep(30) - logger.warning(m18n.n('migrate_tsig_end')) + logger.info(m18n.n('migrate_tsig_end')) return diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index b2fcd08ac..3dee44188 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -36,12 +36,12 @@ class MyMigration(Migration): self.check_assertions() - logger.warning(m18n.n("migration_0003_start", logfile=self.logfile)) + logger.info(m18n.n("migration_0003_start", logfile=self.logfile)) # Preparing the upgrade self.restore_original_nginx_conf_if_needed() - logger.warning(m18n.n("migration_0003_patching_sources_list")) + logger.info(m18n.n("migration_0003_patching_sources_list")) self.patch_apt_sources_list() self.backup_files_to_keep() self.apt_update() @@ -50,7 +50,7 @@ class MyMigration(Migration): self.hold(YUNOHOST_PACKAGES + apps_packages + ["fail2ban"]) # Main dist-upgrade - logger.warning(m18n.n("migration_0003_main_upgrade")) + logger.info(m18n.n("migration_0003_main_upgrade")) _run_service_command("stop", "mysql") self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") @@ -58,7 +58,7 @@ class MyMigration(Migration): raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) # Specific upgrade for fail2ban... - logger.warning(m18n.n("migration_0003_fail2ban_upgrade")) + logger.info(m18n.n("migration_0003_fail2ban_upgrade")) self.unhold(["fail2ban"]) # Don't move this if folder already exists. If it does, we probably are # running this script a 2nd, 3rd, ... time but /etc/fail2ban will @@ -73,7 +73,7 @@ class MyMigration(Migration): os.system("apt clean --assume-yes") # Upgrade yunohost packages - logger.warning(m18n.n("migration_0003_yunohost_upgrade")) + logger.info(m18n.n("migration_0003_yunohost_upgrade")) self.restore_files_to_keep() self.unhold(YUNOHOST_PACKAGES + apps_packages) self.upgrade_yunohost_packages() diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 5196d107a..545408592 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -202,7 +202,7 @@ def domain_dns_conf(domain, ttl=None): is_cli = True if msettings.get('interface') == 'cli' else False if is_cli: - logger.warning(m18n.n("domain_dns_conf_is_just_a_recommendation")) + logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) return result diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index aaf86decd..785b0dd34 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -141,7 +141,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None if not os.path.exists('/etc/yunohost/dyndns'): os.makedirs('/etc/yunohost/dyndns') - logger.info(m18n.n('dyndns_key_generating')) + logger.debug(m18n.n('dyndns_key_generating')) os.system('cd /etc/yunohost/dyndns && ' 'dnssec-keygen -a hmac-sha512 -b 512 -r /dev/urandom -n USER %s' % domain) @@ -288,7 +288,7 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, # to nsupdate as argument write_to_file(DYNDNS_ZONE, '\n'.join(lines)) - logger.info("Now pushing new conf to DynDNS host...") + logger.debug("Now pushing new conf to DynDNS host...") try: command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 97451511f..7b1c72170 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -305,7 +305,7 @@ def firewall_upnp(action='status', no_refresh=False): # Compatibility with previous version if action == 'reload': - logger.info("'reload' action is deprecated and will be removed") + logger.debug("'reload' action is deprecated and will be removed") try: # Remove old cron job os.remove('/etc/cron.d/yunohost-firewall') @@ -357,7 +357,7 @@ def firewall_upnp(action='status', no_refresh=False): # Select UPnP device upnpc.selectigd() except: - logger.info('unable to select UPnP device', exc_info=1) + logger.debug('unable to select UPnP device', exc_info=1) enabled = False else: # Iterate over ports @@ -376,7 +376,7 @@ def firewall_upnp(action='status', no_refresh=False): upnpc.addportmapping(port, protocol, upnpc.lanaddr, port, 'yunohost firewall: port %d' % port, '') except: - logger.info('unable to add port %d using UPnP', + logger.debug('unable to add port %d using UPnP', port, exc_info=1) enabled = False @@ -459,6 +459,6 @@ def _update_firewall_file(rules): def _on_rule_command_error(returncode, cmd, output): """Callback for rules commands error""" # Log error and continue commands execution - logger.info('"%s" returned non-zero exit status %d:\n%s', - cmd, returncode, prependlines(output.rstrip(), '> ')) + logger.debug('"%s" returned non-zero exit status %d:\n%s', + cmd, returncode, prependlines(output.rstrip(), '> ')) return True diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 1f971edb6..32570ab57 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -355,13 +355,13 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, command.append(cmd.format(script=cmd_script, args=cmd_args)) if logger.isEnabledFor(log.DEBUG): - logger.info(m18n.n('executing_command', command=' '.join(command))) + logger.debug(m18n.n('executing_command', command=' '.join(command))) else: - logger.info(m18n.n('executing_script', script=path)) + logger.debug(m18n.n('executing_script', script=path)) # Define output callbacks and call command callbacks = ( - lambda l: logger.info(l.rstrip()), + lambda l: logger.debug(l.rstrip()), lambda l: logger.warning(l.rstrip()), ) returncode = call_async_output( diff --git a/src/yunohost/service.py b/src/yunohost/service.py index c5ee33f83..0dd71742c 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -126,7 +126,7 @@ def service_start(names): m18n.n('service_start_failed', service=name, logs=_get_journalctl_logs(name))) - logger.info(m18n.n('service_already_started', service=name)) + logger.debug(m18n.n('service_already_started', service=name)) def service_stop(names): @@ -148,7 +148,7 @@ def service_stop(names): m18n.n('service_stop_failed', service=name, logs=_get_journalctl_logs(name))) - logger.info(m18n.n('service_already_stopped', service=name)) + logger.debug(m18n.n('service_already_stopped', service=name)) def service_enable(names): @@ -416,7 +416,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): - logger.info(m18n.n( + logger.debug(m18n.n( 'service_regenconf_pending_applying' if not dry_run else 'service_regenconf_dry_pending_applying', service=service)) @@ -459,7 +459,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, regenerated = _regen( system_path, pending_path, save=False) else: - logger.warning(m18n.n( + logger.info(m18n.n( 'service_conf_file_manually_removed', conf=system_path)) conf_status = 'removed' @@ -476,16 +476,16 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # we assume that it is safe to regen it, since the file is backuped # anyway (by default in _regen), as long as we warn the user # appropriately. - logger.warning(m18n.n('service_conf_new_managed_file', - conf=system_path, service=service)) + logger.info(m18n.n('service_conf_new_managed_file', + conf=system_path, service=service)) regenerated = _regen(system_path, pending_path) conf_status = 'new' elif force: regenerated = _regen(system_path) conf_status = 'force-removed' else: - logger.warning(m18n.n('service_conf_file_kept_back', - conf=system_path, service=service)) + logger.info(m18n.n('service_conf_file_kept_back', + conf=system_path, service=service)) conf_status = 'unmanaged' # -> system conf has not been manually modified @@ -530,7 +530,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, # Check for service conf changes if not succeed_regen and not failed_regen: - logger.info(m18n.n('service_conf_up_to_date', service=service)) + logger.debug(m18n.n('service_conf_up_to_date', service=service)) continue elif not failed_regen: logger.success(m18n.n( @@ -865,13 +865,13 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): filesystem.mkdir(backup_dir, 0755, True) shutil.copy2(system_conf, backup_path) - logger.info(m18n.n('service_conf_file_backed_up', + logger.debug(m18n.n('service_conf_file_backed_up', conf=system_conf, backup=backup_path)) try: if not new_conf: os.remove(system_conf) - logger.info(m18n.n('service_conf_file_removed', + logger.debug(m18n.n('service_conf_file_removed', conf=system_conf)) else: system_dir = os.path.dirname(system_conf) @@ -880,8 +880,8 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): filesystem.mkdir(system_dir, 0755, True) shutil.copyfile(new_conf, system_conf) - logger.info(m18n.n('service_conf_file_updated', - conf=system_conf)) + logger.debug(m18n.n('service_conf_file_updated', + conf=system_conf)) except Exception as e: logger.warning("Exception while trying to regenerate conf '%s': %s", system_conf, e, exc_info=1) if not new_conf and os.path.exists(system_conf): diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 3a2958e3c..ad8cfd846 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -224,7 +224,7 @@ def _set_hostname(hostname, pretty_hostname=None): logger.warning(out) raise MoulinetteError(errno.EIO, m18n.n('domain_hostname_failed')) else: - logger.info(out) + logger.debug(out) def _is_inside_container(): @@ -424,7 +424,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): cache = apt.Cache() # Update APT cache - logger.info(m18n.n('updating_apt_cache')) + logger.debug(m18n.n('updating_apt_cache')) if not cache.update(): raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) @@ -438,7 +438,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): 'fullname': pkg.fullname, 'changelog': pkg.get_changelog() }) - logger.info(m18n.n('done')) + logger.debug(m18n.n('done')) # "apps" will list upgradable packages apps = [] From e47fc937c39477cf3c54c084ce6ab864c1a4c8a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jun 2018 21:46:46 +0000 Subject: [PATCH 0884/1066] Set verbose by default (and remove the corresponding option) --- bin/yunohost | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/bin/yunohost b/bin/yunohost index 1522b7118..fd9c2dbfd 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -9,8 +9,8 @@ import argparse IN_DEVEL = False # Level for which loggers will log -LOGGERS_LEVEL = 'INFO' -TTY_LOG_LEVEL = 'SUCCESS' +LOGGERS_LEVEL = 'DEBUG' +TTY_LOG_LEVEL = 'INFO' # Handlers that will be used by loggers # - file: log to the file LOG_DIR/LOG_FILE @@ -58,10 +58,6 @@ def _parse_cli_args(): action='store_true', default=False, help="Log and print debug messages", ) - parser.add_argument('--verbose', - action='store_true', default=False, - help="Be more verbose in the output", - ) parser.add_argument('--quiet', action='store_true', default=False, help="Don't produce any output", @@ -92,13 +88,13 @@ def _parse_cli_args(): return (parser, opts, args) -def _init_moulinette(debug=False, verbose=False, quiet=False): +def _init_moulinette(debug=False, quiet=False): """Configure logging and initialize the moulinette""" # Define loggers handlers handlers = set(LOGGERS_HANDLERS) if quiet and 'tty' in handlers: handlers.remove('tty') - elif verbose and 'tty' not in handlers: + elif 'tty' not in handlers: handlers.append('tty') root_handlers = set(handlers) @@ -108,10 +104,8 @@ def _init_moulinette(debug=False, verbose=False, quiet=False): # Define loggers level level = LOGGERS_LEVEL tty_level = TTY_LOG_LEVEL - if verbose: - tty_level = 'INFO' if debug: - tty_level = level = 'DEBUG' + tty_level = 'DEBUG' # Custom logging configuration logging = { @@ -196,7 +190,7 @@ if __name__ == '__main__': sys.exit(1) parser, opts, args = _parse_cli_args() - _init_moulinette(opts.debug, opts.verbose, opts.quiet) + _init_moulinette(opts.debug, opts.quiet) # Check that YunoHost is installed if not os.path.isfile('/etc/yunohost/installed') and \ From 61949a0fbab59b8f1da25bddd049b57dd4cd4c52 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jun 2018 22:29:21 +0000 Subject: [PATCH 0885/1066] Updating changelog --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index a0992251a..0a8353ef3 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (3.0.0~beta1.5) testing; urgency=low + + * Fix a bug for services with alternate names (mysql<->mariadb) + * Fix a bug in regen conf when computing diff with files that don't exists + * Increase backup filename length + + (Fixes by Bram <3) + + -- Alexandre Aubin Mon, 02 Jun 2018 00:14:00 +0000 + yunohost (3.0.0~beta1.4) testing; urgency=low * Merge with jessie's branches From 89124d4a9fad60c5e85cc255f067865fca663c67 Mon Sep 17 00:00:00 2001 From: ljf Date: Sat, 2 Jun 2018 13:42:43 +0200 Subject: [PATCH 0886/1066] [enh] Support others log categories --- data/actionsmap/yunohost.yml | 6 +-- src/yunohost/log.py | 75 +++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 90067d627..78b6433f8 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,13 +1614,13 @@ log: action_help: List logs api: GET /logs arguments: + categories: + help: Log categories to display (default operations) + nargs: "*" -l: full: --limit help: Maximum number of logs type: int - --full: - help: Show more details - action: store_true ### log_display() display: diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 44a979075..156e8aa0f 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -27,23 +27,26 @@ import os import yaml import errno +import collections from datetime import datetime from logging import FileHandler, getLogger, Formatter from sys import exc_info -from moulinette import m18n +from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -OPERATIONS_PATH = '/var/log/yunohost/operation/' +CATEGORIES_PATH = '/var/log/yunohost/categories/' +CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ + 'app'] METADATA_FILE_EXT = '.yml' LOG_FILE_EXT = '.log' RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(limit=None, full=False): +def log_list(categories=[], limit=None): """ List available logs @@ -51,33 +54,45 @@ def log_list(limit=None, full=False): limit -- Maximum number of logs """ - result = {"operations": []} + if not categories: + is_api = msettings.get('interface') == 'api' + categories = ["operation"] if not is_api else CATEGORIES - if not os.path.exists(OPERATIONS_PATH): - return result + result = collections.OrderedDict() + for category in categories: + result[category] = [] - operations = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(OPERATIONS_PATH)) - operations = reversed(sorted(operations)) + category_path = os.path.join(CATEGORIES_PATH, category) + if not os.path.exists(category_path): + continue - if limit is not None: - operations = operations[:limit] + logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) + logs = reversed(sorted(logs)) - for operation in operations: + if limit is not None: + logs = logs[:limit] - base_filename = operation[:-len(METADATA_FILE_EXT)] - md_filename = operation - md_path = os.path.join(OPERATIONS_PATH, md_filename) + for log in logs: - operation = base_filename.split("-") + base_filename = log[:-len(METADATA_FILE_EXT)] + md_filename = log + md_path = os.path.join(category_path, md_filename) - operation_datetime = datetime.strptime(" ".join(operation[:2]), "%Y%m%d %H%M%S") + log = base_filename.split("-") - result["operations"].append({ - "started_at": operation_datetime, - "description": m18n.n("log_" + operation[2], *operation[3:]), - "name": base_filename, - "path": md_path, - }) + entry = { + "name": base_filename, + "path": md_path, + } + try: + log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + except ValueError: + entry["description"] = m18n.n("log_" + log[0], *log[1:]), + else: + entry["description"] = m18n.n("log_" + log[2], *log[3:]), + entry["started_at"] = log_datetime + + result[category].append(entry) return result @@ -98,7 +113,10 @@ def log_display(path, number=50): abs_path = path log_path = None if not path.startswith('/'): - abs_path = os.path.join(OPERATIONS_PATH, path) + for category in CATEGORIES: + abs_path = os.path.join(CATEGORIES_PATH, category, path) + if os.path.exists(abs_path) or os.path.exists(abs_path + METADATA_FILE_EXT): + break if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path @@ -116,9 +134,14 @@ def log_display(path, number=50): infos = {} # If it's a unit operation, display the name and the description - if base_path.startswith(OPERATIONS_PATH): - operation = base_filename.split("-") - infos['description'] = m18n.n("log_" + operation[2], *operation[3:]), + if base_path.startswith(CATEGORIES_PATH): + log = base_filename.split("-") + try: + datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + except ValueError: + infos["description"] = m18n.n("log_" + log[0], *log[1:]), + else: + infos["description"] = m18n.n("log_" + log[2], *log[3:]), infos['name'] = base_filename # Display metadata if exist From 5676f05dfbfd3e702ea715df0a8dae31183c792b Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 04:04:10 +0200 Subject: [PATCH 0887/1066] [enh] Better help for log list action --- data/actionsmap/yunohost.yml | 4 ++-- locales/en.json | 1 + src/yunohost/log.py | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 78b6433f8..ec150c1ec 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1614,8 +1614,8 @@ log: action_help: List logs api: GET /logs arguments: - categories: - help: Log categories to display (default operations) + category: + help: Log category to display (default operations), could be operation, history, package, system, access, service or app nargs: "*" -l: full: --limit diff --git a/locales/en.json b/locales/en.json index 1b97fd90f..9d4267de6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -206,6 +206,7 @@ "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", + "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_removelist": "Remove an application list", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 156e8aa0f..7adee7e73 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -46,7 +46,7 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') -def log_list(categories=[], limit=None): +def log_list(category=[], limit=None): """ List available logs @@ -54,6 +54,9 @@ def log_list(categories=[], limit=None): limit -- Maximum number of logs """ + categories = category + + # In cli we just display `operation` logs by default if not categories: is_api = msettings.get('interface') == 'api' categories = ["operation"] if not is_api else CATEGORIES @@ -64,6 +67,8 @@ def log_list(categories=[], limit=None): category_path = os.path.join(CATEGORIES_PATH, category) if not os.path.exists(category_path): + logger.warning(m18n.n('log_category_404', category=category)) + continue logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) From a20281ebfcdc118660a7602624e4ef15bb3e54b1 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 18:57:00 +0200 Subject: [PATCH 0888/1066] [enh] Operational unit for backup regenconf, migrations and others --- src/yunohost/app.py | 28 ++++++++++++++++++++++++++++ src/yunohost/backup.py | 38 ++++++++++++++++++++++++++++++++++---- src/yunohost/service.py | 7 ++++++- src/yunohost/tools.py | 19 ++++++++++++++++--- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index df36ccdf7..4273471a0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -623,6 +623,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] uo = UnitOperation('app_upgrade', related_to, env=env_dict) + uo.start() # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) @@ -918,6 +919,8 @@ def app_addaccess(auth, apps, users=[]): apps = [apps, ] for app in apps: + + app_settings = _get_app_settings(app) if not app_settings: continue @@ -927,6 +930,12 @@ def app_addaccess(auth, apps, users=[]): app_settings['mode'] = 'private' if app_settings['mode'] == 'private': + + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_addaccess', related_to) + uo.start() + allowed_users = set() if 'allowed_users' in app_settings: allowed_users = set(app_settings['allowed_users'].split(',')) @@ -939,11 +948,14 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) + uo.related_to.add(('user', allowed_user)) new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) + uo.success() + result[app] = allowed_users app_ssowatconf(auth) @@ -980,6 +992,12 @@ def app_removeaccess(auth, apps, users=[]): allowed_users = set() if app_settings.get('skipped_uris', '') != '/': + + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_removeaccess', related_to) + uo.start() + if remove_all: pass elif 'allowed_users' in app_settings: @@ -991,12 +1009,15 @@ def app_removeaccess(auth, apps, users=[]): if allowed_user not in users: allowed_users.add(allowed_user) + uo.related_to += [ ('user', x) for x in allowed_users ] new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) result[app] = allowed_users + uo.success() + app_ssowatconf(auth) return {'allowed_users': result} @@ -1020,6 +1041,11 @@ def app_clearaccess(auth, apps): if not app_settings: continue + # Start register change on system + related_to = [('app', app)] + uo= UnitOperation('app_clearaccess', related_to) + uo.start() + if 'mode' in app_settings: app_setting(app, 'mode', delete=True) @@ -1028,6 +1054,8 @@ def app_clearaccess(auth, apps): hook_callback('post_app_clearaccess', args=[app]) + uo.success() + app_ssowatconf(auth) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 15c793802..c4b61f1f2 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1125,9 +1125,14 @@ class RestoreManager(): if system_targets == []: return + # Start register change on system + uo = UnitOperation('backup_restore_system') + uo.start() + logger.info(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() + uo.extra.env = env_dict ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1137,9 +1142,16 @@ class RestoreManager(): for part in ret['succeed'].keys(): self.targets.set_result("system", part, "Success") + error_part = [] for part in ret['failed'].keys(): logger.error(m18n.n('restore_system_part_failed', part=part)) self.targets.set_result("system", part, "Error") + error_part.append(part) + + if ret['failed']: + uo.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) + else: + uo.success() service_regen_conf() @@ -1187,6 +1199,11 @@ class RestoreManager(): else: shutil.copy2(s, d) + # Start register change on system + related_to = [('app', app_instance_name)] + uo = UnitOperation('backup_restore_app', related_to) + uo.start() + # Check if the app is not already installed if _is_installed(app_instance_name): logger.error(m18n.n('restore_already_installed_app', @@ -1229,6 +1246,8 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) + uo.extra.env = env_dict + # Execute app restore script hook_exec(restore_script, args=[app_backup_in_archive, app_instance_name], @@ -1237,8 +1256,10 @@ class RestoreManager(): env=env_dict, user="root") except: - logger.exception(m18n.n('restore_app_failed', - app=app_instance_name)) + msg = m18n.n('restore_app_failed',app=app_instance_name) + logger.exception(msg) + uo.error(msg) + self.targets.set_result("apps", app_instance_name, "Error") remove_script = os.path.join(app_scripts_in_archive, 'remove') @@ -1250,12 +1271,20 @@ class RestoreManager(): env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) + uo = UnitOperation('remove_on_failed_restore', + [('app', app_instance_name)], + env=env_dict_remove) + uo.start() + # Execute remove script # TODO: call app_remove instead if hook_exec(remove_script, args=[app_instance_name], env=env_dict_remove, user="root") != 0: - logger.warning(m18n.n('app_not_properly_removed', - app=app_instance_name)) + msg = m18n.n('app_not_properly_removed', app=app_instance_name) + logger.warning(msg) + uo.error(msg) + else: + uo.success() # Cleaning app directory shutil.rmtree(app_settings_new_path, ignore_errors=True) @@ -1263,6 +1292,7 @@ class RestoreManager(): # TODO Cleaning app hooks else: self.targets.set_result("apps", app_instance_name, "Success") + uo.success() finally: # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index cc65fe237..d90965d3e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -274,7 +274,8 @@ def service_log(name, number=50): return result -def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, +@is_unit_operation('names:service', auto=False) +def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ Regenerate the configuration file(s) for a service @@ -289,6 +290,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, """ result = {} + if not dry_run: + uo.start() # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) @@ -482,6 +485,8 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, return post_args + [regen_conf_files, ] hook_callback('conf_regen', names, pre_callback=_pre_call) + uo.success() + return result diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index eefe2d28d..8ffeaecf4 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -834,20 +834,31 @@ def tools_migrations_migrate(target=None, skip=False): # effectively run selected migrations for migration in migrations: + + # Start register change on system + uo= UnitOperation('tools_migrations_migrate_' + mode) + uo.start() + if not skip: logger.warn(m18n.n('migrations_show_currently_running_migration', **migration)) try: if mode == "forward": - migration["module"].MyMigration().migrate() + m = migration["module"].MyMigration() + m.uo = uo + m.migrate() elif mode == "backward": - migration["module"].MyMigration().backward() + m = migration["module"].MyMigration() + m.uo = uo + m.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) except Exception as e: # migration failed, let's stop here but still update state because # we managed to run the previous ones - logger.error(m18n.n('migrations_migration_has_failed', exception=e, **migration), exc_info=1) + msg = m18n.n('migrations_migration_has_failed', exception=e, **migration) + logger.error(msg, exc_info=1) + uo.error(msg) break else: # if skip @@ -859,6 +870,8 @@ def tools_migrations_migrate(target=None, skip=False): "name": migration["name"], } + uo.success() + # special case where we want to go back from the start if target == 0: state["last_run_migration"] = None From 793b4d2f88635e53a97b55a500700abb1f858da8 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 19:18:11 +0200 Subject: [PATCH 0889/1066] [enh] Add some translations and start uo --- locales/en.json | 9 +++++++++ src/yunohost/app.py | 3 +++ src/yunohost/backup.py | 2 ++ 3 files changed, 14 insertions(+) diff --git a/locales/en.json b/locales/en.json index 9d4267de6..50992e7f9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -209,12 +209,18 @@ "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", + "log_app_addaccess": "Add access to '{}'", + "log_app_removeaccess": "Remove access to '{}'", + "log_app_clearaccess": "Remove all access to '{}'", "log_app_removelist": "Remove an application list", "log_app_change_url": "Change the url of '{}' application", "log_app_install": "Install '{}' application", "log_app_remove": "Remove '{}' application", "log_app_upgrade": "Upgrade '{}' application", "log_app_makedefault": "Make '{}' as default application", + "log_backup_restore_system": "Restore system from a backup archive", + "log_backup_restore_app": "Restore '{}' from a backup archive", + "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", "log_domain_add": "Add '{}' domain into system configuration", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", @@ -222,10 +228,13 @@ "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", + "log_service_regen_conf": "Regenerate system configurations '{}'", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", "log_user_update": "Update information of '{}' user", "log_tools_maindomain": "Make '{}' as main domain", + "log_tools_migrations_migrate_forward": "Migrate forward", + "log_tools_migrations_migrate_backward": "Migrate backward", "log_tools_postinstall": "Postinstall your YunoHost server", "log_tools_upgrade": "Upgrade debian packages", "log_tools_shutdown": "Shutdown your server", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4273471a0..86fe1657a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -800,6 +800,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False uo_remove = UnitOperation('remove_on_failed_install', [('app', app_instance_name)], env=env_dict_remove) + uo_remove.start() remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), @@ -950,6 +951,7 @@ def app_addaccess(auth, apps, users=[]): allowed_users.add(allowed_user) uo.related_to.add(('user', allowed_user)) + uo.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) @@ -1010,6 +1012,7 @@ def app_removeaccess(auth, apps, users=[]): allowed_users.add(allowed_user) uo.related_to += [ ('user', x) for x in allowed_users ] + uo.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c4b61f1f2..a95d75a45 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1133,6 +1133,7 @@ class RestoreManager(): env_dict = self._get_env_var() uo.extra.env = env_dict + uo.flush() ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1247,6 +1248,7 @@ class RestoreManager(): env_dict = self._get_env_var(app_instance_name) uo.extra.env = env_dict + uo.flush() # Execute app restore script hook_exec(restore_script, From ab816f292e4067c0188d3f93a2bc1f6dc69240df Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 20:35:45 +0200 Subject: [PATCH 0890/1066] [enh] Display log message to help to get log --- locales/en.json | 5 +++++ src/yunohost/log.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 50992e7f9..606e224d2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,6 +207,11 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", + "log_link_to_log": "Complete log of this operation: '{desc}'", + "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", + "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", + "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", "log_app_addaccess": "Add access to '{}'", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7adee7e73..9cfa0a7e6 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -38,6 +38,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger CATEGORIES_PATH = '/var/log/yunohost/categories/' +OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ 'app'] METADATA_FILE_EXT = '.yml' @@ -219,10 +220,10 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password entity = entity[0] if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): - related_to.append({entity_type: kwargs[entity]}) + related_to.append((entity_type, kwargs[entity])) else: for x in kwargs[entity]: - related_to.append({entity_type: kwargs[x]}) + related_to.append((entity_type, kwargs[x])) context = kwargs.copy() @@ -314,7 +315,7 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - name += self.related_to[0].values() + name += [self.related_to[0][1]] return '-'.join(name) @property @@ -361,6 +362,20 @@ class UnitOperation(object): self._success = error is None if self.logger is not None: self.logger.removeHandler(self.file_handler) + + is_api = msettings.get('interface') == 'api' + desc = _get_description_from_name(self.name) + if error is None: + if is_api: + logger.info(m18n.n('log_link_to_log', name=self.name, desc=desc)) + else: + logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) + else: + if is_api: + logger.warning(m18n.n('log_link_to_failed_log', name=self.name, desc=desc)) + else: + logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) + self.flush() def __del__(self): @@ -371,3 +386,12 @@ class UnitOperation(object): """ self.error(m18n.n('log_operation_unit_unclosed_properly')) +def _get_description_from_name(name): + parts = name.split("-") + try: + datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") + except ValueError: + return m18n.n("log_" + parts[0], *parts[1:]) + else: + return m18n.n("log_" + parts[2], *parts[3:]) + From cad0b9ba1fa55ec323b02a8be627bc202d76b8c1 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 20:58:24 +0200 Subject: [PATCH 0891/1066] [enh] Improve help to get log message --- locales/en.json | 2 +- src/yunohost/log.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 606e224d2..495c95f82 100644 --- a/locales/en.json +++ b/locales/en.json @@ -207,7 +207,7 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", - "log_link_to_log": "Complete log of this operation: '{desc}'", + "log_link_to_log": "Complete log of this operation: '{desc}'", "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 9cfa0a7e6..9ced6cc22 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -372,7 +372,9 @@ class UnitOperation(object): logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) else: if is_api: - logger.warning(m18n.n('log_link_to_failed_log', name=self.name, desc=desc)) + logger.warning("" + m18n.n('log_link_to_failed_log', + name=self.name, desc=desc) + + "") else: logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) From 9d59c8555a0afcbceb42120354a1ebaf22946208 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 21:36:28 +0200 Subject: [PATCH 0892/1066] [enh] Display log help at the end of logs --- locales/en.json | 1 + src/yunohost/app.py | 9 +++++---- src/yunohost/log.py | 17 +++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/locales/en.json b/locales/en.json index 495c95f82..0ba225e79 100644 --- a/locales/en.json +++ b/locales/en.json @@ -226,6 +226,7 @@ "log_backup_restore_system": "Restore system from a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", + "log_remove_on_failed_install": "Remove '{}' after a failed installation", "log_domain_add": "Add '{}' domain into system configuration", "log_domain_remove": "Remove '{}' domain from system configuration", "log_dyndns_subscribe": "Subscribe to a YunoHost subdomain '{}'", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 86fe1657a..570c2173d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -788,7 +788,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - uo.error(m18n.n('unexpected_error')) + error_msg = uo.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -821,9 +821,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False app_ssowatconf(auth) if install_retcode == -1: - raise MoulinetteError(errno.EINTR, - m18n.g('operation_interrupted')) - raise MoulinetteError(errno.EIO, m18n.n('installation_failed')) + msg = m18n.n('operation_interrupted') + " " + error_msg + raise MoulinetteError(errno.EINTR, msg) + msg = m18n.n('installation_failed') + " " + error_msg + raise MoulinetteError(errno.EIO, msg) # Clean hooks and add new ones hook_remove(app_instance_name) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 9ced6cc22..588052058 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -349,7 +349,7 @@ class UnitOperation(object): """ Declare the failure of the unit operation """ - self.close(error) + return self.close(error) def close(self, error=None): """ @@ -367,18 +367,19 @@ class UnitOperation(object): desc = _get_description_from_name(self.name) if error is None: if is_api: - logger.info(m18n.n('log_link_to_log', name=self.name, desc=desc)) + msg = m18n.n('log_link_to_log', name=self.name, desc=desc) else: - logger.info(m18n.n('log_help_to_get_log', name=self.name, desc=desc)) + msg = m18n.n('log_help_to_get_log', name=self.name, desc=desc) + logger.info(msg) else: if is_api: - logger.warning("" + m18n.n('log_link_to_failed_log', - name=self.name, desc=desc) + - "") + msg = "" + m18n.n('log_link_to_failed_log', + name=self.name, desc=desc) + "" else: - logger.warning(m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc)) - + msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) + logger.warning(msg) self.flush() + return msg def __del__(self): """ From 91caae665e5c678ef4c81d8b570e40be2a96417c Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 21:39:28 +0200 Subject: [PATCH 0893/1066] [enh] Improve end failed message on app_install --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 570c2173d..7ec1ddc80 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -823,7 +823,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg raise MoulinetteError(errno.EINTR, msg) - msg = m18n.n('installation_failed') + " " + error_msg + msg = error_msg raise MoulinetteError(errno.EIO, msg) # Clean hooks and add new ones From 97010941eddc1ea11c0590185d61f39b773b7b64 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 22:54:51 +0200 Subject: [PATCH 0894/1066] [fix] Unit operation log decorator fail on internal call --- src/yunohost/log.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 588052058..bbe2346e2 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -213,11 +213,21 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if op_key is None: op_key = func.__name__ + # In case the function is called directly from an other part of the + # code + if len(args) > 0: + from inspect import getargspec + keys = getargspec(func).args + for k, arg in enumerate(args): + kwargs[keys[k]] = arg + args = () + # Search related entity in arguments of the decorated function for entity in entities_list: entity = entity.split(':') entity_type = entity[-1] entity = entity[0] + if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): related_to.append((entity_type, kwargs[entity])) @@ -357,6 +367,8 @@ class UnitOperation(object): """ if self.ended_at is not None or self.started_at is None: return + if error is not None and not isinstance(error, basestring): + error = str(error) self.ended_at = datetime.now() self._error = error self._success = error is None From 63f8dfd87e73506988c27c1d9616cee85640b144 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 4 Jun 2018 23:04:41 +0200 Subject: [PATCH 0895/1066] [fix] Be able to create uo name with dot like domain uo --- src/yunohost/log.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bbe2346e2..d95e948a1 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -127,7 +127,10 @@ def log_display(path, number=50): if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : log_path = abs_path - base_path = os.path.splitext(abs_path)[0] + if abs_path.endswith(METADATA_FILE_EXT) or abs_path.endswith(LOG_FILE_EXT): + base_path = ''.join(os.path.splitext(abs_path)[:-1]) + else: + base_path = abs_path base_filename = os.path.basename(base_path) md_path = base_path + METADATA_FILE_EXT if log_path is None: From f64f0acb5b409688dc27f1e2ae41b3c743d9a5b7 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:16:37 +0200 Subject: [PATCH 0896/1066] [fix] Indices error due to bad yml files --- src/yunohost/log.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d95e948a1..77f20795a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -90,12 +90,12 @@ def log_list(category=[], limit=None): "name": base_filename, "path": md_path, } + entry["description"] = _get_description_from_name(base_filename) try: log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") except ValueError: - entry["description"] = m18n.n("log_" + log[0], *log[1:]), + pass else: - entry["description"] = m18n.n("log_" + log[2], *log[3:]), entry["started_at"] = log_datetime result[category].append(entry) @@ -144,13 +144,7 @@ def log_display(path, number=50): # If it's a unit operation, display the name and the description if base_path.startswith(CATEGORIES_PATH): - log = base_filename.split("-") - try: - datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") - except ValueError: - infos["description"] = m18n.n("log_" + log[0], *log[1:]), - else: - infos["description"] = m18n.n("log_" + log[2], *log[3:]), + infos["description"] = _get_description_from_name(base_filename) infos['name'] = base_filename # Display metadata if exist @@ -407,9 +401,11 @@ class UnitOperation(object): def _get_description_from_name(name): parts = name.split("-") try: - datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") - except ValueError: - return m18n.n("log_" + parts[0], *parts[1:]) - else: - return m18n.n("log_" + parts[2], *parts[3:]) - + try: + datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") + except ValueError: + return m18n.n("log_" + parts[0], *parts[1:]) + else: + return m18n.n("log_" + parts[2], *parts[3:]) + except IndexError: + return name From 9eee035a8d7d96c1454ccd5c4d7a89903d0780a3 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:32:28 +0200 Subject: [PATCH 0897/1066] [fix] Collect the last error for uo log --- src/yunohost/log.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 77f20795a..440a454fb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -248,6 +248,9 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if not auto: args = (uo,) + args result = func(*args, **kwargs) + except Exception as e: + uo.error(e) + raise e finally: # Close the unit operation if it hasn't been closed before uo.close(exc_info()[0]) From 5dbf7b567e5328a9cd2099fadb4a7439fa7f8653 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 00:58:41 +0200 Subject: [PATCH 0898/1066] [fix] Detect un declared success uo --- src/yunohost/log.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 440a454fb..6bf24eb9d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -251,9 +251,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password except Exception as e: uo.error(e) raise e - finally: - # Close the unit operation if it hasn't been closed before - uo.close(exc_info()[0]) + else: + uo.success() return result return func_wrapper return decorate From 65c18dcaa1ed3bdab154184d272ab8e3024c04e1 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:21:21 +0200 Subject: [PATCH 0899/1066] [fix] Internal call with uo args --- src/yunohost/log.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 6bf24eb9d..d09a943cf 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -215,6 +215,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if len(args) > 0: from inspect import getargspec keys = getargspec(func).args + keys.remove('uo') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -250,6 +251,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) + import pdb;pdb.set_trace() raise e else: uo.success() From 312429d92a0da466691645b61d4fbe9419750aae Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:25:37 +0200 Subject: [PATCH 0900/1066] [fix] Reraise correctly the error --- src/yunohost/log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d09a943cf..d55bf963d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -251,8 +251,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) - import pdb;pdb.set_trace() - raise e + t, v, tb = exc_info() + raise t, v, tb else: uo.success() return result From 031ff9922376f89acebbeac6d0d7a6468a625cb5 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 11:36:11 +0200 Subject: [PATCH 0901/1066] [fix] List arguments not correctly managed --- src/yunohost/log.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d55bf963d..ef675e3ef 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -215,7 +215,8 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password if len(args) > 0: from inspect import getargspec keys = getargspec(func).args - keys.remove('uo') + if 'uo' in keys: + keys.remove('uo') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -231,7 +232,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password related_to.append((entity_type, kwargs[entity])) else: for x in kwargs[entity]: - related_to.append((entity_type, kwargs[x])) + related_to.append((entity_type, x)) context = kwargs.copy() From a54f70d9f5d3e5ffe024582b573cf5124f7cb0a4 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 13:15:34 +0200 Subject: [PATCH 0902/1066] [fix] Display error correctly --- src/yunohost/domain.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 774ab928e..22b5a33ee 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -116,13 +116,16 @@ def domain_add(auth, domain, dyndns=False): service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) app_ssowatconf(auth) - except: + except Exception, e: + from sys import exc_info; + t, v, tb = exc_info() + # Force domain removal silently try: domain_remove(auth, domain, True) except: pass - raise + raise t, v, tb hook_callback('post_domain_add', args=[domain]) From 6601fb13fcac35900e9c44945e46ad5554c956db Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 13:16:40 +0200 Subject: [PATCH 0903/1066] [fix] Replace dict by tuple --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 69043bdbd..8fd9677a1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -162,7 +162,7 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: - uo = UnitOperation('selfsigned_cert_install', [{'domain', domain}], + uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], args={'force': force}) uo.start() From 65d8d88a097dea18cc78fe4e7cb7b4af130d4089 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 16:32:12 +0200 Subject: [PATCH 0904/1066] [fix] Some unit operation setup --- locales/en.json | 2 ++ src/yunohost/app.py | 11 +++++++---- src/yunohost/backup.py | 5 +++-- src/yunohost/certificate.py | 4 ++-- src/yunohost/log.py | 7 +++++-- src/yunohost/tools.py | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index 0ba225e79..1ca2dabae 100644 --- a/locales/en.json +++ b/locales/en.json @@ -217,6 +217,7 @@ "log_app_addaccess": "Add access to '{}'", "log_app_removeaccess": "Remove access to '{}'", "log_app_clearaccess": "Remove all access to '{}'", + "log_app_fetchlist": "Add an application list", "log_app_removelist": "Remove an application list", "log_app_change_url": "Change the url of '{}' application", "log_app_install": "Install '{}' application", @@ -234,6 +235,7 @@ "log_letsencrypt_cert_install": "Install Let's encrypt certificate on '{}' domain", "log_selfsigned_cert_install": "Install self signed certificate on '{}' domain", "log_letsencrypt_cert_renew": "Renew '{}' Let's encrypt certificate", + "log_service_enable": "Enable '{}' service", "log_service_regen_conf": "Regenerate system configurations '{}'", "log_user_create": "Add '{}' user", "log_user_delete": "Delete '{}' user", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7ec1ddc80..9dc0cea6f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,7 +44,7 @@ from moulinette.utils.log import getActionLogger from yunohost.service import service_log, _run_service_command from yunohost.utils import packages -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation logger = getActionLogger('yunohost.app') @@ -110,10 +110,13 @@ def app_fetchlist(url=None, name=None): # the fetch only this list if url is not None: if name: + uo = UnitOperation('app_fetchlist') + uo.start() _register_new_appslist(url, name) # Refresh the appslists dict appslists = _read_appslist_list() appslists_to_be_fetched = [name] + uo.success() else: raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) @@ -622,7 +625,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('app_upgrade', related_to, env=env_dict) + uo = unitoperation('app_upgrade', related_to, env=env_dict) uo.start() # Execute App upgrade script @@ -950,7 +953,7 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) - uo.related_to.add(('user', allowed_user)) + uo.related_to.append(('user', allowed_user)) uo.flush() new_users = ','.join(allowed_users) @@ -1010,7 +1013,7 @@ def app_removeaccess(auth, apps, users=[]): else: for allowed_user in user_list(auth)['users'].keys(): if allowed_user not in users: - allowed_users.add(allowed_user) + allowed_users.append(allowed_user) uo.related_to += [ ('user', x) for x in allowed_users ] uo.flush() diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a95d75a45..029f2d6b5 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,6 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf +from yunohost.log import UnitOperation, is_unit_operation BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH @@ -1132,7 +1133,7 @@ class RestoreManager(): logger.info(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() - uo.extra.env = env_dict + uo.extra['env'] = env_dict uo.flush() ret = hook_callback('restore', system_targets, @@ -1247,7 +1248,7 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) - uo.extra.env = env_dict + uo.extra['env'] = env_dict uo.flush() # Execute app restore script diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8fd9677a1..aada9de8f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -287,7 +287,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [{'domain', domain}], + uo = UnitOperation('letsencrypt_cert_install', [{'domain': domain}], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -388,7 +388,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [{'domain', domain}], + uo = UnitOperation('letsencrypt_cert_renew', [{'domain': domain}], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() diff --git a/src/yunohost/log.py b/src/yunohost/log.py index ef675e3ef..bcfb93dfb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -68,7 +68,7 @@ def log_list(category=[], limit=None): category_path = os.path.join(CATEGORIES_PATH, category) if not os.path.exists(category_path): - logger.warning(m18n.n('log_category_404', category=category)) + logger.debug(m18n.n('log_category_404', category=category)) continue @@ -327,7 +327,10 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - name += [self.related_to[0][1]] + if isinstance(self.related_to[0], tuple): + name += [self.related_to[0][1]] + else: + name += self.related_to[0].values() return '-'.join(name) @property diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8ffeaecf4..649fc27a1 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,7 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip -from yunohost.log import is_unit_operation +from yunohost.log import is_unit_operation, UnitOperation # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' From 9e1dac99f723132a6c2fd652be726db093014ea8 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 5 Jun 2018 17:38:46 +0200 Subject: [PATCH 0905/1066] [fix] Avoid double log if name change --- src/yunohost/log.py | 7 ++++++- src/yunohost/service.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index bcfb93dfb..859e4b43a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -278,6 +278,7 @@ class UnitOperation(object): self.started_at = None self.ended_at = None self.logger = None + self._name = None self.path = OPERATIONS_PATH @@ -324,6 +325,9 @@ class UnitOperation(object): Name of the operation This name is used as filename, so don't use space """ + if self._name is not None: + return self._name + name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: @@ -331,7 +335,8 @@ class UnitOperation(object): name += [self.related_to[0][1]] else: name += self.related_to[0].values() - return '-'.join(name) + self._name = '-'.join(name) + return self._name @property def metadata(self): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index d90965d3e..6306f0b8a 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -290,8 +290,6 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False """ result = {} - if not dry_run: - uo.start() # Return the list of pending conf if list_pending: pending_conf = _get_pending_conf(names) @@ -305,6 +303,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False } return pending_conf + if not dry_run: + uo.related_to = [('service', x) for x in names] + if not names: + uo.related_to = [('service', 'all')] + uo.start() + # Clean pending conf directory if os.path.isdir(PENDING_CONF_DIR): if not names: @@ -340,8 +344,13 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True + uo.related_to = [] + # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): + if not dry_run: + uo.related_to.append(('service', service)) + logger.info(m18n.n( 'service_regenconf_pending_applying' if not dry_run else 'service_regenconf_dry_pending_applying', From e05b8549b92b9887348420f8d8860b9af330530e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 5 Jun 2018 18:22:50 +0200 Subject: [PATCH 0906/1066] Update changelog --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 60467eec7..9bd67a3f8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (2.7.13.4) testing; urgency=low + + * Fix a bug for services with alternate names (mysql<->mariadb) + * Fix a bug in regen conf when computing diff with files that don't exists + * Increase backup filename length + + (Fixes by Bram <3) + + -- Alexandre Aubin Tue, 05 Jun 2018 18:22:00 +0000 + yunohost (2.7.13.3) testing; urgency=low * [enh] Add postgresql helpers (#238) From e56f8dafbb73ae9553a39e979e36e8840cebb2b0 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 6 Jun 2018 09:52:18 +0200 Subject: [PATCH 0907/1066] [fix] Bad call --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9dc0cea6f..8ad57b636 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -625,7 +625,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = unitoperation('app_upgrade', related_to, env=env_dict) + uo = UnitOperation('app_upgrade', related_to, env=env_dict) uo.start() # Execute App upgrade script From ffc773c856c6c5f23391203b73bc2c1b92d96df3 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 7 Jun 2018 13:51:10 +0200 Subject: [PATCH 0908/1066] [mod] we moved away from redmine --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9aed880ac..4033bd6fb 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This repository is the core of YunoHost code. ## Issues -- [Please report issues on YunoHost bugtracker](https://dev.yunohost.org/projects/yunohost/issues) (no registration needed). +- [Please report issues on YunoHost bugtracker](https://github.com/YunoHost/issues). ## Contribute - You can develop on this repository using [ynh-dev tool](https://github.com/YunoHost/ynh-dev) with `use-git` sub-command. From cc6819691f7866f3cba5cb370537ed2085168876 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 7 Jun 2018 14:00:04 +0200 Subject: [PATCH 0909/1066] [mod] we moved away from redmine --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a4ab8db7b..18a8be84b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1746,7 +1746,7 @@ def _check_manifest_requirements(manifest, app_instance_name): # Validate multi-instance app if is_true(manifest.get('multi_instance', False)): # Handle backward-incompatible change introduced in yunohost >= 2.3.6 - # See https://dev.yunohost.org/issues/156 + # See https://github.com/YunoHost/issues/issues/156 yunohost_req = requirements.get('yunohost', None) if (not yunohost_req or not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'): From e11730c1e3bb0869093e23c17377edc6ec8afcb6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 10 Jun 2018 18:07:51 +0200 Subject: [PATCH 0910/1066] Fix service description for php7.0 --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 1c120661d..85745e1fb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -348,7 +348,7 @@ "service_description_mysql": "stores applications data (SQL database)", "service_description_nginx": "serves or provides access to all the websites hosted on your server", "service_description_nslcd": "handles YunoHost user shell connection", - "service_description_php5-fpm": "runs applications written in PHP with nginx", + "service_description_php7.0-fpm": "runs applications written in PHP with nginx", "service_description_postfix": "used to send and receive emails", "service_description_redis-server": "a specialized database used for rapid data access, task queue and communication between programs", "service_description_rmilter": "checks various parameters in emails", From 8e8d8e54ab359e2a6e0dc5356ed251cb71a4797e Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 11 Jun 2018 11:56:54 +0200 Subject: [PATCH 0911/1066] [enh] Enable ynh_info by default --- src/yunohost/app.py | 13 +++++-------- src/yunohost/hook.py | 9 +++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 24229571d..e0467d7a7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -490,8 +490,7 @@ def app_change_url(auth, app, domain, path): # XXX journal if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), - args=args_list, env=env_dict, user="root", - enable_stdinfo=True) != 0: + args=args_list, env=env_dict, user="root") != 0: logger.error("Failed to change '%s' url." % app) # restore values modified by app_checkurl @@ -606,8 +605,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) if hook_exec(extracted_app_folder + '/scripts/upgrade', - args=args_list, env=env_dict, user="root", - enable_stdinfo=True) != 0: + args=args_list, env=env_dict, user="root") != 0: logger.error(m18n.n('app_upgrade_failed', app=app_instance_name)) else: now = int(time.time()) @@ -742,7 +740,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict, user="root", enable_stdinfo=True) + args=args_list, env=env_dict, user="root") except (KeyboardInterrupt, EOFError): install_retcode = -1 except: @@ -759,8 +757,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): # Execute remove script remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove, user="root", - enable_stdinfo=True) + args=[app_instance_name], env=env_dict_remove, user="root") if remove_retcode != 0: logger.warning(m18n.n('app_not_properly_removed', app=app_instance_name)) @@ -832,7 +829,7 @@ def app_remove(auth, app): env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, - env=env_dict, user="root", enable_stdinfo=True) == 0: + env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) if os.path.exists(app_setting_path): diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 489cebd5d..94ba0de16 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -298,7 +298,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="admin", enable_stdinfo=False): + chdir=None, env=None, user="admin"): """ Execute hook from a file with arguments @@ -337,11 +337,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, env = {} env['YNH_CWD'] = chdir - if enable_stdinfo: - stdinfo = os.path.join(tempfile.mkdtemp(), "stdinfo") - env['YNH_STDINFO'] = stdinfo - else: - stdinfo = None + stdinfo = os.path.join(tempfile.mkdtemp(), "stdinfo") + env['YNH_STDINFO'] = stdinfo # Construct command to execute if user == "root": From 2adc80f8db5138e354fae509b24d837d170a67cf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 16:18:38 +0200 Subject: [PATCH 0912/1066] [fix] Microdecision : bug in get_file_diff following refactoring ... --- src/yunohost/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b56c7ad20..0ce8073fa 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -733,9 +733,9 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): else: orig_file = [] - if not os.path.exists(new_file): + if os.path.exists(new_file): with open(new_file, 'r') as new_file: - new_file.readlines() + new_file = new_file.readlines() else: new_file = [] From 3681e6adfcf33b4a4643c310ac0891fe0d87d07f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 17:45:17 +0200 Subject: [PATCH 0913/1066] Attempt to make postgresql not send an email about the 9.4->9.6 migration --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index b2fcd08ac..8b8d37447 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -228,6 +228,8 @@ class MyMigration(Migration): # Make apt-get happy os.system("echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections") + # Don't send an email to root about the postgresql migration. It should be handled automatically after. + os.system("echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections") command = "" command += " DEBIAN_FRONTEND=noninteractive" From 18330ab042f6c784583c9b673cee1bef69b4ac5c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 18:15:19 +0200 Subject: [PATCH 0914/1066] Check available space in /var/lib/postgresql before running postgresql migration --- locales/en.json | 1 + src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py | 7 +++---- src/yunohost/utils/filesystem.py | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index 85745e1fb..5eb977713 100644 --- a/locales/en.json +++ b/locales/en.json @@ -242,6 +242,7 @@ "migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten at the end of the upgrade : {manually_modified_files}", "migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", + "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index a6bfafcf2..9df51979d 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -5,6 +5,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration +from yunohost.utils.filesystem import free_space_in_directory, space_used_by_directory logger = getActionLogger('yunohost.migration') @@ -12,7 +13,6 @@ logger = getActionLogger('yunohost.migration') class MyMigration(Migration): "Migrate DBs from Postgresql 9.4 to 9.6 after migrating to Stretch" - def migrate(self): if not self.package_is_installed("postgresql-9.4"): @@ -22,8 +22,8 @@ class MyMigration(Migration): if not self.package_is_installed("postgresql-9.6"): raise MoulinetteError(m18n.n("migration_0005_postgresql_96_not_installed")) - # FIXME / TODO : maybe add checks about the size of - # /var/lib/postgresql/9.4/main/base/ compared to available space ? + if not space_used_by_directory("/var/lib/postgresql/9.4") < free_space_in_directory("/var/lib/postgresql"): + raise MoulinetteError(m18n.n("migration_0005_not_enough_space", path="/var/lib/postgresql/")) subprocess.check_call("service postgresql stop", shell=True) subprocess.check_call("pg_dropcluster --stop 9.6 main", shell=True) @@ -35,7 +35,6 @@ class MyMigration(Migration): pass - def package_is_installed(self, package_name): p = subprocess.Popen("dpkg --list | grep -q -w {}".format(package_name), shell=True) diff --git a/src/yunohost/utils/filesystem.py b/src/yunohost/utils/filesystem.py index 9b39f5daa..3f026f980 100644 --- a/src/yunohost/utils/filesystem.py +++ b/src/yunohost/utils/filesystem.py @@ -23,3 +23,7 @@ import os def free_space_in_directory(dirpath): stat = os.statvfs(dirpath) return stat.f_frsize * stat.f_bavail + +def space_used_by_directory(dirpath): + stat = os.statvfs(dirpath) + return stat.f_frsize * stat.f_blocks From 0f6d4de8e469471f4042da942e8bf4774c1b6102 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 18:23:38 +0200 Subject: [PATCH 0915/1066] Remove the logrotate for php5-fpm --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index b6a36e44b..1afbf16b7 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -56,6 +56,7 @@ class MyMigration(Migration): # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") os.system("systemctl stop php5-fpm") + os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy # Get list of nginx conf file nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") From d55c029c9b27403a7ac2ed9aff02e3d683c62a7f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 21:58:01 +0200 Subject: [PATCH 0916/1066] Don't open old IMAP port (465) --- data/templates/yunohost/firewall.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/templates/yunohost/firewall.yml b/data/templates/yunohost/firewall.yml index 201a39092..835a82519 100644 --- a/data/templates/yunohost/firewall.yml +++ b/data/templates/yunohost/firewall.yml @@ -1,10 +1,10 @@ uPnP: enabled: false - TCP: [22, 25, 80, 443, 465, 587, 993, 5222, 5269] + TCP: [22, 25, 80, 443, 587, 993, 5222, 5269] UDP: [] ipv4: - TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] + TCP: [22, 25, 53, 80, 443, 587, 993, 5222, 5269] UDP: [53, 5353] ipv6: - TCP: [22, 25, 53, 80, 443, 465, 587, 993, 5222, 5269] + TCP: [22, 25, 53, 80, 443, 587, 993, 5222, 5269] UDP: [53, 5353] From 0ebfa145b8ae7184cdc78638949013457803c39a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 23:40:23 +0200 Subject: [PATCH 0917/1066] Close port 465, open 587 during migration according to SMTP port change --- locales/en.json | 2 +- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index e7bfd2ded..37160d403 100644 --- a/locales/en.json +++ b/locales/en.json @@ -235,7 +235,7 @@ "migration_0003_not_jessie": "The current debian distribution is not Jessie !", "migration_0003_system_not_fully_up_to_date": "Your system is not fully up to date. Please perform a regular upgrade before running the migration to stretch.", "migration_0003_still_on_jessie_after_main_upgrade": "Something wrong happened during the main upgrade : system is still on Jessie !? To investigate the issue, please look at {log} :s ...", - "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.", + "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app. More infos on https://yunohost.org/backup ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.\n\nAdditionally, the port for SMTP, used by external email clients like (Thunderbird or K9-Mail) was changed from 465 (SSL/TLS) to 587 (STARTTLS). The old port 465 will automatically be closed and the new port 587 will be opened in the firewall. You and your users *will* have to adapt the configuration of your email clients accordingly!", "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an applist or are not flagged as 'working'. Consequently, we cannot guarantee that they will still work after the upgrade : {problematic_apps}", "migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten at the end of the upgrade : {manually_modified_files}", "migrations_backward": "Migrating backward.", diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 8b8d37447..6900b678a 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -15,6 +15,7 @@ from yunohost.service import (_run_service_command, manually_modified_files_compared_to_debian_default) from yunohost.utils.filesystem import free_space_in_directory from yunohost.utils.packages import get_installed_version +from yunohost.firewall import firewall_allow, firewall_disallow logger = getActionLogger('yunohost.migration') @@ -72,6 +73,11 @@ class MyMigration(Migration): os.system("apt autoremove --assume-yes") os.system("apt clean --assume-yes") + # We moved to port 587 for SMTP + # https://busylog.net/smtp-tls-ssl-25-465-587/ + firewall_allow("Both", 587) + firewall_disallow("Both", 465) + # Upgrade yunohost packages logger.warning(m18n.n("migration_0003_yunohost_upgrade")) self.restore_files_to_keep() From a895662693133d3da06afa56cf43478fc8051ae4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jun 2018 23:42:18 +0200 Subject: [PATCH 0918/1066] Explicitly enable php7.0-fpm and disable php5-fpm during php migration --- src/yunohost/data_migrations/0004_php5_to_php7_pools.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 1afbf16b7..0237ddb38 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -55,7 +55,9 @@ class MyMigration(Migration): # Reload/restart the php pools _run_service_command("restart", "php7.0-fpm") + _run_service_command("enable", "php7.0-fpm") os.system("systemctl stop php5-fpm") + os.system("systemctl disable php5-fpm") os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy # Get list of nginx conf file From f96aa4450537dec77c4c1479bbdb1f36ad24d3c0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 00:05:47 +0200 Subject: [PATCH 0919/1066] Rely on the codename instead of release number because lsb_release is fokin stupid --- .../data_migrations/0003_migrate_to_stretch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 6900b678a..add511a81 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -55,7 +55,7 @@ class MyMigration(Migration): _run_service_command("stop", "mysql") self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") - if self.debian_major_version() == 8: + if self.debian_major_version() == "jessie": raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) # Specific upgrade for fail2ban... @@ -89,7 +89,10 @@ class MyMigration(Migration): # because "platform" relies on uname, which on some weird setups does # not behave correctly (still says running Jessie when lsb_release says # Stretch...) - return int(check_output("lsb_release -r").split("\t")[1][0]) + # Also lsb_release is fucking stupid and sometimes return "Release: 8" + # and "Codename: stretch". So apparently the codename is more reliable + # than the release number :| + return check_output("lsb_release -c").split("\t")[1].strip() def yunohost_major_version(self): return int(get_installed_version("yunohost").split('.')[0]) @@ -100,7 +103,7 @@ class MyMigration(Migration): # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... - if not self.debian_major_version() == 8 \ + if not self.debian_major_version() == "jessie" \ and not self.yunohost_major_version() == 2: raise MoulinetteError(m18n.n("migration_0003_not_jessie")) @@ -125,7 +128,7 @@ class MyMigration(Migration): # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... - if not self.debian_major_version() == 8 \ + if not self.debian_major_version() == "jessie" \ and not self.yunohost_major_version() == 2: return None From 201edd8c38b7ab333f05e38ee00a3a1e4b517504 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 00:29:30 +0200 Subject: [PATCH 0920/1066] Revert "Rely on the codename instead of release number because lsb_release is fokin stupid" This reverts commit f96aa4450537dec77c4c1479bbdb1f36ad24d3c0. --- .../data_migrations/0003_migrate_to_stretch.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index add511a81..6900b678a 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -55,7 +55,7 @@ class MyMigration(Migration): _run_service_command("stop", "mysql") self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") - if self.debian_major_version() == "jessie": + if self.debian_major_version() == 8: raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) # Specific upgrade for fail2ban... @@ -89,10 +89,7 @@ class MyMigration(Migration): # because "platform" relies on uname, which on some weird setups does # not behave correctly (still says running Jessie when lsb_release says # Stretch...) - # Also lsb_release is fucking stupid and sometimes return "Release: 8" - # and "Codename: stretch". So apparently the codename is more reliable - # than the release number :| - return check_output("lsb_release -c").split("\t")[1].strip() + return int(check_output("lsb_release -r").split("\t")[1][0]) def yunohost_major_version(self): return int(get_installed_version("yunohost").split('.')[0]) @@ -103,7 +100,7 @@ class MyMigration(Migration): # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... - if not self.debian_major_version() == "jessie" \ + if not self.debian_major_version() == 8 \ and not self.yunohost_major_version() == 2: raise MoulinetteError(m18n.n("migration_0003_not_jessie")) @@ -128,7 +125,7 @@ class MyMigration(Migration): # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be >= 9.x but yunohost package # would still be in 2.x... - if not self.debian_major_version() == "jessie" \ + if not self.debian_major_version() == 8 \ and not self.yunohost_major_version() == 2: return None From 1b55eacf9cd1e41f65e222d830ed760eba012b9c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 00:29:52 +0200 Subject: [PATCH 0921/1066] Rely on /etc/os-release to get the release number.. --- .../data_migrations/0003_migrate_to_stretch.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 6900b678a..49e85a64e 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -85,11 +85,13 @@ class MyMigration(Migration): self.upgrade_yunohost_packages() def debian_major_version(self): - # We rely on lsb_release instead of the python module "platform", - # because "platform" relies on uname, which on some weird setups does - # not behave correctly (still says running Jessie when lsb_release says - # Stretch...) - return int(check_output("lsb_release -r").split("\t")[1][0]) + # The python module "platform" and lsb_release are not reliable because + # on some setup, they still return Release=8 even after upgrading to + # stretch ... (Apparently this is related to OVH overriding some stuff + # with /etc/lsb-release for instance -_-) + # Instead, we rely on /etc/os-release which should be the raw info from + # the distribution... + return int(check_output("grep VERSION_ID /etc/os-release | tr '\"' ' ' | cut -d ' ' -f2")) def yunohost_major_version(self): return int(get_installed_version("yunohost").split('.')[0]) From 98862ee25613f8d8aaf92b71e145de8692495c3c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 01:04:04 +0200 Subject: [PATCH 0922/1066] Update changelog --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9bd67a3f8..2c855eae1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (2.7.13.5) testing; urgency=low + + * [fix] a bug when log to be fetched is empty + * [fix] a bug when computing diff in regen_conf + * [stretch-migration] Tell postgresql-common to not send an email about 9.4->9.6 migration + * [stretch-migration] Close port 465 / open port 587 during migration according to SMTP port change in postfix + * [stretch-migration] Rely on /etc/os-release to get debian release number + + Fixes by Bram and Aleks + + -- Alexandre Aubin Tue, 12 Jun 2018 01:00:00 +0000 + yunohost (2.7.13.4) testing; urgency=low * Fix a bug for services with alternate names (mysql<->mariadb) From 11fee7d6a5798a77ba1bd07f7d546299a5e9f96e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 01:12:01 +0200 Subject: [PATCH 0923/1066] Update changelog --- debian/changelog | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/debian/changelog b/debian/changelog index 1254ac2f5..5c3cd8b53 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,10 +1,16 @@ +yunohost (3.0.0~beta1.6) testing; urgency=low + + * [fix] Service description for php7.0-fpm + * [fix] Remove old logrotate for php5-fpm during migration + * [fix] Explicitly enable php7.0-fpm and disable php5-fpm during migration + * [fix] Don't open the old SMTP port anymore (465) + * [enh] Check space available before running the postgresql migration + + -- Alexandre Aubin Tue, 12 Jun 2018 01:00:00 +0000 + yunohost (3.0.0~beta1.5) testing; urgency=low - * Fix a bug for services with alternate names (mysql<->mariadb) - * Fix a bug in regen conf when computing diff with files that don't exists - * Increase backup filename length - - (Fixes by Bram <3) + * (c.f. 2.7.13.4) -- Alexandre Aubin Mon, 02 Jun 2018 00:14:00 +0000 From abf0b6c0d9a37c3b790bc03f299da67eea7f5b90 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 12 Jun 2018 23:22:31 +0200 Subject: [PATCH 0924/1066] Typo --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 37160d403..4ca65826c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -235,7 +235,7 @@ "migration_0003_not_jessie": "The current debian distribution is not Jessie !", "migration_0003_system_not_fully_up_to_date": "Your system is not fully up to date. Please perform a regular upgrade before running the migration to stretch.", "migration_0003_still_on_jessie_after_main_upgrade": "Something wrong happened during the main upgrade : system is still on Jessie !? To investigate the issue, please look at {log} :s ...", - "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app. More infos on https://yunohost.org/backup ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.\n\nAdditionally, the port for SMTP, used by external email clients like (Thunderbird or K9-Mail) was changed from 465 (SSL/TLS) to 587 (STARTTLS). The old port 465 will automatically be closed and the new port 587 will be opened in the firewall. You and your users *will* have to adapt the configuration of your email clients accordingly!", + "migration_0003_general_warning": "Please note that this migration is a delicate operation. While the YunoHost team did its best to review and test it, the migration might still break parts of the system or apps.\n\nTherefore, we recommend you to :\n - Perform a backup of any critical data or app. More infos on https://yunohost.org/backup ;\n - Be patient after launching the migration : depending on your internet connection and hardware, it might take up to a few hours for everything to upgrade.\n\nAdditionally, the port for SMTP, used by external email clients (like Thunderbird or K9-Mail) was changed from 465 (SSL/TLS) to 587 (STARTTLS). The old port 465 will automatically be closed and the new port 587 will be opened in the firewall. You and your users *will* have to adapt the configuration of your email clients accordingly!", "migration_0003_problematic_apps_warning": "Please note that the following possibly problematic installed apps were detected. It looks like those were not installed from an applist or are not flagged as 'working'. Consequently, we cannot guarantee that they will still work after the upgrade : {problematic_apps}", "migration_0003_modified_files": "Please note that the following files were found to be manually modified and might be overwritten at the end of the upgrade : {manually_modified_files}", "migrations_backward": "Migrating backward.", From 9df14c6506993ba81af186764731296af009d261 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 12 Jun 2018 23:43:40 +0200 Subject: [PATCH 0925/1066] [fix] remove slice by mistake in 14c270cebdc67a1123331237cee6dba347ee7623 --- src/yunohost/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 0ce8073fa..ef711a4a9 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -709,7 +709,7 @@ def _tail(file, n): lines = f.read().splitlines() if len(lines) >= to_read or pos == 0: - return lines[-to_read] + return lines[-to_read:] avg_line_length *= 1.3 From 8b08de665a17881052d57a676546d443564d9c25 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 13 Jun 2018 19:39:56 +0200 Subject: [PATCH 0926/1066] Add util functions to extract network interfaces and gateway --- src/yunohost/monitor.py | 2 + src/yunohost/utils/network.py | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index ed13d532d..fc10a4fbc 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -164,6 +164,7 @@ def monitor_network(units=None, human_readable=False): units = ['check', 'usage', 'infos'] # Get network devices and their addresses + # TODO / FIXME : use functions in utils/network.py to manage this devices = {} output = subprocess.check_output('ip addr show'.split()) for d in re.split('^(?:[0-9]+: )', output, flags=re.MULTILINE): @@ -213,6 +214,7 @@ def monitor_network(units=None, human_readable=False): elif u == 'infos': p_ipv4 = get_public_ip() or 'unknown' + # TODO / FIXME : use functions in utils/network.py to manage this l_ip = 'unknown' for name, addrs in devices.items(): if name == 'lo': diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index e22d1644d..470354f63 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -19,10 +19,13 @@ """ import logging +import re +import subprocess from urllib import urlopen logger = logging.getLogger('yunohost.utils.network') + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" @@ -37,3 +40,76 @@ def get_public_ip(protocol=4): return urlopen(url).read().strip() except IOError: return None + + +def get_network_interfaces(): + + # Get network devices and their addresses (raw infos from 'ip addr') + devices_raw = {} + output = subprocess.check_output('ip addr show'.split()) + for d in re.split('^(?:[0-9]+: )', output, flags=re.MULTILINE): + # Extract device name (1) and its addresses (2) + m = re.match('([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL) + if m: + devices_raw[m.group(1)] = m.group(2) + + # Parse relevant informations for each of them + devices = {name: _extract_inet(addrs) for name, addrs in devices_raw.items() if name != "lo"} + + return devices or "unknown" + + +def get_gateway(): + + output = subprocess.check_output('ip route show'.split()) + m = re.search('default via (.*) dev ([a-z]+[0-9]?)', output) + if not m: + return "unknown" + + addr = _extract_inet(m.group(1), True) + return addr.popitem()[1] if len(addr) == 1 else "unknown" + + +############################################################################### + + +def _extract_inet(string, skip_netmask=False, skip_loopback=True): + """ + Extract IP addresses (v4 and/or v6) from a string limited to one + address by protocol + + Keyword argument: + string -- String to search in + skip_netmask -- True to skip subnet mask extraction + skip_loopback -- False to include addresses reserved for the + loopback interface + + Returns: + A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6' + + """ + ip4_pattern = '((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}' + ip6_pattern = '(((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)' + ip4_pattern += '/[0-9]{1,2})' if not skip_netmask else ')' + ip6_pattern += '/[0-9]{1,3})' if not skip_netmask else ')' + result = {} + + for m in re.finditer(ip4_pattern, string): + addr = m.group(1) + if skip_loopback and addr.startswith('127.'): + continue + + # Limit to only one result + result['ipv4'] = addr + break + + for m in re.finditer(ip6_pattern, string): + addr = m.group(1) + if skip_loopback and addr == '::1': + continue + + # Limit to only one result + result['ipv6'] = addr + break + + return result From 2aeed38ea5574ae8d12df6bb0c32aea3942f53e5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 13 Jun 2018 20:01:41 +0200 Subject: [PATCH 0927/1066] Disable predictable network interface names to avoid cases where devices don't reboot properly because interface name changed... --- .../0003_migrate_to_stretch.py | 20 +++++++++++++++++++ src/yunohost/utils/network.py | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 8b8d37447..105480241 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -15,6 +15,7 @@ from yunohost.service import (_run_service_command, manually_modified_files_compared_to_debian_default) from yunohost.utils.filesystem import free_space_in_directory from yunohost.utils.packages import get_installed_version +from yunohost.utils.network import get_network_interfaces logger = getActionLogger('yunohost.migration') @@ -68,6 +69,8 @@ class MyMigration(Migration): self.apt_dist_upgrade(conf_flags=["new", "miss", "def"]) _run_service_command("restart", "fail2ban") + self.disable_predicable_interface_names() + # Clean the mess os.system("apt autoremove --assume-yes") os.system("apt clean --assume-yes") @@ -352,3 +355,20 @@ class MyMigration(Migration): # command showing in the terminal, since 'info' channel is only # enabled if the user explicitly add --verbose ... os.system(command) + + def disable_predicable_interface_names(self): + + # Try to see if currently used interface names are predictable ones or not... + # If we ain't using "eth0" or "wlan0", assume we are using predictable interface + # names and therefore they shouldnt be disabled + network_interfaces = get_network_interfaces().keys() + if "eth0" not in network_interfaces and "wlan0" not in network_interfaces: + return + + interfaces_config = read_file("/etc/network/interfaces") + if "eth0" not in interfaces_config and "wlan0" not in interfaces_config: + return + + # Disable predictive interface names + # c.f. https://unix.stackexchange.com/a/338730 + os.system("ln -s /dev/null /etc/systemd/network/99-default.link") diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 470354f63..dec0384bf 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -56,7 +56,7 @@ def get_network_interfaces(): # Parse relevant informations for each of them devices = {name: _extract_inet(addrs) for name, addrs in devices_raw.items() if name != "lo"} - return devices or "unknown" + return devices def get_gateway(): @@ -64,10 +64,10 @@ def get_gateway(): output = subprocess.check_output('ip route show'.split()) m = re.search('default via (.*) dev ([a-z]+[0-9]?)', output) if not m: - return "unknown" + return None addr = _extract_inet(m.group(1), True) - return addr.popitem()[1] if len(addr) == 1 else "unknown" + return addr.popitem()[1] if len(addr) == 1 else None ############################################################################### From 898d6659000f075f8b3edf57a204f3b8dbf1a43f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 15 Jun 2018 15:29:05 +0200 Subject: [PATCH 0928/1066] Remove archivemount stuff (#491) --- debian/control | 2 +- src/yunohost/backup.py | 89 +++++++++------------- src/yunohost/tests/test_backuprestore.py | 96 +----------------------- 3 files changed, 39 insertions(+), 148 deletions(-) diff --git a/debian/control b/debian/control index ba5f65b6c..256038598 100644 --- a/debian/control +++ b/debian/control @@ -33,7 +33,7 @@ Recommends: yunohost-admin , python-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl -Suggests: htop, vim, rsync, acpi-support-base, udisks2, archivemount +Suggests: htop, vim, rsync, acpi-support-base, udisks2 Conflicts: iptables-persistent , moulinette-yunohost, yunohost-config , yunohost-config-others, yunohost-config-postfix diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 1fe67e406..db0f44abe 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1316,9 +1316,7 @@ class BackupMethod(object): TarBackupMethod --------------- This method compresses all files to backup in a .tar.gz archive. When - restoring, it tries to mount the archive using archivemount/fuse instead - of untaring the archive. Some systems don't support fuse (for these, - it automatically falls back to untaring the required parts). + restoring, it untars the required parts. CustomBackupMethod ------------------ @@ -1687,8 +1685,7 @@ class CopyBackupMethod(BackupMethod): class TarBackupMethod(BackupMethod): """ - This class compress all files to backup in archive. To restore it try to - mount the archive with archivemount (fuse). Some system don't support fuse. + This class compress all files to backup in archive. """ def __init__(self, repo=None): @@ -1760,8 +1757,6 @@ class TarBackupMethod(BackupMethod): Exceptions: backup_archive_open_failed -- Raised if the archive can't be open - backup_archive_mount_failed -- Raised if the system don't support - archivemount """ super(TarBackupMethod, self).mount(restore_manager) @@ -1776,60 +1771,50 @@ class TarBackupMethod(BackupMethod): tar.close() # Mount the tarball + logger.debug(m18n.n("restore_extracting")) + tar = tarfile.open(self._archive_file, "r:gz") + tar.extract('info.json', path=self.work_dir) + try: - ret = subprocess.call(['archivemount', '-o', 'readonly', - self._archive_file, self.work_dir]) - except: - ret = -1 + tar.extract('backup.csv', path=self.work_dir) + except KeyError: + # Old backup archive have no backup.csv file + pass - # If archivemount failed, extract the archive - if ret != 0: - logger.warning(m18n.n('backup_archive_mount_failed')) + # Extract system parts backup + conf_extracted = False - logger.debug(m18n.n("restore_extracting")) - tar = tarfile.open(self._archive_file, "r:gz") - tar.extract('info.json', path=self.work_dir) + system_targets = self.manager.targets.list("system", exclude=["Skipped"]) + apps_targets = self.manager.targets.list("apps", exclude=["Skipped"]) - try: - tar.extract('backup.csv', path=self.work_dir) - except KeyError: - # Old backup archive have no backup.csv file - pass - - # Extract system parts backup - conf_extracted = False - - system_targets = self.manager.targets.list("system", exclude=["Skipped"]) - apps_targets = self.manager.targets.list("apps", exclude=["Skipped"]) - - for system_part in system_targets: - # Caution: conf_ynh_currenthost helpers put its files in - # conf/ynh - if system_part.startswith("conf_"): - if conf_extracted: - continue - system_part = "conf/" - conf_extracted = True - else: - system_part = system_part.replace("_", "/") + "/" - subdir_and_files = [ - tarinfo for tarinfo in tar.getmembers() - if tarinfo.name.startswith(system_part) - ] - tar.extractall(members=subdir_and_files, path=self.work_dir) + for system_part in system_targets: + # Caution: conf_ynh_currenthost helpers put its files in + # conf/ynh + if system_part.startswith("conf_"): + if conf_extracted: + continue + system_part = "conf/" + conf_extracted = True + else: + system_part = system_part.replace("_", "/") + "/" subdir_and_files = [ tarinfo for tarinfo in tar.getmembers() - if tarinfo.name.startswith("hooks/restore/") + if tarinfo.name.startswith(system_part) ] tar.extractall(members=subdir_and_files, path=self.work_dir) + subdir_and_files = [ + tarinfo for tarinfo in tar.getmembers() + if tarinfo.name.startswith("hooks/restore/") + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) - # Extract apps backup - for app in apps_targets: - subdir_and_files = [ - tarinfo for tarinfo in tar.getmembers() - if tarinfo.name.startswith("apps/" + app) - ] - tar.extractall(members=subdir_and_files, path=self.work_dir) + # Extract apps backup + for app in apps_targets: + subdir_and_files = [ + tarinfo for tarinfo in tar.getmembers() + if tarinfo.name.startswith("apps/" + app) + ] + tar.extractall(members=subdir_and_files, path=self.work_dir) class BorgBackupMethod(BackupMethod): diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 8c860fc60..1071c1642 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -101,9 +101,6 @@ def app_is_installed(app): def backup_test_dependencies_are_met(): - # We need archivemount installed for the backup features to work - assert os.system("which archivemount >/dev/null") == 0 - # Dummy test apps (or backup archives) assert os.path.exists("./tests/apps/backup_wordpress_from_2p4") assert os.path.exists("./tests/apps/backup_legacy_app_ynh") @@ -250,42 +247,6 @@ def test_backup_and_restore_all_sys(): assert os.path.exists("/etc/ssowat/conf.json") -def test_backup_and_restore_archivemount_failure(monkeypatch, mocker): - - # Create the backup - backup_create(ignore_system=False, ignore_apps=True) - - archives = backup_list()["archives"] - assert len(archives) == 1 - - archives_info = backup_info(archives[0], with_details=True) - assert archives_info["apps"] == {} - assert (len(archives_info["system"].keys()) == - len(os.listdir("/usr/share/yunohost/hooks/backup/"))) - - # Remove ssowat conf - assert os.path.exists("/etc/ssowat/conf.json") - os.system("rm -rf /etc/ssowat/") - assert not os.path.exists("/etc/ssowat/conf.json") - - def custom_subprocess_call(*args, **kwargs): - import subprocess as subprocess2 - if args[0] and args[0][0]=="archivemount": - monkeypatch.undo() - return 1 - return subprocess.call(*args, **kwargs) - - monkeypatch.setattr("subprocess.call", custom_subprocess_call) - mocker.spy(m18n, "n") - - # Restore the backup - backup_restore(auth, name=archives[0], force=True, - ignore_system=False, ignore_apps=True) - - # Check ssowat conf is back - assert os.path.exists("/etc/ssowat/conf.json") - - ############################################################################### # System restore from 2.4 # ############################################################################### @@ -311,38 +272,6 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker): ignore_apps=True, force=True) - -@pytest.mark.with_system_archive_from_2p4 -def test_restore_system_from_Ynh2p4_archivemount_failure(monkeypatch, mocker): - - # Backup current system - backup_create(ignore_system=False, ignore_apps=True) - archives = backup_list()["archives"] - assert len(archives) == 2 - - def custom_subprocess_call(*args, **kwargs): - import subprocess as subprocess2 - if args[0] and args[0][0]=="archivemount": - monkeypatch.undo() - return 1 - return subprocess.call(*args, **kwargs) - - monkeypatch.setattr("subprocess.call", custom_subprocess_call) - - try: - # Restore system from 2.4 - backup_restore(auth, name=backup_list()["archives"][1], - ignore_system=False, - ignore_apps=True, - force=True) - finally: - # Restore system as it was - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=False, - ignore_apps=True, - force=True) - - ############################################################################### # App backup # ############################################################################### @@ -545,29 +474,6 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("yoloswag") -@pytest.mark.with_wordpress_archive_from_2p4 -def test_restore_app_archivemount_failure(monkeypatch, mocker): - - def custom_subprocess_call(*args, **kwargs): - import subprocess as subprocess2 - if args[0] and args[0][0]=="archivemount": - monkeypatch.undo() - return 1 - return subprocess.call(*args, **kwargs) - - monkeypatch.setattr("subprocess.call", custom_subprocess_call) - mocker.spy(m18n, "n") - - assert not _is_installed("wordpress") - - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, - apps=["wordpress"]) - - assert _is_installed("wordpress") - - @pytest.mark.with_wordpress_archive_from_2p4 def test_restore_app_already_installed(mocker): @@ -643,7 +549,7 @@ def test_restore_archive_with_no_json(mocker): # Create a backup with no info.json associated os.system("touch /tmp/afile") os.system("tar -czvf /home/yunohost.backup/archives/badbackup.tar.gz /tmp/afile") - + assert "badbackup" in backup_list()["archives"] mocker.spy(m18n, "n") From 022524473e532ec5f6ebe9d0bd889a6e3e384206 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 15 Jun 2018 16:21:54 +0200 Subject: [PATCH 0929/1066] Update changelog --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2c855eae1..2e480e1bb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ + +yunohost (2.7.13.6) testing; urgency=low + + * Misc fixes + * [stretch-migration] Disable predictable network interface names + + Fixes by Bram and Aleks + + -- Alexandre Aubin Fri, 15 Jun 2018 16:20:00 +0000 + yunohost (2.7.13.5) testing; urgency=low * [fix] a bug when log to be fetched is empty From aca404a5d78b3690ea5ec7fa74e5e253719b3603 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 15 Jun 2018 17:25:27 +0200 Subject: [PATCH 0930/1066] Condition typo :< --- src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index 9df51979d..871edcd19 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -22,7 +22,7 @@ class MyMigration(Migration): if not self.package_is_installed("postgresql-9.6"): raise MoulinetteError(m18n.n("migration_0005_postgresql_96_not_installed")) - if not space_used_by_directory("/var/lib/postgresql/9.4") < free_space_in_directory("/var/lib/postgresql"): + if not space_used_by_directory("/var/lib/postgresql/9.4") > free_space_in_directory("/var/lib/postgresql"): raise MoulinetteError(m18n.n("migration_0005_not_enough_space", path="/var/lib/postgresql/")) subprocess.check_call("service postgresql stop", shell=True) From 8df696849b7608054ce5cc524abde96ad43bc8f2 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 15 Jun 2018 18:54:14 +0200 Subject: [PATCH 0931/1066] [fix] Redirect corectly php5 to php7 file during restore (#489) * [fix] Redirect corectly php5 to php7 file during restore * [enh] Avoid to use bash command to do replace * [enh] Use moulinette helpers * [fix] Migrate correctly the first column of backup csv * [fix] Missing argument in locale --- locales/en.json | 1 + src/yunohost/backup.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/locales/en.json b/locales/en.json index cbfefad93..21aa67d8b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -103,6 +103,7 @@ "backup_output_directory_not_empty": "The output directory is not empty", "backup_output_directory_required": "You must provide an output directory for the backup", "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", + "backup_php5_to_php7_migration_may_fail": "Could not convert your archive to support php7, your php apps may fail to restore (reason: {error:s})", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", "backup_system_part_failed": "Unable to backup the '{part:s}' system part", diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index db0f44abe..41724d61c 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1111,11 +1111,58 @@ class RestoreManager(): try: self._postinstall_if_needed() + + # Apply dirty patch to redirect php5 file on php7 + self._patch_backup_csv_file() + + self._restore_system() self._restore_apps() finally: self.clean() + def _patch_backup_csv_file(self): + """ + Apply dirty patch to redirect php5 file on php7 + """ + + backup_csv = os.path.join(self.work_dir, 'backup.csv') + + if not os.path.isfile(backup_csv): + return + + try: + contains_php5 = False + with open(backup_csv) as csvfile: + reader = csv.DictReader(csvfile, fieldnames=['source', 'dest']) + newlines = [] + for row in reader: + if 'php5' in row['source']: + contains_php5 = True + row['source'] = row['source'].replace('/etc/php5', '/etc/php/7.0') \ + .replace('/var/run/php5-fpm', '/var/run/php/php7.0-fpm') \ + .replace('php5','php7') + + newlines.append(row) + except (IOError, OSError, csv.Error) as e: + raise MoulinetteError(errno.EIO,m18n.n('error_reading_file', + file=backup_csv, + error=str(e))) + + if not contains_php5: + return + + try: + with open(backup_csv, 'w') as csvfile: + writer = csv.DictWriter(csvfile, + fieldnames=['source', 'dest'], + quoting=csv.QUOTE_ALL) + for row in newlines: + writer.writerow(row) + except (IOError, OSError, csv.Error) as e: + logger.warning(m18n.n('backup_php5_to_php7_migration_may_fail', + error=str(e))) + def _restore_system(self): """ Restore user and system parts """ @@ -1202,6 +1249,10 @@ class RestoreManager(): # Apply dirty patch to make php5 apps compatible with php7 _patch_php5(app_settings_in_archive) + # Delete _common.sh file in backup + common_file = os.path.join(app_backup_in_archive, '_common.sh') + filesystem.rm(common_file, force=True) + # Check if the app has a restore script app_restore_script_in_archive = os.path.join(app_scripts_in_archive, 'restore') From 8268d6e0747a65594b1feb5bbb2c9502ae6f737d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 15 Jun 2018 19:30:51 +0200 Subject: [PATCH 0932/1066] Fix counter-intuitive backup API (#490) * Remove old/deprecated --hooks * Remove --ignore-system and --ignore-apps, backup/restore everything by default, or only the parts explicitly given --- data/actionsmap/yunohost.yml | 37 +++---------------- src/yunohost/backup.py | 71 ++++-------------------------------- 2 files changed, 13 insertions(+), 95 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 4cf0f8955..7db57b0e6 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -780,7 +780,7 @@ backup: ### backup_create() create: - action_help: Create a backup local archive + action_help: Create a backup local archive. If neither --apps or --system are given, this will backup all apps and all system parts. If only --apps if given, this will only backup apps and no system parts. Similarly, if only --system is given, this will only backup system parts and no apps. api: POST /backup arguments: -n: @@ -804,28 +804,15 @@ backup: help: List of backup methods to apply (copy or tar by default) nargs: "*" --system: - help: List of system parts to backup (all by default) + help: List of system parts to backup (or all if none given). nargs: "*" --apps: - help: List of application names to backup (all by default) + help: List of application names to backup (or all if none given) nargs: "*" - --hooks: - help: (Deprecated) See --system - nargs: "*" - --ignore-system: - help: Do not backup system - action: store_true - --ignore-apps: - help: Do not backup apps - action: store_true - --ignore-hooks: - help: (Deprecated) See --ignore-system - action: store_true - ### backup_restore() restore: - action_help: Restore from a local backup archive + action_help: Restore from a local backup archive. If neither --apps or --system are given, this will restore all apps and all system parts in the archive. If only --apps if given, this will only restore apps and no system parts. Similarly, if only --system is given, this will only restore system parts and no apps. api: POST /backup/restore/ configuration: authenticate: all @@ -834,23 +821,11 @@ backup: name: help: Name of the local backup archive --system: - help: List of system parts to restore (all by default) + help: List of system parts to restore (or all if none is given) nargs: "*" --apps: - help: List of application names to restore (all by default) + help: List of application names to restore (or all if none is given) nargs: "*" - --hooks: - help: (Deprecated) See --system - nargs: "*" - --ignore-system: - help: Do not restore system parts - action: store_true - --ignore-apps: - help: Do not restore apps - action: store_true - --ignore-hooks: - help: (Deprecated) See --ignore-system - action: store_true --force: help: Force restauration on an already installed system action: store_true diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 41724d61c..acb7eb574 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1959,9 +1959,7 @@ class CustomBackupMethod(BackupMethod): def backup_create(name=None, description=None, methods=[], output_directory=None, no_compress=False, - ignore_system=False, system=[], - ignore_apps=False, apps=[], - ignore_hooks=False, hooks=[]): + system=[], apps=[]): """ Create a backup local archive @@ -1972,12 +1970,7 @@ def backup_create(name=None, description=None, methods=[], output_directory -- Output directory for the backup no_compress -- Do not create an archive file system -- List of system elements to backup - ignore_system -- Ignore system elements apps -- List of application names to backup - ignore_apps -- Do not backup apps - - hooks -- (Deprecated) Renamed to "system" - ignore_hooks -- (Deprecated) Renamed to "ignore_system" """ # TODO: Add a 'clean' argument to clean output directory @@ -1986,22 +1979,6 @@ def backup_create(name=None, description=None, methods=[], # Validate / parse arguments # ########################################################################### - # Historical, deprecated options - if ignore_hooks is not False: - logger.warning("--ignore-hooks is deprecated and will be removed in the" - "future. Please use --ignore-system instead.") - ignore_system = ignore_hooks - - if hooks != [] and hooks is not None: - logger.warning("--hooks is deprecated and will be removed in the" - "future. Please use --system instead.") - system = hooks - - # Validate that there's something to backup - if ignore_system and ignore_apps: - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_action_required')) - # Validate there is no archive with the same name if name and name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, @@ -2034,14 +2011,9 @@ def backup_create(name=None, description=None, methods=[], else: methods = ['tar'] # In future, borg will be the default actions - if ignore_system: - system = None - elif system is None: + # If no --system or --apps given, backup everything + if system is None and apps is None: system = [] - - if ignore_apps: - apps = None - elif apps is None: apps = [] ########################################################################### @@ -2090,11 +2062,7 @@ def backup_create(name=None, description=None, methods=[], } -def backup_restore(auth, name, - system=[], ignore_system=False, - apps=[], ignore_apps=False, - hooks=[], ignore_hooks=False, - force=False): +def backup_restore(auth, name, system=[], apps=[], force=False): """ Restore from a local backup archive @@ -2102,48 +2070,23 @@ def backup_restore(auth, name, name -- Name of the local backup archive force -- Force restauration on an already installed system system -- List of system parts to restore - ignore_system -- Do not restore any system parts apps -- List of application names to restore - ignore_apps -- Do not restore apps - - hooks -- (Deprecated) Renamed to "system" - ignore_hooks -- (Deprecated) Renamed to "ignore_system" """ ########################################################################### # Validate / parse arguments # ########################################################################### - # Historical, deprecated options - if ignore_hooks is not False: - logger.warning("--ignore-hooks is deprecated and will be removed in the" - "future. Please use --ignore-system instead.") - ignore_system = ignore_hooks - if hooks != [] and hooks is not None: - logger.warning("--hooks is deprecated and will be removed in the" - "future. Please use --system instead.") - system = hooks - - # Validate what to restore - if ignore_system and ignore_apps: - raise MoulinetteError(errno.EINVAL, - m18n.n('restore_action_required')) - - if ignore_system: - system = None - elif system is None: + # If no --system or --apps given, restore everything + if system is None and apps is None: system = [] - - if ignore_apps: - apps = None - elif apps is None: apps = [] # TODO don't ask this question when restoring apps only and certain system # parts # Check if YunoHost is installed - if os.path.isfile('/etc/yunohost/installed') and not ignore_system: + if system is not None and os.path.isfile('/etc/yunohost/installed'): logger.warning(m18n.n('yunohost_already_installed')) if not force: try: From a25b746ea595b2b8f8727771bc731d06304ab721 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 16 Jun 2018 16:22:14 +0200 Subject: [PATCH 0933/1066] Update changelog --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 82eaff86f..e5a059351 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (3.0.0~beta1.7) testing; urgency=low + + * Merge with jessie's branches + * Set verbose by default + * Remove archivemount stuff + * Correctly patch php5/php7 stuff when doing a backup restore + * Fix counter-intuitive backup API + + -- Alexandre Aubin Sat, 16 Jun 2018 16:20:00 +0000 + yunohost (3.0.0~beta1.6) testing; urgency=low * [fix] Service description for php7.0-fpm From 0c248bcb7eafcfcccb14570c8b0983183dcd30e0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 16 Jun 2018 20:59:18 +0200 Subject: [PATCH 0934/1066] Last minute fix : install php7.0-acpu to hopefully make stretch still work after the upgrade --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index c291a31b7..6969de571 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -221,9 +221,9 @@ class MyMigration(Migration): upgrade_command += " apt-get install" upgrade_command += " --assume-yes " upgrade_command += " ".join(YUNOHOST_PACKAGES) - # We also install php-zip to fix an issue with nextcloud and kanboard - # that need it when on stretch. - upgrade_command += " php-zip" + # We also install php-zip and php7.0-acpu to fix an issue with + # nextcloud and kanboard that need it when on stretch. + upgrade_command += " php-zip php7.0-apcu" upgrade_command += " 2>&1 | tee -a {}".format(self.logfile) wait_until_end_of_yunohost_command = "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) From 572daa15e409e52d03ab300fdb0f1b27ddbc2c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Wed, 23 May 2018 15:07:50 +0000 Subject: [PATCH 0935/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 23.0% (93 of 404 strings) --- locales/oc.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index e0d476cf2..a1af70124 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -84,5 +84,12 @@ "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", "backup_running_app_script": "Lançament de l’escript de salvagarda de l’aplicacion « {app:s} »...", "backup_running_hooks": "Execucion dels scripts de salvagarda...", - "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma" + "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma", + "app_requirements_failed": "Impossible de complir las condicions requesidas per {app} : {error}", + "app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}", + "appslist_could_not_migrate": "Migracion de la lista impossibla {appslist:s} ! Impossible d’analizar l’URL… L’anciana tasca cron es estada servada dins {bkp_file:s}.", + "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", + "backup_applying_method_custom": "Crida lo metòde de salvagarda personalizat « {method:s} »…", + "backup_borg_not_implemented": "Lo metòde de salvagarda Bord es pas encara implementat", + "backup_couldnt_bind": "Impossible de ligar {src:s} amb {dest:s}" } From d8ddc0f8de7f52bf11a872265383d4442d0d15c5 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Mon, 21 May 2018 08:05:35 +0000 Subject: [PATCH 0936/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (404 of 404 strings) --- locales/fr.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index ff1278369..3939503f9 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -398,5 +398,23 @@ "migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", "migrations_to_be_ran_manually": "La migration {number} {name} doit être lancée manuellement. Veuillez aller dans Outils > Migration dans l’interface admin, ou lancer `yunohost tools migrations migrate`.", - "migrations_need_to_accept_disclaimer": "Pour lancer la migration {number} {name}, vous devez accepter cette clause de non-responsabilité :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer." + "migrations_need_to_accept_disclaimer": "Pour lancer la migration {number} {name}, vous devez accepter cette clause de non-responsabilité :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.", + "service_description_avahi-daemon": "permet d’atteindre votre serveur via yunohost.local sur votre réseau local", + "service_description_dnsmasq": "assure la résolution des noms de domaine (DNS)", + "service_description_dovecot": "permet aux clients de messagerie d’accéder/récupérer les courriels (via IMAP et POP3)", + "service_description_fail2ban": "protège contre les attaques brute-force et autres types d’attaques venant d’Internet", + "service_description_glances": "surveille les informations système de votre serveur", + "service_description_metronome": "gère les comptes de messagerie instantanée XMPP", + "service_description_mysql": "stocke les données des applications (bases de données SQL)", + "service_description_nginx": "sert ou permet l’accès à tous les sites web hébergés sur votre serveur", + "service_description_nslcd": "gère la connexion en ligne de commande des utilisateurs YunoHost", + "service_description_php5-fpm": "exécute des applications écrites en PHP avec nginx", + "service_description_postfix": "utilisé pour envoyer et recevoir des courriels", + "service_description_redis-server": "une base de donnée spécialisée utilisée pour l’accès rapide aux données, les files d’attentes et la communication inter-programmes", + "service_description_rmilter": "vérifie divers paramètres dans les courriels", + "service_description_rspamd": "filtre le pourriel, et d’autres fonctionnalités liées au courriel", + "service_description_slapd": "stocke les utilisateurs, domaines et leurs informations liées", + "service_description_ssh": "vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)", + "service_description_yunohost-api": "permet les interactions entre l’interface web de YunoHost et le système", + "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services" } From fc7b4ca6921ac2696b2cce2db385af5b00d09b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Wed, 23 May 2018 15:19:03 +0000 Subject: [PATCH 0937/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 28.2% (114 of 404 strings) --- locales/oc.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index a1af70124..91876c154 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -91,5 +91,26 @@ "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", "backup_applying_method_custom": "Crida lo metòde de salvagarda personalizat « {method:s} »…", "backup_borg_not_implemented": "Lo metòde de salvagarda Bord es pas encara implementat", - "backup_couldnt_bind": "Impossible de ligar {src:s} amb {dest:s}" + "backup_couldnt_bind": "Impossible de ligar {src:s} amb {dest:s}.", + "backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV", + "backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »", + "backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »", + "backup_custom_need_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « need_mount »", + "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat", + "backup_nothings_done": "I a pas res de salvagardar", + "backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid", + "service_status_failed": "Impossible dedeterminar l’estat del servici « {service:s} »", + "service_stopped": "Lo servici « {service:s} » es estat arrestat", + "service_unknown": "Servici « {service:s} » desconegut", + "unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada", + "unit_unknown": "Unitat « {unit:s} » desconeguda", + "unlimit": "Cap de quòta", + "unrestore_app": "L’aplicacion « {app:s} » serà pas restaurada", + "upnp_dev_not_found": "Cap de periferic compatible UPnP pas trobat", + "upnp_disabled": "UPnP es desactivat", + "upnp_enabled": "UPnP es activat", + "upnp_port_open_failed": "Impossible de dobrir los pòrts amb UPnP", + "yunohost_already_installed": "YunoHost es ja installat", + "yunohost_configured": "YunoHost es estat configurat", + "yunohost_installing": "Installacion de YunoHost..." } From 38f54e12d0b4a02d742155cdf64db825d4675f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Fri, 25 May 2018 09:14:15 +0000 Subject: [PATCH 0938/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 30.1% (122 of 404 strings) --- locales/oc.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 91876c154..77ae81a4e 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -112,5 +112,13 @@ "upnp_port_open_failed": "Impossible de dobrir los pòrts amb UPnP", "yunohost_already_installed": "YunoHost es ja installat", "yunohost_configured": "YunoHost es estat configurat", - "yunohost_installing": "Installacion de YunoHost..." + "yunohost_installing": "Installacion de YunoHost...", + "backup_applying_method_borg": "Mandadís de totes los fichièrs a la salvagarda dins lo repertòri borg-backup…", + "backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion", + "backup_extracting_archive": "Extraccion de l’archiu de salvagarda…", + "backup_output_symlink_dir_broken": "Avètz un ligam simbolic copat allòc de vòstre repertòri d’archiu « {path:s} ». Poiriatz aver una configuracion personalizada per salvagardar vòstras donadas sus un autre sistèma de fichièrs, en aquel cas, saique oblidèretz de montar o de connectar lo disc o la clau USB.", + "backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.", + "backup_with_no_restore_script_for_app": "L’aplicacion {app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.", + "certmanager_acme_not_configured_for_domain": "Lo certificat del domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr cert-install per aqueste domeni.", + "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let's Encrypt. Impossible de lo renovar automaticament !" } From 7dea3b66b101bac7c9ab0cd1e8d2464984d5121a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Fri, 25 May 2018 09:32:52 +0000 Subject: [PATCH 0939/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 34.6% (140 of 404 strings) To bypass, en occitan...vesi pas, deu i aver un imatge tot pariu --- locales/oc.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 77ae81a4e..53550d8bf 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -120,5 +120,24 @@ "backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.", "backup_with_no_restore_script_for_app": "L’aplicacion {app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.", "certmanager_acme_not_configured_for_domain": "Lo certificat del domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr cert-install per aqueste domeni.", - "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let's Encrypt. Impossible de lo renovar automaticament !" + "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let's Encrypt. Impossible de lo renovar automaticament !", + "certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain:s} es a man d’expirar ! Utilizatz --force per cortcircuitar", + "certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain:s} (fichièr : {file:s}), rason : {reason:s}", + "certmanager_cert_install_success": "Installacion capitada del certificat Let’s Encrypt pel domeni {domain:s} !", + "certmanager_cert_install_success_selfsigned": "Installacion capitada del certificat auto-signat pel domeni {domain:s} !", + "certmanager_cert_signing_failed": "Fracàs de la signatura del noù certificat", + "certmanager_domain_cert_not_selfsigned": "Lo certificat del domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utiliatz --force)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz ço que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", + "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e nginx son corrèctas", + "certmanager_domain_unknown": "Domeni desconegut {domain:s}", + "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain:s} (fichièr : {file:s})", + "certmanager_self_ca_conf_file_not_found": "Lo fichièr de configuracion per l’autoritat del certificat auto-signat es introbabla (fichièr : {file:s})", + "certmanager_unable_to_parse_self_CA_name": "Analisi impossible lo nom de l’autoritat del certificat auto-signat (fichièr : {file:s})", + "custom_app_url_required": "Cal que donetz una URL per actualizar vòstra aplicacion personalizada {app:s}", + "custom_appslist_name_required": "Cal que nomenetz vòstra lista d’aplicacions personalizadas", + "diagnosis_debian_version_error": "Impossible de determinar la version de Debian : {error}", + "diagnosis_kernel_version_error": "Impossible de recuperar la version del nuclèu : {error}", + "diagnosis_no_apps": "Pas cap d’aplicacion installada", + "dnsmasq_isnt_installed": "dnsmasq sembla pas èsser installat, mercés de lançar « apt-get remove bind9 && apt-get install dnsmasq »", + "domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr." } From 0fc6597fc48fe13003f0b648720b6a46653e3f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Fri, 25 May 2018 12:56:19 +0000 Subject: [PATCH 0940/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 41.5% (168 of 404 strings) --- locales/oc.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 53550d8bf..be272f80a 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -139,5 +139,33 @@ "diagnosis_kernel_version_error": "Impossible de recuperar la version del nuclèu : {error}", "diagnosis_no_apps": "Pas cap d’aplicacion installada", "dnsmasq_isnt_installed": "dnsmasq sembla pas èsser installat, mercés de lançar « apt-get remove bind9 && apt-get install dnsmasq »", - "domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr." + "domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr", + "domain_cert_gen_failed": "Generacion del certificat impossibla", + "domain_created": "Lo domeni es creat", + "domain_creation_failed": "Creacion del certificat impossibla", + "domain_deleted": "Lo domeni es suprimit", + "domain_deletion_failed": "Supression impossibla del domeni", + "domain_dyndns_invalid": "Domeni incorrècte per una utilizacion amb DynDNS", + "domain_dyndns_root_unknown": "Domeni DynDNS màger desconegut", + "domain_exists": "Lo domeni existís ja", + "domain_hostname_failed": "Fracàs de la creacion d’un nòu nom d’òst", + "domain_unknown": "Domeni desconegut", + "domain_zone_exists": "Lo fichièr zòna DNS existís ja", + "domain_zone_not_found": "Fichèr de zòna DNS introbable pel domeni {:s}", + "domains_available": "Domenis disponibles :", + "done": "Acabat", + "downloading": "Telecargament…", + "dyndns_could_not_check_provide": "Impossible de verificar se {provider:s} pòt provesir {domain:s}.", + "dyndns_cron_installed": "La tasca cron pel domeni DynDNS es installada", + "dyndns_cron_remove_failed": "Impossible de levar la tasca cron pel domeni DynDNS", + "dyndns_cron_removed": "La tasca cron pel domeni DynDNS es levada", + "dyndns_ip_update_failed": "Impossible d’actualizar l’adreça IP sul domeni DynDNS", + "dyndns_ip_updated": "Vòstra adreça IP es estada actualizada pel domeni DynDNS", + "dyndns_key_generating": "La clau DNS es a se generar, pòt trigar una estona...", + "dyndns_key_not_found": "Clau DNS introbabla pel domeni", + "dyndns_no_domain_registered": "Cap de domeni pas enregistrat amb DynDNS", + "dyndns_registered": "Lo domeni DynDNS es enregistrat", + "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossibla : {error:s}", + "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider:s} pòt pas fornir lo domeni {domain:s}.", + "dyndns_unavailable": "Lo domeni {domain:s} es pas disponible" } From 78af61b308e72c1a7e77d6bc3739971e15d6cc45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Fri, 25 May 2018 14:15:01 +0000 Subject: [PATCH 0941/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 56.4% (228 of 404 strings) --- locales/oc.json | 62 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index be272f80a..ca6ced9ab 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -167,5 +167,65 @@ "dyndns_registered": "Lo domeni DynDNS es enregistrat", "dyndns_registration_failed": "Enregistrament del domeni DynDNS impossibla : {error:s}", "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider:s} pòt pas fornir lo domeni {domain:s}.", - "dyndns_unavailable": "Lo domeni {domain:s} es pas disponible" + "dyndns_unavailable": "Lo domeni {domain:s} es pas disponible.", + "extracting": "Extraccion…", + "field_invalid": "Camp incorrècte : « {:s} »", + "format_datetime_short": "%d/%m/%Y %H:%M", + "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason:s}", + "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en picant « yunohost settings list »", + "global_settings_reset_success": "Capitada ! Vòstra configuracion precedenta es estada salvagarda dins {path:s}", + "global_settings_setting_example_bool": "Exemple d’opcion booleana", + "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartat e salvagardat dins /etc/yunohost/unkown_settings.json", + "installation_failed": "Fracàs de l’installacion", + "invalid_url_format": "Format d’URL pas valid", + "ldap_initialized": "L’annuari LDAP es inicializat", + "license_undefined": "indefinida", + "maindomain_change_failed": "Modificacion impossibla del domeni màger", + "maindomain_changed": "Lo domeni màger es estat modificat", + "migrate_tsig_end": "La migracion cap a hmac-sha512 es acabada", + "migrate_tsig_wait_2": "2 minutas…", + "migrate_tsig_wait_3": "1 minuta…", + "migrate_tsig_wait_4": "30 segondas…", + "migration_description_0002_migrate_to_tsig_sha256": "Melhora la seguretat de DynDNS TSIG en utilizar SHA512 allòc de MD5", + "migration_description_0003_migrate_to_stretch": "Mesa a nivèl del sistèma cap a Debian Stretch e YunoHost 3.0", + "migration_0003_backward_impossible": "La migracion Stretch es pas reversibla.", + "migration_0003_start": "Aviada de la migracion cap a Stretech. Los jornals seràn disponibles dins {logfile}.", + "migration_0003_patching_sources_list": "Petaçatge de sources.lists…", + "migration_0003_main_upgrade": "Aviada de la mesa a nivèl màger…", + "migration_0003_fail2ban_upgrade": "Aviada de la mesa a nivèl de fail2ban…", + "migration_0003_not_jessie": "La distribucion Debian actuala es pas Jessie !", + "migrations_cant_reach_migration_file": "Impossible d’accedir als fichièrs de migracion amb lo camin %s", + "migrations_current_target": "La cibla de migracion est {}", + "migrations_error_failed_to_load_migration": "ERROR : fracàs del cargament de la migracion {number} {name}", + "migrations_list_conflict_pending_done": "Podètz pas utilizar --previous e --done a l’encòp.", + "migrations_loading_migration": "Cargament de la migracion{number} {name}…", + "migrations_no_migrations_to_run": "Cap de migracion de lançar", + "migrations_show_currently_running_migration": "Realizacion de la migracion {number} {name}…", + "migrations_show_last_migration": "La darrièra migracion realizada es {}", + "monitor_glances_con_failed": "Connexion impossibla al servidor Glances", + "monitor_not_enabled": "Lo seguiment de l’estat del servidor es pas activat", + "monitor_stats_no_update": "Cap de donadas d’estat del servidor d’actualizar", + "mountpoint_unknown": "Ponch de montatge desconegut", + "mysql_db_creation_failed": "Creacion de la basa de donadas MySQL impossibla", + "no_appslist_found": "Cap de lista d’aplicacions pas trobaba", + "no_internet_connection": "Lo servidor es pas connectat a Internet", + "package_not_installed": "Lo paquet « {pkgname} » es pas installat", + "package_unknown": "Paquet « {pkgname} » desconegut", + "packages_no_upgrade": "I a pas cap de paquet d’actualizar", + "packages_upgrade_failed": "Actualizacion de totes los paquets impossibla", + "path_removal_failed": "Impossible de suprimir lo camin {:s}", + "pattern_domain": "Deu èsser un nom de domeni valid (ex : mon-domeni.org)", + "pattern_email": "Deu èsser una adreça electronica valida (ex : escais@domeni.org)", + "pattern_firstname": "Deu èsser un pichon nom valid", + "pattern_lastname": "Deu èsser un nom valid", + "pattern_password": "Deu conténer almens 3 caractèrs", + "pattern_port": "Deu èsser un numèro de pòrt valid (ex : 0-65535)", + "pattern_port_or_range": "Deu èsser un numèro de pòrt valid (ex : 0-65535) o un interval de pòrt (ex : 100:200)", + "pattern_positive_number": "Deu èsser un nombre positiu", + "port_already_closed": "Lo pòrt {port:d} es ja tampat per las connexions {ip_version:s}", + "port_already_opened": "Lo pòrt {port:d} es ja dubèrt per las connexions {ip_version:s}", + "port_available": "Lo pòrt {port:d} es disponible", + "port_unavailable": "Lo pòrt {port:d} es pas disponible", + "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", + "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »" } From 9dd1389d8893fea28db0ddbb781225b508c3126f Mon Sep 17 00:00:00 2001 From: Quenti Date: Sun, 27 May 2018 17:18:09 +0000 Subject: [PATCH 0942/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 56.9% (230 of 404 strings) --- locales/oc.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index ca6ced9ab..f0029aac0 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -227,5 +227,7 @@ "port_available": "Lo pòrt {port:d} es disponible", "port_unavailable": "Lo pòrt {port:d} es pas disponible", "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", - "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »" + "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »", + "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporala. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?", + "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »" } From e823bb84f8a9d22b7a2121b7c005d62c537e5cec Mon Sep 17 00:00:00 2001 From: Quenti Date: Sun, 27 May 2018 17:23:34 +0000 Subject: [PATCH 0943/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 56.9% (230 of 404 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index f0029aac0..566853198 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -228,6 +228,6 @@ "port_unavailable": "Lo pòrt {port:d} es pas disponible", "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »", - "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporala. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?", + "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporària. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?", "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »" } From e0223b197b57710a895e1648a40be8e5312095fb Mon Sep 17 00:00:00 2001 From: Quenti Date: Sun, 27 May 2018 17:24:55 +0000 Subject: [PATCH 0944/1066] [i18n] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 57.9% (234 of 404 strings) dorsièr o repertòri ? --- locales/oc.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 566853198..b7686ee37 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -229,5 +229,10 @@ "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »", "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporària. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?", - "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »" + "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »", + "backup_output_directory_forbidden": "Repertòri de destinacion defendut. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", + "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)", + "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let's Encrypt pel domeni {domain:s} !", + "certmanager_certificate_fetching_or_enabling_failed": "Sembla d’aver fracassat l’activacion d’un nòu certificat per {domain:s}…", + "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr" } From 2d66651aaeb8cf64cae784a8959effe64171e500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 06:57:41 +0000 Subject: [PATCH 0945/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 59.1% (239 of 404 strings) --- locales/oc.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index b7686ee37..de443e93f 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -234,5 +234,10 @@ "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)", "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let's Encrypt pel domeni {domain:s} !", "certmanager_certificate_fetching_or_enabling_failed": "Sembla d’aver fracassat l’activacion d’un nòu certificat per {domain:s}…", - "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr" + "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr", + "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let's Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.", + "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasetz, utilizatz --no-checks per desactivar las verificacions.)", + "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let'S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", + "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", + "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat." } From 4bb25bca9c0b8d6dd923bd980f365a08cec317da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 07:27:59 +0000 Subject: [PATCH 0946/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 74.5% (301 of 404 strings) --- locales/oc.json | 64 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index de443e93f..660f55473 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -239,5 +239,67 @@ "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasetz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let'S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", - "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat." + "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat.", + "domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.", + "domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS", + "domain_dyndns_dynette_is_unreachable": "Impossible de contactar la dynette YunoHost, siá YunoHost pas es pas corrèctament connectat a Internet, siá lo servidor de la dynett es arrestat. Error : {error}", + "domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni", + "firewall_reload_failed": "Impossible de recargar lo parafòc", + "firewall_reloaded": "Lo parafòc es estat recargat", + "firewall_rules_cmd_failed": "Unas règlas del parafòc an fracassat. Per mai informacions, consultatz lo jornal.", + "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {received_type:s}, esperat {expected_type:s}", + "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte. Recebut : {received_type:s}, esperat {expected_type:s}", + "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}", + "global_settings_setting_example_enum": "Exemple d’opcion de tipe enumeracion", + "global_settings_setting_example_int": "Exemple d’opcion de tipe entièr", + "global_settings_setting_example_string": "Exemple d’opcion de tipe cadena", + "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting:s} sembla d’aver lo tipe {unknown_type:s} mas es pas un tipe pres en carga pel sistèma.", + "hook_exec_failed": "Fracàs de l’execucion del script « {path:s} »", + "hook_exec_not_terminated": "L’execucion del escript « {path:s} » es pas acabada", + "hook_list_by_invalid": "La proprietat de tria de las accions es invalida", + "hook_name_unknown": "Nom de script « {name:s} » desconegut", + "ldap_init_failed_to_create_admin": "L’inicializacion de LDAP a pas pogut crear l’utilizaire admin", + "mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut", + "mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá", + "migrate_tsig_failed": "La migracion del domeni dyndns {domain} cap a hmac-sha512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}", + "migrate_tsig_wait": "Esperem 3 minutas que lo servidor dyndns prenga en compte la novèla clau…", + "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni dyndns, donc cap de migracion es pas necessària !", + "migration_0003_yunohost_upgrade": "Aviada de la mesa a nivèl del paquet YunoHost… La migracion acabarà, mas la mesa a jorn reala se realizarà tot bèl aprèp. Un còp acabada, poiretz vos reconnectar a l’administracion web.", + "migration_0003_system_not_fully_up_to_date": "Lo sistèma es pas complètament a jorn. Mercés de lançar una mesa a jorn classica abans de començar la migracion per Stretch.", + "migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}", + "monitor_period_invalid": "Lo periòde de temps es incorrècte", + "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbabe", + "monitor_stats_period_unavailable": "Cap d’estatisticas son pas disponiblas pel periòde", + "mysql_db_init_failed": "Impossible d’inicializar la basa de donadas MySQL", + "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_disabled": "Lo servici « {service:s} » es desactivat", + "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_enabled": "Lo servici « {service:s} » es activat", + "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »", + "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…", + "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}", + "service_regenconf_pending_applying": "Aplicacion de as configuraion en espèra pel servici « {service} »…", + "service_remove_failed": "Impossible de levar lo servici « {service:s} »", + "service_removed": "Lo servici « {service:s} » es estat levat", + "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_started": "Lo servici « {service:s} » es aviat", + "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}", + "ssowat_conf_generated": "La configuracion SSowat es generada", + "ssowat_conf_updated": "La configuracion SSOwat es estada actualizada", + "system_upgraded": "Lo sistèma es estat actualizat", + "system_username_exists": "Lo nom d’utilizaire existís ja dins los utilizaires sistèma", + "unexpected_error": "Una error inesperada s’es producha", + "upgrade_complete": "Actualizacion acabada", + "upgrading_packages": "Actualizacion dels paquets…", + "user_created": "L’utilizaire es creat", + "user_creation_failed": "Creacion de l’utilizaire impossibla", + "user_deleted": "L’utilizaire es suprimit", + "user_deletion_failed": "Supression impossibla de l’utilizaire", + "user_home_creation_failed": "Creacion impossibla del repertòri personal a l’utilizaire", + "user_info_failed": "Recuperacion impossibla de las informacions tocant l’utilizaire", + "user_unknown": "Utilizaire « {user:s} » desconegut", + "user_update_failed": "Modificacion impossibla de l’utilizaire", + "user_updated": "L’utilizaire es estat modificat", + "yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion", + "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada." } From 7b99d3dc35ac5658e7457535310bb8ac1730899e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 12:13:25 +0000 Subject: [PATCH 0947/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 77.4% (313 of 404 strings) --- locales/oc.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 660f55473..bbfa9af2a 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -301,5 +301,17 @@ "user_update_failed": "Modificacion impossibla de l’utilizaire", "user_updated": "L’utilizaire es estat modificat", "yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion", - "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada." + "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada.", + "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat", + "service_conf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat", + "service_conf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es suprimit manualament e serà pas creat", + "service_conf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »", + "service_conf_file_removed": "Lo fichièr de configuracion « {conf} » es suprimit", + "service_conf_file_updated": "Lo fichièr de configuracion « {conf} » es actualizat", + "service_conf_new_managed_file": "Lo servici {service} gerís ara lo fichièr de configuracion « {conf} ».", + "service_conf_up_to_date": "La configuracion del servici « {service} » es ja actualizada", + "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada", + "service_description_avahi-daemon": "permet d’aténher vòstre servidr via yunohost.local sus vòstre ret local", + "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", + "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles..." } From 00ec54991448753c13fbf6ccdc876028b40256e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 12:54:12 +0000 Subject: [PATCH 0948/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 77.9% (315 of 404 strings) --- locales/oc.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index bbfa9af2a..7b481f562 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -302,7 +302,7 @@ "user_updated": "L’utilizaire es estat modificat", "yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion", "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada.", - "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat", + "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat.", "service_conf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat", "service_conf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es suprimit manualament e serà pas creat", "service_conf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »", @@ -313,5 +313,7 @@ "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada", "service_description_avahi-daemon": "permet d’aténher vòstre servidr via yunohost.local sus vòstre ret local", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", - "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles..." + "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", + "service_conf_file_backed_up": "Lo fichièr de configuracion « {new} » es salvagardat dins « {backup} »", + "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »" } From 0d87b6f7866e64c81a73fe36a5b3863f8738cb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 12:55:55 +0000 Subject: [PATCH 0949/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 79.2% (320 of 404 strings) --- locales/oc.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 7b481f562..ba5b09218 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -314,6 +314,11 @@ "service_description_avahi-daemon": "permet d’aténher vòstre servidr via yunohost.local sus vòstre ret local", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", - "service_conf_file_backed_up": "Lo fichièr de configuracion « {new} » es salvagardat dins « {backup} »", - "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »" + "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", + "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", + "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ?", + "service_add_failed": "Apondon impossible del servici « {service:s} »", + "service_added": "Lo servici « {service:s} » es ajustat", + "service_already_started": "Lo servici « {service:s} » es ja aviat", + "service_already_stopped": "Lo servici « {service:s} » es ja arrestat" } From 61983511ce937e2fe5515d4b260201de819e8b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 14:17:23 +0000 Subject: [PATCH 0950/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 83.4% (337 of 404 strings) --- locales/oc.json | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index ba5b09218..343bcd439 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -316,9 +316,26 @@ "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", - "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ?", + "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}", "service_add_failed": "Apondon impossible del servici « {service:s} »", "service_added": "Lo servici « {service:s} » es ajustat", "service_already_started": "Lo servici « {service:s} » es ja aviat", - "service_already_stopped": "Lo servici « {service:s} » es ja arrestat" + "service_already_stopped": "Lo servici « {service:s} » es ja arrestat", + "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda", + "restore_complete": "Restauracion acabada", + "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", + "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", + "restore_failed": "Impossible de restaurar lo sistèma", + "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tampauc dins l’archiu", + "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", + "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »", + "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", + "restore_nothings_done": "Res es pas estat restaurat", + "restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari", + "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app:s} »…", + "restore_running_hooks": "Execucion dels scripts de restauracion…", + "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma", + "server_shutdown": "Lo servidor serà atudat", + "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}", + "server_reboot": "Lo servidor es per reaviar" } From c0b74a0cdc46a4d390399c095aa6078e07f49475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Mon, 28 May 2018 14:30:43 +0000 Subject: [PATCH 0951/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 88.1% (356 of 404 strings) --- locales/oc.json | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 343bcd439..f3f27e274 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -321,7 +321,7 @@ "service_added": "Lo servici « {service:s} » es ajustat", "service_already_started": "Lo servici « {service:s} » es ja aviat", "service_already_stopped": "Lo servici « {service:s} » es ja arrestat", - "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda", + "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de restauracion", "restore_complete": "Restauracion acabada", "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", @@ -337,5 +337,24 @@ "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma", "server_shutdown": "Lo servidor serà atudat", "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}", - "server_reboot": "Lo servidor es per reaviar" + "server_reboot": "Lo servidor es per reaviar", + "network_check_mx_ko": "L’enregistrament DNS MX es pas especificat", + "new_domain_required": "Vos cal especificar lo domeni màger", + "no_ipv6_connectivity": "La connectivitat IPv6 es pas disponibla", + "not_enough_disk_space": "Espaci disc insufisent sus « {path:s} »", + "package_unexpected_error": "Una error inesperada es apareguda amb lo paquet « {pkgname} »", + "packages_upgrade_critical_later": "Los paquets critics {packages:s} seràn actualizats mai tard", + "restore_action_required": "Devètz precisar çò que cal restaurar", + "service_cmd_exec_failed": "Impossible d’executar la comanda « {command:s} »", + "service_conf_updated": "La configuracion es estada actualizada pel servici « {service} »", + "service_description_mysql": "garda las donadas de las aplicacions (base de donadas SQL)", + "service_description_php5-fpm": "executa d’aplicacions escrichas en PHP amb nginx", + "service_description_postfix": "emplegat per enviar e recebre de corrièls", + "service_description_rmilter": "verifica mantun paramètres dels corrièls", + "service_description_slapd": "garda los utilizaires, domenis e lors informacions ligadas", + "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", + "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", + "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", + "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", + "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON" } From 9bc24f98f728d58ca1522f3e43d950bba58b615c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Tue, 29 May 2018 09:08:35 +0000 Subject: [PATCH 0952/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 90.5% (366 of 404 strings) --- locales/oc.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index f3f27e274..3701b5b99 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -356,5 +356,15 @@ "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", - "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON" + "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", + "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion 'letsencrypt' es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: aquò probarà de tornar installar los certificats de totes los domenis amb un certificat Let's Encrypt o las auto-signats", + "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", + "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}", + "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}", + "executing_command": "Execucion de la comanda « {command:s} »…", + "executing_script": "Execucion del script « {script:s} »…", + "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}", + "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", + "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", + "update_cache_failed": "Impossible d’actualizar lo cache de l’APT" } From 6db89672946b7b81dd98e6393157fb2538bc734f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Tue, 29 May 2018 09:19:45 +0000 Subject: [PATCH 0953/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 97.0% (392 of 404 strings) ortho alias --- locales/oc.json | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 3701b5b99..d575dca4e 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -357,7 +357,7 @@ "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", - "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion 'letsencrypt' es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: aquò probarà de tornar installar los certificats de totes los domenis amb un certificat Let's Encrypt o las auto-signats", + "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion 'letsencrypt' es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : aquò provarà de tornar installar los certificats de totes los domenis amb un certificat Let's Encrypt o las auto-signats", "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}", "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}", @@ -366,5 +366,32 @@ "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}", "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", - "update_cache_failed": "Impossible d’actualizar lo cache de l’APT" + "update_cache_failed": "Impossible d’actualizar lo cache de l’APT", + "mail_alias_remove_failed": "Supression impossibla de l'alias de corrièl « {mail:s} »", + "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »", + "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a hmac-sha512 que’s mai securizat", + "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", + "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.", + "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", + "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.", + "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", + "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", + "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", + "migrations_skip_migration": "Passatge de la migracion {number} {name}…", + "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisias > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", + "migrations_need_to_accept_disclaimer": "Per lançar la migracion {number} {name} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", + "monitor_disabled": "La supervision del servidor es desactivada", + "monitor_enabled": "La supervision del servidor es activada", + "mysql_db_initialized": "La basa de donadas MySQL es estada inicializada", + "no_restore_script": "Lo script de salvagarda es pas estat trobat per l’aplicacion « {app:s} »", + "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", + "pattern_listname": "Deu èsser compausat solament de caractèrs alfanumerics e de tiret bas", + "service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)", + "service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet", + "service_description_glances": "susvelha las informacions sistèma de vòstre servidor", + "service_description_metronome": "gerís los comptes de messatjariás instantanèas XMPP", + "service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor", + "service_description_nslcd": "gerís la connexion en linha de comanda dels utilizaires YunoHost", + "service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas", + "service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl" } From 3d295a4fb267f3dc73b61f6dd29f332c66124352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Tue, 29 May 2018 13:17:59 +0000 Subject: [PATCH 0954/1066] [i18n] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 98.0% (396 of 404 strings) ortho endarrièr --- locales/oc.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index d575dca4e..51ca785c1 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -393,5 +393,10 @@ "service_description_nginx": "fornís o permet l’accès a totes los sites web albergats sus vòstre servidor", "service_description_nslcd": "gerís la connexion en linha de comanda dels utilizaires YunoHost", "service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas", - "service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl" + "service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl", + "migrations_backward": "Migracion endarrièr.", + "migrations_forward": "Migracion en avant", + "network_check_smtp_ko": "Lo trafic de corrièl sortent (pòrt 25 SMTP) sembla blocat per vòstra ret", + "network_check_smtp_ok": "Lo trafic de corrièl sortent (pòrt 25 SMTP) es pas blocat", + "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o0 per desactivar la quòta" } From f1c7839240904a3f7791266bdd32b566e6a391a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=B2u=20utilizaire?= Date: Tue, 29 May 2018 13:22:03 +0000 Subject: [PATCH 0955/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 99.0% (400 of 404 strings) --- locales/oc.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 51ca785c1..416ee2e09 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -385,7 +385,7 @@ "mysql_db_initialized": "La basa de donadas MySQL es estada inicializada", "no_restore_script": "Lo script de salvagarda es pas estat trobat per l’aplicacion « {app:s} »", "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", - "pattern_listname": "Deu èsser compausat solament de caractèrs alfanumerics e de tiret bas", + "pattern_listname": "Deu èsser compausat solament de caractèrs alfanumerics e de tirets basses", "service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)", "service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet", "service_description_glances": "susvelha las informacions sistèma de vòstre servidor", @@ -398,5 +398,9 @@ "migrations_forward": "Migracion en avant", "network_check_smtp_ko": "Lo trafic de corrièl sortent (pòrt 25 SMTP) sembla blocat per vòstra ret", "network_check_smtp_ok": "Lo trafic de corrièl sortent (pòrt 25 SMTP) es pas blocat", - "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o0 per desactivar la quòta" + "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o0 per desactivar la quòta", + "backup_archive_writing_error": "Impossible d’ajustar los fichièrs a la salvagarda dins l’archiu comprimit", + "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", + "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", + "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses" } From 53aa3fbf60913123dcccd55e7cebf55095405309 Mon Sep 17 00:00:00 2001 From: Quenti Date: Tue, 29 May 2018 19:20:31 +0000 Subject: [PATCH 0956/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 99.0% (400 of 404 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 416ee2e09..e5bf34f4d 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -394,7 +394,7 @@ "service_description_nslcd": "gerís la connexion en linha de comanda dels utilizaires YunoHost", "service_description_redis-server": "una basa de donadas especializada per un accès rapid a las donadas, las filas d’espèra e la comunicacion entre programas", "service_description_rspamd": "filtra lo corrièl pas desirat e mai foncionalitats ligadas al corrièl", - "migrations_backward": "Migracion endarrièr.", + "migrations_backward": "Migracion en darrièr.", "migrations_forward": "Migracion en avant", "network_check_smtp_ko": "Lo trafic de corrièl sortent (pòrt 25 SMTP) sembla blocat per vòstra ret", "network_check_smtp_ok": "Lo trafic de corrièl sortent (pòrt 25 SMTP) es pas blocat", From 054f0d89c3291eac7a3a3cf6eade5a0bb7f5c389 Mon Sep 17 00:00:00 2001 From: Quenti Date: Tue, 29 May 2018 19:21:40 +0000 Subject: [PATCH 0957/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 99.7% (403 of 404 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index e5bf34f4d..75830e52f 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -398,7 +398,7 @@ "migrations_forward": "Migracion en avant", "network_check_smtp_ko": "Lo trafic de corrièl sortent (pòrt 25 SMTP) sembla blocat per vòstra ret", "network_check_smtp_ok": "Lo trafic de corrièl sortent (pòrt 25 SMTP) es pas blocat", - "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o0 per desactivar la quòta", + "pattern_mailbox_quota": "Deu èsser una talha amb lo sufixe b/k/M/G/T o 0 per desactivar la quòta", "backup_archive_writing_error": "Impossible d’ajustar los fichièrs a la salvagarda dins l’archiu comprimit", "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", From e41887fb53ef66c34b56e81db878dca3fcc4897e Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 30 May 2018 05:31:20 +0000 Subject: [PATCH 0958/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 72 ++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 75830e52f..8221d5579 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -1,7 +1,7 @@ { - "admin_password": "Senhal d'administracion", + "admin_password": "Senhal d’administracion", "admin_password_change_failed": "Impossible de cambiar lo senhal", - "admin_password_changed": "Lo senhal d'administracion es ben estat cambiat", + "admin_password_changed": "Lo senhal d’administracion es ben estat cambiat", "app_already_installed": "{app:s} es ja installat", "app_already_up_to_date": "{app:s} es ja a jorn", "installation_complete": "Installacion acabada", @@ -46,7 +46,7 @@ "app_change_url_failed_nginx_reload": "La reaviada de nginx a fracassat. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}", "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain:s}{path:s}, pas res a far.", "app_change_url_success": "L’URL de l’aplicacion {app:s} a cambiat per {domain:s}{path:s}", - "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' es obsolèt ! Utilizatz 'app register-url' a la plaça !", + "app_checkurl_is_deprecated": "Packagers /!\\ ’app checkurl’ es obsolèt ! Utilizatz ’app register-url’ a la plaça !", "app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla", "app_incompatible": "L’aplicacion {app} es pas compatibla amb vòstra version de YunoHost", "app_location_already_used": "L’aplicacion « {app} » es ja installada a aqueste emplaçament ({path})", @@ -120,7 +120,7 @@ "backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.", "backup_with_no_restore_script_for_app": "L’aplicacion {app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.", "certmanager_acme_not_configured_for_domain": "Lo certificat del domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr cert-install per aqueste domeni.", - "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let's Encrypt. Impossible de lo renovar automaticament !", + "certmanager_attempt_to_renew_nonLE_cert": "Lo certificat pel domeni {domain:s} es pas provesit per Let’s Encrypt. Impossible de lo renovar automaticament !", "certmanager_attempt_to_renew_valid_cert": "Lo certificat pel domeni {domain:s} es a man d’expirar ! Utilizatz --force per cortcircuitar", "certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain:s} (fichièr : {file:s}), rason : {reason:s}", "certmanager_cert_install_success": "Installacion capitada del certificat Let’s Encrypt pel domeni {domain:s} !", @@ -232,24 +232,24 @@ "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »", "backup_output_directory_forbidden": "Repertòri de destinacion defendut. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)", - "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let's Encrypt pel domeni {domain:s} !", + "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni {domain:s} !", "certmanager_certificate_fetching_or_enabling_failed": "Sembla d’aver fracassat l’activacion d’un nòu certificat per {domain:s}…", "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr", - "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let's Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.", + "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let’s Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.", "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasetz, utilizatz --no-checks per desactivar las verificacions.)", - "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let'S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", + "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat.", "domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.", "domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS", - "domain_dyndns_dynette_is_unreachable": "Impossible de contactar la dynette YunoHost, siá YunoHost pas es pas corrèctament connectat a Internet, siá lo servidor de la dynett es arrestat. Error : {error}", + "domain_dyndns_dynette_is_unreachable": "Impossible de contactar la dynette YunoHost, siá YunoHost pas es pas corrèctament connectat a Internet, siá lo servidor de la dynett es arrestat. Error : {error}", "domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni", "firewall_reload_failed": "Impossible de recargar lo parafòc", "firewall_reloaded": "Lo parafòc es estat recargat", "firewall_rules_cmd_failed": "Unas règlas del parafòc an fracassat. Per mai informacions, consultatz lo jornal.", - "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {received_type:s}, esperat {expected_type:s}", - "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte. Recebut : {received_type:s}, esperat {expected_type:s}", - "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}", + "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {received_type:s}, esperat {expected_type:s}", + "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte. Recebut : {received_type:s}, esperat {expected_type:s}", + "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}", "global_settings_setting_example_enum": "Exemple d’opcion de tipe enumeracion", "global_settings_setting_example_int": "Exemple d’opcion de tipe entièr", "global_settings_setting_example_string": "Exemple d’opcion de tipe cadena", @@ -261,29 +261,29 @@ "ldap_init_failed_to_create_admin": "L’inicializacion de LDAP a pas pogut crear l’utilizaire admin", "mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut", "mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá", - "migrate_tsig_failed": "La migracion del domeni dyndns {domain} cap a hmac-sha512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}", + "migrate_tsig_failed": "La migracion del domeni dyndns {domain} cap a hmac-sha512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}", "migrate_tsig_wait": "Esperem 3 minutas que lo servidor dyndns prenga en compte la novèla clau…", - "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni dyndns, donc cap de migracion es pas necessària !", + "migrate_tsig_not_needed": "Sembla qu’utilizatz pas un domeni dyndns, donc cap de migracion es pas necessària !", "migration_0003_yunohost_upgrade": "Aviada de la mesa a nivèl del paquet YunoHost… La migracion acabarà, mas la mesa a jorn reala se realizarà tot bèl aprèp. Un còp acabada, poiretz vos reconnectar a l’administracion web.", "migration_0003_system_not_fully_up_to_date": "Lo sistèma es pas complètament a jorn. Mercés de lançar una mesa a jorn classica abans de començar la migracion per Stretch.", - "migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}", + "migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}", "monitor_period_invalid": "Lo periòde de temps es incorrècte", "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbabe", "monitor_stats_period_unavailable": "Cap d’estatisticas son pas disponiblas pel periòde", "mysql_db_init_failed": "Impossible d’inicializar la basa de donadas MySQL", - "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", "service_disabled": "Lo servici « {service:s} » es desactivat", - "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", "service_enabled": "Lo servici « {service:s} » es activat", "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »", "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…", - "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}", + "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}", "service_regenconf_pending_applying": "Aplicacion de as configuraion en espèra pel servici « {service} »…", "service_remove_failed": "Impossible de levar lo servici « {service:s} »", "service_removed": "Lo servici « {service:s} » es estat levat", - "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", "service_started": "Lo servici « {service:s} » es aviat", - "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}", + "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}", "ssowat_conf_generated": "La configuracion SSowat es generada", "ssowat_conf_updated": "La configuracion SSOwat es estada actualizada", "system_upgraded": "Lo sistèma es estat actualizat", @@ -316,27 +316,27 @@ "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", - "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}", + "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}", "service_add_failed": "Apondon impossible del servici « {service:s} »", "service_added": "Lo servici « {service:s} » es ajustat", "service_already_started": "Lo servici « {service:s} » es ja aviat", "service_already_stopped": "Lo servici « {service:s} » es ja arrestat", "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de restauracion", "restore_complete": "Restauracion acabada", - "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", + "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", "restore_failed": "Impossible de restaurar lo sistèma", "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tampauc dins l’archiu", - "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", + "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »", - "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", + "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", "restore_nothings_done": "Res es pas estat restaurat", "restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari", "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app:s} »…", "restore_running_hooks": "Execucion dels scripts de restauracion…", "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma", "server_shutdown": "Lo servidor serà atudat", - "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}", + "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}", "server_reboot": "Lo servidor es per reaviar", "network_check_mx_ko": "L’enregistrament DNS MX es pas especificat", "new_domain_required": "Vos cal especificar lo domeni màger", @@ -355,31 +355,31 @@ "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", - "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", - "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", - "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion 'letsencrypt' es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : aquò provarà de tornar installar los certificats de totes los domenis amb un certificat Let's Encrypt o las auto-signats", - "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", - "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}", - "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}", + "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", + "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", + "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion ’letsencrypt’ es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : aquò provarà de tornar installar los certificats de totes los domenis amb un certificat Let’s Encrypt o las auto-signats", + "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", + "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}", + "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}", "executing_command": "Execucion de la comanda « {command:s} »…", "executing_script": "Execucion del script « {script:s} »…", - "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}", + "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}", "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "update_cache_failed": "Impossible d’actualizar lo cache de l’APT", - "mail_alias_remove_failed": "Supression impossibla de l'alias de corrièl « {mail:s} »", + "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail:s} »", "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »", "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a hmac-sha512 que’s mai securizat", "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.", - "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", - "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.", - "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", + "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", + "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.", + "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", "migrations_skip_migration": "Passatge de la migracion {number} {name}…", "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisias > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", - "migrations_need_to_accept_disclaimer": "Per lançar la migracion {number} {name} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", + "migrations_need_to_accept_disclaimer": "Per lançar la migracion {number} {name} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", "monitor_disabled": "La supervision del servidor es desactivada", "monitor_enabled": "La supervision del servidor es activada", "mysql_db_initialized": "La basa de donadas MySQL es estada inicializada", From 48044b0f5ae966354890a9ae5936e82ef8a99af8 Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 30 May 2018 05:33:29 +0000 Subject: [PATCH 0959/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 174 ++++++++++++++++++++++++------------------------ 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 8221d5579..20686ade6 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -34,78 +34,78 @@ "ask_password": "Senhal", "ask_path": "Camin", "backup_action_required": "Devètz precisar çò que cal salvagardar", - "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app:s} »", + "backup_app_failed": "Impossible de salvagardar l’aplicacion « {app:s} »", "backup_applying_method_copy": "Còpia de totes los fichièrs dins la salvagarda…", "backup_applying_method_tar": "Creacion de l’archiu tar de la salvagarda…", "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja", - "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut", - "action_invalid": "Accion « {action:s} » incorrècte", - "app_argument_choice_invalid": "Causida invalida pel paramètre « {name:s} », cal que siá un de {choices:s}", - "app_argument_invalid": "Valor invalida pel paramètre « {name:s} » : {error:s}", - "app_argument_required": "Lo paramètre « {name:s} » es requesit", + "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut", + "action_invalid": "Accion « {action:s} » incorrècte", + "app_argument_choice_invalid": "Causida invalida pel paramètre « {name:s} », cal que siá un de {choices:s}", + "app_argument_invalid": "Valor invalida pel paramètre « {name:s} » : {error:s}", + "app_argument_required": "Lo paramètre « {name:s} » es requesit", "app_change_url_failed_nginx_reload": "La reaviada de nginx a fracassat. Vaquí la sortida de « nginx -t » :\n{nginx_errors:s}", "app_change_url_identical_domains": "L’ancian e lo novèl coble domeni/camin son identics per {domain:s}{path:s}, pas res a far.", "app_change_url_success": "L’URL de l’aplicacion {app:s} a cambiat per {domain:s}{path:s}", "app_checkurl_is_deprecated": "Packagers /!\\ ’app checkurl’ es obsolèt ! Utilizatz ’app register-url’ a la plaça !", "app_extraction_failed": "Extraccion dels fichièrs d’installacion impossibla", "app_incompatible": "L’aplicacion {app} es pas compatibla amb vòstra version de YunoHost", - "app_location_already_used": "L’aplicacion « {app} » es ja installada a aqueste emplaçament ({path})", + "app_location_already_used": "L’aplicacion « {app} » es ja installada a aqueste emplaçament ({path})", "app_manifest_invalid": "Manifest d’aplicacion incorrècte : {error}", "app_package_need_update": "Lo paquet de l’aplicacion {app} deu èsser mes a jorn per seguir los cambiaments de YunoHost", "app_requirements_checking": "Verificacion dels paquets requesida per {app}...", "app_sources_fetch_failed": "Recuperacion dels fichièrs fonts impossibla", "app_unsupported_remote_type": "Lo tipe alonhat utilizat per l’aplicacion es pas suportat", "appslist_retrieve_error": "Impossible de recuperar la lista d’aplicacions alonhadas {appslist:s} : {error:s}", - "backup_archive_app_not_found": "L’aplicacion « {app:s} » es pas estada trobada dins l’archiu de la salvagarda", + "backup_archive_app_not_found": "L’aplicacion « {app:s} » es pas estada trobada dins l’archiu de la salvagarda", "backup_archive_broken_link": "Impossible d‘accedir a l’archiu de salvagarda (ligam invalid cap a {path:s})", "backup_archive_mount_failed": "Lo montatge de l’archiu de salvagarda a fracassat", "backup_archive_open_failed": "Impossible de dobrir l’archiu de salvagarda", - "backup_archive_system_part_not_available": "La part « {part:s} » del sistèma es pas disponibla dins aquesta salvagarda", + "backup_archive_system_part_not_available": "La part « {part:s} » del sistèma es pas disponibla dins aquesta salvagarda", "backup_cleaning_failed": "Impossible de netejar lo repertòri temporari de salvagarda", "backup_copying_to_organize_the_archive": "Còpia de {size:s} Mio per organizar l’archiu", "backup_created": "Salvagarda acabada", "backup_creating_archive": "Creacion de l’archiu de salvagarda...", "backup_creation_failed": "Impossible de crear la salvagarda", - "app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.", + "app_already_installed_cant_change_url": "Aquesta aplicacion es ja installada. Aquesta foncion pòt pas simplament cambiar l’URL. Agachatz « app changeurl » s’es disponible.", "app_change_no_change_url_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, poiretz aver de la metre a jorn.", "app_change_url_no_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, benlèu que vos cal la metre a jorn.", - "app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}", + "app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}", "app_location_install_failed": "Impossible d’installar l’aplicacion a aqueste emplaçament per causa de conflicte amb l’aplicacion {other_app} qu’es ja installada sus {other_path}", "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta", "appslist_corrupted_json": "Cargament impossible de la lista d’aplicacion. Sembla que {filename:s} siá gastat.", - "backup_delete_error": "Impossible de suprimir « {path:s} »", + "backup_delete_error": "Impossible de suprimir « {path:s} »", "backup_deleted": "La salvagarda es estada suprimida", - "backup_hook_unknown": "Script de salvagarda « {hook:s} » desconegut", + "backup_hook_unknown": "Script de salvagarda « {hook:s} » desconegut", "backup_invalid_archive": "Archiu de salvagarda incorrècte", "backup_method_borg_finished": "La salvagarda dins Borg es acabada", "backup_method_copy_finished": "La còpia de salvagarda es acabada", "backup_method_tar_finished": "L’archiu tar de la salvagarda es estat creat", "backup_output_directory_not_empty": "Lo dorsièr de sortida es pas void", "backup_output_directory_required": "Vos cal especificar un dorsièr de sortida per la salvagarda", - "backup_running_app_script": "Lançament de l’escript de salvagarda de l’aplicacion « {app:s} »...", + "backup_running_app_script": "Lançament de l’escript de salvagarda de l’aplicacion « {app:s} »...", "backup_running_hooks": "Execucion dels scripts de salvagarda...", - "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma", + "backup_system_part_failed": "Impossible de salvagardar la part « {part:s} » del sistèma", "app_requirements_failed": "Impossible de complir las condicions requesidas per {app} : {error}", "app_requirements_unmeet": "Las condicions requesidas per {app} son pas complidas, lo paquet {pkgname} ({version}) deu èsser {spec}", "appslist_could_not_migrate": "Migracion de la lista impossibla {appslist:s} ! Impossible d’analizar l’URL… L’anciana tasca cron es estada servada dins {bkp_file:s}.", "backup_abstract_method": "Aqueste metòde de salvagarda es pas encara implementat", - "backup_applying_method_custom": "Crida lo metòde de salvagarda personalizat « {method:s} »…", + "backup_applying_method_custom": "Crida lo metòde de salvagarda personalizat « {method:s} »…", "backup_borg_not_implemented": "Lo metòde de salvagarda Bord es pas encara implementat", "backup_couldnt_bind": "Impossible de ligar {src:s} amb {dest:s}.", "backup_csv_addition_failed": "Impossible d’ajustar de fichièrs a la salvagarda dins lo fichièr CSV", - "backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »", - "backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »", - "backup_custom_need_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « need_mount »", - "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat", + "backup_custom_backup_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « backup »", + "backup_custom_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « mount »", + "backup_custom_need_mount_error": "Fracàs del metòde de salvagarda personalizat a l’etapa « need_mount »", + "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat", "backup_nothings_done": "I a pas res de salvagardar", "backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid", - "service_status_failed": "Impossible dedeterminar l’estat del servici « {service:s} »", - "service_stopped": "Lo servici « {service:s} » es estat arrestat", - "service_unknown": "Servici « {service:s} » desconegut", - "unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada", - "unit_unknown": "Unitat « {unit:s} » desconeguda", + "service_status_failed": "Impossible dedeterminar l’estat del servici « {service:s} »", + "service_stopped": "Lo servici « {service:s} » es estat arrestat", + "service_unknown": "Servici « {service:s} » desconegut", + "unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada", + "unit_unknown": "Unitat « {unit:s} » desconeguda", "unlimit": "Cap de quòta", - "unrestore_app": "L’aplicacion « {app:s} » serà pas restaurada", + "unrestore_app": "L’aplicacion « {app:s} » serà pas restaurada", "upnp_dev_not_found": "Cap de periferic compatible UPnP pas trobat", "upnp_disabled": "UPnP es desactivat", "upnp_enabled": "UPnP es activat", @@ -116,7 +116,7 @@ "backup_applying_method_borg": "Mandadís de totes los fichièrs a la salvagarda dins lo repertòri borg-backup…", "backup_csv_creation_failed": "Creacion impossibla del fichièr CSV necessari a las operacions futuras de restauracion", "backup_extracting_archive": "Extraccion de l’archiu de salvagarda…", - "backup_output_symlink_dir_broken": "Avètz un ligam simbolic copat allòc de vòstre repertòri d’archiu « {path:s} ». Poiriatz aver una configuracion personalizada per salvagardar vòstras donadas sus un autre sistèma de fichièrs, en aquel cas, saique oblidèretz de montar o de connectar lo disc o la clau USB.", + "backup_output_symlink_dir_broken": "Avètz un ligam simbolic copat allòc de vòstre repertòri d’archiu « {path:s} ». Poiriatz aver una configuracion personalizada per salvagardar vòstras donadas sus un autre sistèma de fichièrs, en aquel cas, saique oblidèretz de montar o de connectar lo disc o la clau USB.", "backup_with_no_backup_script_for_app": "L’aplicacion {app:s} a pas cap de script de salvagarda. I fasèm pas cas.", "backup_with_no_restore_script_for_app": "L’aplicacion {app:s} a pas cap de script de restauracion, poiretz pas restaurar automaticament la salvagarda d’aquesta aplicacion.", "certmanager_acme_not_configured_for_domain": "Lo certificat del domeni {domain:s} sembla pas corrèctament installat. Mercés de lançar d’en primièr cert-install per aqueste domeni.", @@ -127,7 +127,7 @@ "certmanager_cert_install_success_selfsigned": "Installacion capitada del certificat auto-signat pel domeni {domain:s} !", "certmanager_cert_signing_failed": "Fracàs de la signatura del noù certificat", "certmanager_domain_cert_not_selfsigned": "Lo certificat del domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utiliatz --force)", - "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz ço que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz ço que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e nginx son corrèctas", "certmanager_domain_unknown": "Domeni desconegut {domain:s}", "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain:s} (fichièr : {file:s})", @@ -138,7 +138,7 @@ "diagnosis_debian_version_error": "Impossible de determinar la version de Debian : {error}", "diagnosis_kernel_version_error": "Impossible de recuperar la version del nuclèu : {error}", "diagnosis_no_apps": "Pas cap d’aplicacion installada", - "dnsmasq_isnt_installed": "dnsmasq sembla pas èsser installat, mercés de lançar « apt-get remove bind9 && apt-get install dnsmasq »", + "dnsmasq_isnt_installed": "dnsmasq sembla pas èsser installat, mercés de lançar « apt-get remove bind9 && apt-get install dnsmasq »", "domain_cannot_remove_main": "Impossible de levar lo domeni màger. Definissètz un novèl domeni màger d’en primièr", "domain_cert_gen_failed": "Generacion del certificat impossibla", "domain_created": "Lo domeni es creat", @@ -169,10 +169,10 @@ "dyndns_domain_not_provided": "Lo provesidor DynDNS {provider:s} pòt pas fornir lo domeni {domain:s}.", "dyndns_unavailable": "Lo domeni {domain:s} es pas disponible.", "extracting": "Extraccion…", - "field_invalid": "Camp incorrècte : « {:s} »", + "field_invalid": "Camp incorrècte : « {:s} »", "format_datetime_short": "%d/%m/%Y %H:%M", "global_settings_cant_open_settings": "Fracàs de la dobertura del fichièr de configuracion, rason : {reason:s}", - "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en picant « yunohost settings list »", + "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en picant « yunohost settings list »", "global_settings_reset_success": "Capitada ! Vòstra configuracion precedenta es estada salvagarda dins {path:s}", "global_settings_setting_example_bool": "Exemple d’opcion booleana", "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartat e salvagardat dins /etc/yunohost/unkown_settings.json", @@ -209,8 +209,8 @@ "mysql_db_creation_failed": "Creacion de la basa de donadas MySQL impossibla", "no_appslist_found": "Cap de lista d’aplicacions pas trobaba", "no_internet_connection": "Lo servidor es pas connectat a Internet", - "package_not_installed": "Lo paquet « {pkgname} » es pas installat", - "package_unknown": "Paquet « {pkgname} » desconegut", + "package_not_installed": "Lo paquet « {pkgname} » es pas installat", + "package_unknown": "Paquet « {pkgname} » desconegut", "packages_no_upgrade": "I a pas cap de paquet d’actualizar", "packages_upgrade_failed": "Actualizacion de totes los paquets impossibla", "path_removal_failed": "Impossible de suprimir lo camin {:s}", @@ -226,10 +226,10 @@ "port_already_opened": "Lo pòrt {port:d} es ja dubèrt per las connexions {ip_version:s}", "port_available": "Lo pòrt {port:d} es disponible", "port_unavailable": "Lo pòrt {port:d} es pas disponible", - "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", - "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »", + "restore_already_installed_app": "Una aplicacion es ja installada amb l’id « {app:s} »", + "restore_app_failed": "Impossible de restaurar l’aplicacion « {app:s} »", "backup_ask_for_copying_if_needed": "D’unes fichièrs an pas pogut èsser preparatz per la salvagarda en utilizar lo metòde qu’evita de gastar d’espaci sul sistèma de manièra temporària. Per lançar la salvagarda, cal utilizar temporàriament {size:s} Mo. Acceptatz ?", - "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »", + "yunohost_not_installed": "YunoHost es pas installat o corrèctament installat. Mercés d’executar « yunohost tools postinstall »", "backup_output_directory_forbidden": "Repertòri de destinacion defendut. Las salvagardas pòdon pas se realizar dins los repertòris bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "certmanager_attempt_to_replace_valid_cert": "Sètz a remplaçar un certificat corrècte e valid pel domeni {domain:s} ! (Utilizatz --force per cortcircuitar)", "certmanager_cert_renew_success": "Renovèlament capitat d’un certificat Let’s Encrypt pel domeni {domain:s} !", @@ -237,7 +237,7 @@ "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr", "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let’s Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.", "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasetz, utilizatz --no-checks per desactivar las verificacions.)", - "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", + "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat.", "domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.", @@ -254,12 +254,12 @@ "global_settings_setting_example_int": "Exemple d’opcion de tipe entièr", "global_settings_setting_example_string": "Exemple d’opcion de tipe cadena", "global_settings_unknown_type": "Situacion inesperada, la configuracion {setting:s} sembla d’aver lo tipe {unknown_type:s} mas es pas un tipe pres en carga pel sistèma.", - "hook_exec_failed": "Fracàs de l’execucion del script « {path:s} »", - "hook_exec_not_terminated": "L’execucion del escript « {path:s} » es pas acabada", + "hook_exec_failed": "Fracàs de l’execucion del script « {path:s} »", + "hook_exec_not_terminated": "L’execucion del escript « {path:s} » es pas acabada", "hook_list_by_invalid": "La proprietat de tria de las accions es invalida", - "hook_name_unknown": "Nom de script « {name:s} » desconegut", + "hook_name_unknown": "Nom de script « {name:s} » desconegut", "ldap_init_failed_to_create_admin": "L’inicializacion de LDAP a pas pogut crear l’utilizaire admin", - "mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut", + "mail_domain_unknown": "Lo domeni de corrièl « {domain:s} » es desconegut", "mailbox_used_space_dovecot_down": "Lo servici corrièl Dovecot deu èsser aviat, se volètz conéisser l’espaci ocupat per la messatjariá", "migrate_tsig_failed": "La migracion del domeni dyndns {domain} cap a hmac-sha512 a pas capitat, anullacion de las modificacions. Error : {error_code} - {error}", "migrate_tsig_wait": "Esperem 3 minutas que lo servidor dyndns prenga en compte la novèla clau…", @@ -271,19 +271,19 @@ "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbabe", "monitor_stats_period_unavailable": "Cap d’estatisticas son pas disponiblas pel periòde", "mysql_db_init_failed": "Impossible d’inicializar la basa de donadas MySQL", - "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", - "service_disabled": "Lo servici « {service:s} » es desactivat", - "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", - "service_enabled": "Lo servici « {service:s} » es activat", - "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »", - "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…", + "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_disabled": "Lo servici « {service:s} » es desactivat", + "service_enable_failed": "Impossible d’activar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_enabled": "Lo servici « {service:s} » es activat", + "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »", + "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…", "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}", - "service_regenconf_pending_applying": "Aplicacion de as configuraion en espèra pel servici « {service} »…", - "service_remove_failed": "Impossible de levar lo servici « {service:s} »", - "service_removed": "Lo servici « {service:s} » es estat levat", - "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", - "service_started": "Lo servici « {service:s} » es aviat", - "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}", + "service_regenconf_pending_applying": "Aplicacion de as configuraion en espèra pel servici « {service} »…", + "service_remove_failed": "Impossible de levar lo servici « {service:s} »", + "service_removed": "Lo servici « {service:s} » es estat levat", + "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", + "service_started": "Lo servici « {service:s} » es aviat", + "service_stop_failed": "Impossible d’arrestar lo servici « {service:s} »↵\n\nJornals recents : {logs:s}", "ssowat_conf_generated": "La configuracion SSowat es generada", "ssowat_conf_updated": "La configuracion SSOwat es estada actualizada", "system_upgraded": "Lo sistèma es estat actualizat", @@ -297,56 +297,56 @@ "user_deletion_failed": "Supression impossibla de l’utilizaire", "user_home_creation_failed": "Creacion impossibla del repertòri personal a l’utilizaire", "user_info_failed": "Recuperacion impossibla de las informacions tocant l’utilizaire", - "user_unknown": "Utilizaire « {user:s} » desconegut", + "user_unknown": "Utilizaire « {user:s} » desconegut", "user_update_failed": "Modificacion impossibla de l’utilizaire", "user_updated": "L’utilizaire es estat modificat", "yunohost_ca_creation_failed": "Creacion impossibla de l’autoritat de certificacion", "yunohost_ca_creation_success": "L’autoritat de certificacion locala es creada.", - "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat.", - "service_conf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat", - "service_conf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es suprimit manualament e serà pas creat", - "service_conf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »", - "service_conf_file_removed": "Lo fichièr de configuracion « {conf} » es suprimit", - "service_conf_file_updated": "Lo fichièr de configuracion « {conf} » es actualizat", - "service_conf_new_managed_file": "Lo servici {service} gerís ara lo fichièr de configuracion « {conf} ».", - "service_conf_up_to_date": "La configuracion del servici « {service} » es ja actualizada", - "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada", + "service_conf_file_kept_back": "Lo fichièr de configuracion « {conf} » deuriá èsser suprimit pel servici {service} mas es estat servat.", + "service_conf_file_manually_modified": "Lo fichièr de configuracion « {conf} » es estat modificat manualament e serà pas actualizat", + "service_conf_file_manually_removed": "Lo fichièr de configuracion « {conf} » es suprimit manualament e serà pas creat", + "service_conf_file_remove_failed": "Supression impossibla del fichièr de configuracion « {conf} »", + "service_conf_file_removed": "Lo fichièr de configuracion « {conf} » es suprimit", + "service_conf_file_updated": "Lo fichièr de configuracion « {conf} » es actualizat", + "service_conf_new_managed_file": "Lo servici {service} gerís ara lo fichièr de configuracion « {conf} ».", + "service_conf_up_to_date": "La configuracion del servici « {service} » es ja actualizada", + "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada", "service_description_avahi-daemon": "permet d’aténher vòstre servidr via yunohost.local sus vòstre ret local", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", - "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", - "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", + "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", + "service_conf_file_copy_failed": "Còpia impossibla del nòu fichièr de configuracion « {new} » cap a « {conf} »", "server_reboot_confirm": "Lo servidor es per reaviar sul pic, o volètz vertadièrament ? {answers:s}", - "service_add_failed": "Apondon impossible del servici « {service:s} »", - "service_added": "Lo servici « {service:s} » es ajustat", - "service_already_started": "Lo servici « {service:s} » es ja aviat", - "service_already_stopped": "Lo servici « {service:s} » es ja arrestat", + "service_add_failed": "Apondon impossible del servici « {service:s} »", + "service_added": "Lo servici « {service:s} » es ajustat", + "service_already_started": "Lo servici « {service:s} » es ja aviat", + "service_already_stopped": "Lo servici « {service:s} » es ja arrestat", "restore_cleaning_failed": "Impossible de netejar lo repertòri temporari de restauracion", "restore_complete": "Restauracion acabada", "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", "restore_failed": "Impossible de restaurar lo sistèma", - "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tampauc dins l’archiu", + "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tampauc dins l’archiu", "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", - "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »", + "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »", "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", "restore_nothings_done": "Res es pas estat restaurat", "restore_removing_tmp_dir_failed": "Impossible de levar u ancian repertòri temporari", - "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app:s} »…", + "restore_running_app_script": "Lançament del script de restauracion per l’aplicacion « {app:s} »…", "restore_running_hooks": "Execucion dels scripts de restauracion…", - "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma", + "restore_system_part_failed": "Restauracion impossibla de la part « {part:s} » del sistèma", "server_shutdown": "Lo servidor serà atudat", "server_shutdown_confirm": "Lo servidor es per s’atudar sul pic, o volètz vertadièrament ? {answers:s}", "server_reboot": "Lo servidor es per reaviar", "network_check_mx_ko": "L’enregistrament DNS MX es pas especificat", "new_domain_required": "Vos cal especificar lo domeni màger", "no_ipv6_connectivity": "La connectivitat IPv6 es pas disponibla", - "not_enough_disk_space": "Espaci disc insufisent sus « {path:s} »", - "package_unexpected_error": "Una error inesperada es apareguda amb lo paquet « {pkgname} »", + "not_enough_disk_space": "Espaci disc insufisent sus « {path:s} »", + "package_unexpected_error": "Una error inesperada es apareguda amb lo paquet « {pkgname} »", "packages_upgrade_critical_later": "Los paquets critics {packages:s} seràn actualizats mai tard", "restore_action_required": "Devètz precisar çò que cal restaurar", - "service_cmd_exec_failed": "Impossible d’executar la comanda « {command:s} »", - "service_conf_updated": "La configuracion es estada actualizada pel servici « {service} »", + "service_cmd_exec_failed": "Impossible d’executar la comanda « {command:s} »", + "service_conf_updated": "La configuracion es estada actualizada pel servici « {service} »", "service_description_mysql": "garda las donadas de las aplicacions (base de donadas SQL)", "service_description_php5-fpm": "executa d’aplicacions escrichas en PHP amb nginx", "service_description_postfix": "emplegat per enviar e recebre de corrièls", @@ -361,30 +361,30 @@ "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", "diagnosis_monitor_network_error": "Impossible de supervisar la ret : {error}", "diagnosis_monitor_system_error": "Impossible de supervisar lo sistèma : {error}", - "executing_command": "Execucion de la comanda « {command:s} »…", - "executing_script": "Execucion del script « {script:s} »…", + "executing_command": "Execucion de la comanda « {command:s} »…", + "executing_script": "Execucion del script « {script:s} »…", "global_settings_cant_serialize_settings": "Fracàs de la serializacion de las donadas de parametratge, rason : {reason:s}", "ip6tables_unavailable": "Podètz pas jogar amb ip6tables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "iptables_unavailable": "Podètz pas jogar amb iptables aquí. Siá sèts dins un contenedor, siá vòstre nuclèu es pas compatible amb aquela opcion", "update_cache_failed": "Impossible d’actualizar lo cache de l’APT", - "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail:s} »", - "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »", - "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a hmac-sha512 que’s mai securizat", - "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", + "mail_alias_remove_failed": "Supression impossibla de l’alias de corrièl « {mail:s} »", + "mail_forward_remove_failed": "Supression impossibla del corrièl de transferiment « {mail:s} »", + "migrate_tsig_start": "L’algorisme de generacion de claus es pas pro securizat per la signatura TSIG del domeni « {domain} », lançament de la migracion cap a hmac-sha512 que’s mai securizat", + "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.", "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.", "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", - "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", + "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", "migrations_skip_migration": "Passatge de la migracion {number} {name}…", - "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisias > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", + "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisias > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", "migrations_need_to_accept_disclaimer": "Per lançar la migracion {number} {name} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", "monitor_disabled": "La supervision del servidor es desactivada", "monitor_enabled": "La supervision del servidor es activada", "mysql_db_initialized": "La basa de donadas MySQL es estada inicializada", - "no_restore_script": "Lo script de salvagarda es pas estat trobat per l’aplicacion « {app:s} »", - "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", + "no_restore_script": "Lo script de salvagarda es pas estat trobat per l’aplicacion « {app:s} »", + "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", "pattern_listname": "Deu èsser compausat solament de caractèrs alfanumerics e de tirets basses", "service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)", "service_description_fail2ban": "protegís contra los atacs brute-force e d’autres atacs venents d’Internet", From 2f868f6c9cf1366e7f080acae3b23828e6a0e15d Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 05:12:59 +0000 Subject: [PATCH 0960/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 29.4% (119 of 404 strings) --- locales/pt.json | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index ee94b6352..1dddf2498 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -2,14 +2,14 @@ "action_invalid": "Acção Inválida '{action:s}'", "admin_password": "Senha de administração", "admin_password_change_failed": "Não foi possível alterar a senha", - "admin_password_changed": "Senha de administração alterada com êxito", + "admin_password_changed": "A palavra-passe de administração foi alterada com sucesso", "app_already_installed": "{app:s} já está instalada", "app_extraction_failed": "Não foi possível extrair os ficheiros para instalação", "app_id_invalid": "A ID da aplicação é inválida", "app_install_files_invalid": "Ficheiros para instalação corrompidos", - "app_location_already_used": "Já existe uma aplicação instalada neste diretório", - "app_location_install_failed": "Não é possível instalar a aplicação neste diretório", - "app_manifest_invalid": "Manifesto da aplicação inválido", + "app_location_already_used": "A aplicação {app} Já está instalada nesta localização ({path})", + "app_location_install_failed": "Não é possível instalar a aplicação neste diretório porque está em conflito com a aplicação '{other_app}', que já está instalada no diretório '{other_path}'", + "app_manifest_invalid": "Manifesto da aplicação inválido: {error}", "app_no_upgrade": "Não existem aplicações para atualizar", "app_not_installed": "{app:s} não está instalada", "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette", @@ -145,10 +145,10 @@ "yunohost_configured": "YunoHost configurada com êxito", "yunohost_installing": "A instalar a YunoHost...", "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'.", - "app_incompatible": "A aplicação é incompatível com a sua versão de Yunohost", + "app_incompatible": "A aplicação {app} é incompatível com a sua versão de Yunohost", "app_not_correctly_installed": "{app:s} parece não estar corretamente instalada", "app_not_properly_removed": "{app:s} não foi corretamente removido", - "app_requirements_checking": "Verificando os pacotes necessários...", + "app_requirements_checking": "Verificando os pacotes necessários para {app}...", "app_unsupported_remote_type": "Remoto tipo utilizado para a aplicação não suportado", "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup", "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path:s} )", @@ -168,5 +168,9 @@ "app_argument_choice_invalid": "Escolha inválida para o argumento '{name:s}', deve ser um dos {choices:s}", "app_argument_invalid": "Valor inválido de argumento '{name:s}': {error:s}", "app_argument_required": "O argumento '{name:s}' é obrigatório", - "app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors:s}" + "app_change_url_failed_nginx_reload": "Falha ao reiniciar o nginx. Aqui está o retorno de 'nginx -t':\n{nginx_errors:s}", + "app_change_no_change_url_script": "A aplicação {app_name:s} ainda não permite mudança da URL, talvez seja necessário atualiza-la.", + "app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada", + "app_package_need_update": "O pacote da aplicação {app} precisa ser atualizado para aderir as mudanças do YunoHost", + "app_requirements_failed": "Não foi possível atender às requisições da aplicação {app}: {error}" } From f46b12ac340ebd585c28e839f109c8ce51e846da Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 05:33:58 +0000 Subject: [PATCH 0961/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 29.9% (121 of 404 strings) --- locales/pt.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index 1dddf2498..e701288c6 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -14,9 +14,9 @@ "app_not_installed": "{app:s} não está instalada", "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette", "app_removed": "{app:s} removida com êxito", - "app_sources_fetch_failed": "Impossível obter os ficheiros fonte", + "app_sources_fetch_failed": "Incapaz obter os ficheiros fonte", "app_unknown": "Aplicação desconhecida", - "app_upgrade_failed": "Impossível atualizar {app:s}", + "app_upgrade_failed": "Impossibilitado de atualizar {app:s}", "app_upgraded": "{app:s} atualizada com sucesso", "appslist_fetched": "Lista de aplicações processada com sucesso", "appslist_removed": "Lista de aplicações removida com sucesso", @@ -149,7 +149,7 @@ "app_not_correctly_installed": "{app:s} parece não estar corretamente instalada", "app_not_properly_removed": "{app:s} não foi corretamente removido", "app_requirements_checking": "Verificando os pacotes necessários para {app}...", - "app_unsupported_remote_type": "Remoto tipo utilizado para a aplicação não suportado", + "app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado", "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup", "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path:s} )", "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup", @@ -172,5 +172,7 @@ "app_change_no_change_url_script": "A aplicação {app_name:s} ainda não permite mudança da URL, talvez seja necessário atualiza-la.", "app_location_unavailable": "Esta url não está disponível ou está em conflito com outra aplicação já instalada", "app_package_need_update": "O pacote da aplicação {app} precisa ser atualizado para aderir as mudanças do YunoHost", - "app_requirements_failed": "Não foi possível atender às requisições da aplicação {app}: {error}" + "app_requirements_failed": "Não foi possível atender aos requisitos da aplicação {app}: {error}", + "app_upgrade_app_name": "Atualizando aplicação {app}…", + "app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações" } From c72aa4fbff16cb606f4bfeb7cbfd3360dcc405aa Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 05:47:13 +0000 Subject: [PATCH 0962/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 32.1% (130 of 404 strings) --- locales/pt.json | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index e701288c6..61a88eb98 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -16,17 +16,17 @@ "app_removed": "{app:s} removida com êxito", "app_sources_fetch_failed": "Incapaz obter os ficheiros fonte", "app_unknown": "Aplicação desconhecida", - "app_upgrade_failed": "Impossibilitado de atualizar {app:s}", + "app_upgrade_failed": "Não foi possível atualizar {app:s}", "app_upgraded": "{app:s} atualizada com sucesso", - "appslist_fetched": "Lista de aplicações processada com sucesso", - "appslist_removed": "Lista de aplicações removida com sucesso", - "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas", - "appslist_unknown": "Lista de aplicaçoes desconhecida", - "ask_current_admin_password": "Senha de administração atual", - "ask_email": "Correio eletrónico", + "appslist_fetched": "A lista de aplicações, {appslist:s}, foi trazida com sucesso", + "appslist_removed": "A Lista de aplicações {appslist:s} foi removida", + "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas {appslist:s}: {error:s}", + "appslist_unknown": "Desconhece-se a lista de aplicaçoes {appslist:s}.", + "ask_current_admin_password": "Senha atual da administração", + "ask_email": "Endereço de Email", "ask_firstname": "Primeiro nome", "ask_lastname": "Último nome", - "ask_list_to_remove": "Lista para remover", + "ask_list_to_remove": "Lista à remover", "ask_main_domain": "Domínio principal", "ask_new_admin_password": "Senha de administração nova", "ask_password": "Senha", @@ -174,5 +174,10 @@ "app_package_need_update": "O pacote da aplicação {app} precisa ser atualizado para aderir as mudanças do YunoHost", "app_requirements_failed": "Não foi possível atender aos requisitos da aplicação {app}: {error}", "app_upgrade_app_name": "Atualizando aplicação {app}…", - "app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações" + "app_upgrade_some_app_failed": "Não foi possível atualizar algumas aplicações", + "appslist_corrupted_json": "Falha ao carregar a lista de aplicações. O arquivo {filename:s} aparenta estar corrompido.", + "appslist_migrating": "Migando lista de aplicações {appslist:s}…", + "appslist_name_already_tracked": "Já existe uma lista de aplicações registrada com o nome {name:s}.", + "appslist_retrieve_bad_format": "O arquivo recuperado para a lista de aplicações {appslist:s} é invalido", + "appslist_url_already_tracked": "Já existe uma lista de aplicações registrada com a url {url:s}." } From 4fe8c7430db2e5abc765ca11a22c653c9f176db4 Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 06:02:13 +0000 Subject: [PATCH 0963/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 33.1% (134 of 404 strings) --- locales/pt.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index 61a88eb98..0595ff107 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -26,9 +26,9 @@ "ask_email": "Endereço de Email", "ask_firstname": "Primeiro nome", "ask_lastname": "Último nome", - "ask_list_to_remove": "Lista à remover", + "ask_list_to_remove": "Lista para remover", "ask_main_domain": "Domínio principal", - "ask_new_admin_password": "Senha de administração nova", + "ask_new_admin_password": "Nova senha de administração", "ask_password": "Senha", "backup_created": "Backup completo", "backup_creating_archive": "A criar ficheiro de backup...", @@ -179,5 +179,9 @@ "appslist_migrating": "Migando lista de aplicações {appslist:s}…", "appslist_name_already_tracked": "Já existe uma lista de aplicações registrada com o nome {name:s}.", "appslist_retrieve_bad_format": "O arquivo recuperado para a lista de aplicações {appslist:s} é invalido", - "appslist_url_already_tracked": "Já existe uma lista de aplicações registrada com a url {url:s}." + "appslist_url_already_tracked": "Já existe uma lista de aplicações registrada com a url {url:s}.", + "ask_path": "Caminho", + "backup_abstract_method": "Este metodo de backup ainda não foi implementado", + "backup_action_required": "Deve-se especificar algo a salvar", + "backup_app_failed": "Não foi possível de fazer o backup dos aplicativos '{app:s}'" } From 414f9db5c33e85650d88bee513a4be9913b0ecb8 Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 06:05:34 +0000 Subject: [PATCH 0964/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 35.1% (142 of 404 strings) --- locales/pt.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index 0595ff107..e024b6787 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -151,11 +151,11 @@ "app_requirements_checking": "Verificando os pacotes necessários para {app}...", "app_unsupported_remote_type": "A aplicação não possui suporte ao tipo remoto utilizado", "backup_archive_app_not_found": "A aplicação '{app:s}' não foi encontrada no arquivo de backup", - "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado para {path:s} )", + "backup_archive_broken_link": "Impossível acessar o arquivo de backup (link quebrado ao {path:s})", "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup", "backup_archive_name_exists": "O nome do arquivo de backup já existe", - "backup_archive_open_failed": "Impossível abrir o arquivo de backup", - "backup_cleaning_failed": "Impossível apagar o diretório temporário de backup", + "backup_archive_open_failed": "Não é possível abrir o arquivo de backup", + "backup_cleaning_failed": "Não é possível limpar a pasta de backup temporário", "backup_creation_failed": "A criação do backup falhou", "backup_delete_error": "Impossível apagar '{path:s}'", "backup_deleted": "O backup foi suprimido", @@ -183,5 +183,13 @@ "ask_path": "Caminho", "backup_abstract_method": "Este metodo de backup ainda não foi implementado", "backup_action_required": "Deve-se especificar algo a salvar", - "backup_app_failed": "Não foi possível de fazer o backup dos aplicativos '{app:s}'" + "backup_app_failed": "Não foi possível fazer o backup dos aplicativos '{app:s}'", + "backup_applying_method_custom": "Chamando o metodo personalizado de backup '{method:s}'…", + "backup_applying_method_tar": "Criando o arquivo tar de backup…", + "backup_archive_mount_failed": "Falha ao montar o arquivo de backup", + "backup_archive_name_unknown": "Desconhece-se o arquivo local de backup de nome '{name:s}'", + "backup_archive_system_part_not_available": "A seção do sistema '{part:s}' está indisponivel neste backup", + "backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?", + "backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.", + "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido" } From 8fd75ea014235deb6c320afefa366fa3f9edca28 Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 30 May 2018 17:11:22 +0000 Subject: [PATCH 0965/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 20686ade6..f5f9f911d 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -39,7 +39,7 @@ "backup_applying_method_tar": "Creacion de l’archiu tar de la salvagarda…", "backup_archive_name_exists": "Un archiu de salvagarda amb aquesta nom existís ja", "backup_archive_name_unknown": "L’archiu local de salvagarda apelat « {name:s} » es desconegut", - "action_invalid": "Accion « {action:s} » incorrècte", + "action_invalid": "Accion « {action:s} » incorrècta", "app_argument_choice_invalid": "Causida invalida pel paramètre « {name:s} », cal que siá un de {choices:s}", "app_argument_invalid": "Valor invalida pel paramètre « {name:s} » : {error:s}", "app_argument_required": "Lo paramètre « {name:s} » es requesit", @@ -99,7 +99,7 @@ "backup_method_custom_finished": "Lo metòde de salvagarda personalizat « {method:s} » es acabat", "backup_nothings_done": "I a pas res de salvagardar", "backup_unable_to_organize_files": "Impossible d’organizar los fichièrs dins l’archiu amb lo metòde rapid", - "service_status_failed": "Impossible dedeterminar l’estat del servici « {service:s} »", + "service_status_failed": "Impossible de determinar l’estat del servici « {service:s} »", "service_stopped": "Lo servici « {service:s} » es estat arrestat", "service_unknown": "Servici « {service:s} » desconegut", "unbackup_app": "L’aplicacion « {app:s} » serà pas salvagardada", @@ -125,9 +125,9 @@ "certmanager_cannot_read_cert": "Quicòm a trucat en ensajar de dobrir lo certificat actual pel domeni {domain:s} (fichièr : {file:s}), rason : {reason:s}", "certmanager_cert_install_success": "Installacion capitada del certificat Let’s Encrypt pel domeni {domain:s} !", "certmanager_cert_install_success_selfsigned": "Installacion capitada del certificat auto-signat pel domeni {domain:s} !", - "certmanager_cert_signing_failed": "Fracàs de la signatura del noù certificat", + "certmanager_cert_signing_failed": "Fracàs de la signatura del nòu certificat", "certmanager_domain_cert_not_selfsigned": "Lo certificat del domeni {domain:s} es pas auto-signat. Volètz vertadièrament lo remplaçar ? (Utiliatz --force)", - "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz ço que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", + "certmanager_domain_dns_ip_differs_from_public_ip": "L’enregistrament DNS « A » del domeni {domain:s} es diferent de l’adreça IP d’aqueste servidor. Se fa pauc qu’avètz modificat l’enregistrament « A », mercés d’esperar l’espandiment (qualques verificadors d’espandiment son disponibles en linha). (Se sabètz çò que fasèm, utilizatz --no-checks per desactivar aqueles contraròtles)", "certmanager_domain_http_not_working": "Sembla que lo domeni {domain:s} es pas accessible via HTTP. Mercés de verificar que las configuracions DNS e nginx son corrèctas", "certmanager_domain_unknown": "Domeni desconegut {domain:s}", "certmanager_no_cert_file": "Lectura impossibla del fichièr del certificat pel domeni {domain:s} (fichièr : {file:s})", @@ -175,7 +175,7 @@ "global_settings_key_doesnt_exists": "La clau « {settings_key:s} » existís pas dins las configuracions globalas, podètz veire totas las claus disponiblas en picant « yunohost settings list »", "global_settings_reset_success": "Capitada ! Vòstra configuracion precedenta es estada salvagarda dins {path:s}", "global_settings_setting_example_bool": "Exemple d’opcion booleana", - "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartat e salvagardat dins /etc/yunohost/unkown_settings.json", + "global_settings_unknown_setting_from_settings_file": "Clau desconeguda dins los paramètres : {setting_key:s}, apartada e salvagardada dins /etc/yunohost/unkown_settings.json", "installation_failed": "Fracàs de l’installacion", "invalid_url_format": "Format d’URL pas valid", "ldap_initialized": "L’annuari LDAP es inicializat", @@ -207,7 +207,7 @@ "monitor_stats_no_update": "Cap de donadas d’estat del servidor d’actualizar", "mountpoint_unknown": "Ponch de montatge desconegut", "mysql_db_creation_failed": "Creacion de la basa de donadas MySQL impossibla", - "no_appslist_found": "Cap de lista d’aplicacions pas trobaba", + "no_appslist_found": "Cap de lista d’aplicacions pas trobada", "no_internet_connection": "Lo servidor es pas connectat a Internet", "package_not_installed": "Lo paquet « {pkgname} » es pas installat", "package_unknown": "Paquet « {pkgname} » desconegut", @@ -236,7 +236,7 @@ "certmanager_certificate_fetching_or_enabling_failed": "Sembla d’aver fracassat l’activacion d’un nòu certificat per {domain:s}…", "certmanager_conflicting_nginx_file": "Impossible de preparar lo domeni pel desfís ACME : lo fichièr de configuracion nginx {filepath:s} es en conflicte e deu èsser levat d’en primièr", "certmanager_couldnt_fetch_intermediate_cert": "Expiracion del relambi pendent l’ensag de recuperacion del certificat intermediari dins de Let’s Encrypt. L’installacion / lo renovèlament es estat interromput - tornatz ensajar mai tard.", - "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasetz, utilizatz --no-checks per desactivar las verificacions.)", + "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat.", @@ -268,7 +268,7 @@ "migration_0003_system_not_fully_up_to_date": "Lo sistèma es pas complètament a jorn. Mercés de lançar una mesa a jorn classica abans de començar la migracion per Stretch.", "migration_0003_modified_files": "Mercés de notar que los fichièrs seguents son estats detectats coma modificats manualament e poiràn èsser escafats a la fin de la mesa a nivèl : {manually_modified_files}", "monitor_period_invalid": "Lo periòde de temps es incorrècte", - "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbabe", + "monitor_stats_file_not_found": "Lo fichièr d’estatisticas es introbable", "monitor_stats_period_unavailable": "Cap d’estatisticas son pas disponiblas pel periòde", "mysql_db_init_failed": "Impossible d’inicializar la basa de donadas MySQL", "service_disable_failed": "Impossible de desactivar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", @@ -278,7 +278,7 @@ "service_no_log": "Cap de jornal de far veire pel servici « {service:s} »", "service_regenconf_dry_pending_applying": "Verificacion de las configuracions en espèra que poirián èsser aplicadas pel servici « {service} »…", "service_regenconf_failed": "Regeneracion impossibla de la configuracion pels servicis : {services}", - "service_regenconf_pending_applying": "Aplicacion de as configuraion en espèra pel servici « {service} »…", + "service_regenconf_pending_applying": "Aplicacion de las configuracions en espèra pel servici « {service} »…", "service_remove_failed": "Impossible de levar lo servici « {service:s} »", "service_removed": "Lo servici « {service:s} » es estat levat", "service_start_failed": "Impossible d’aviar lo servici « {service:s} »↵\n↵\nJornals recents : {logs:s}", @@ -311,7 +311,7 @@ "service_conf_new_managed_file": "Lo servici {service} gerís ara lo fichièr de configuracion « {conf} ».", "service_conf_up_to_date": "La configuracion del servici « {service} » es ja actualizada", "service_conf_would_be_updated": "La configuracion del servici « {service} » seriá estada actualizada", - "service_description_avahi-daemon": "permet d’aténher vòstre servidr via yunohost.local sus vòstre ret local", + "service_description_avahi-daemon": "permet d’aténher vòstre servidor via yunohost.local sus vòstre ret local", "service_description_dnsmasq": "gerís la resolucion dels noms de domeni (DNS)", "updating_apt_cache": "Actualizacion de la lista dels paquets disponibles...", "service_conf_file_backed_up": "Lo fichièr de configuracion « {conf} » es salvagardat dins « {backup} »", @@ -326,7 +326,7 @@ "restore_confirm_yunohost_installed": "Volètz vertadièrament restaurar un sistèma ja installat ? {answers:s}", "restore_extracting": "Extraccions dels fichièrs necessaris dins de l’archiu…", "restore_failed": "Impossible de restaurar lo sistèma", - "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tampauc dins l’archiu", + "restore_hook_unavailable": "Lo script de restauracion « {part:s} » es pas disponible sus vòstre sistèma e es pas tanpauc dins l’archiu", "restore_may_be_not_enough_disk_space": "Lo sistèma sembla d’aver pas pro d’espaci disponible (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", "restore_mounting_archive": "Montatge de l’archiu dins « {path:s} »", "restore_not_enough_disk_space": "Espaci disponible insufisent (liure : {free_space:d} octets, necessari : {needed_space:d} octets, marge de seguretat : {margin:d} octets)", @@ -355,7 +355,7 @@ "service_description_ssh": "vos permet de vos connectar a distància a vòstre servidor via un teminal (protocòl SSH)", "service_description_yunohost-api": "permet las interaccions entre l’interfàcia web de YunoHost e le sistèma", "service_description_yunohost-firewall": "gerís los pòrts de connexion dobèrts e tampats als servicis", - "ssowat_persistent_conf_read_error": "Error en ligir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", + "ssowat_persistent_conf_read_error": "Error en legir la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", "ssowat_persistent_conf_write_error": "Error en salvagardar la configuracion duradissa de SSOwat : {error:s}. Modificatz lo fichièr /etc/ssowat/conf.json.persistent per reparar la sintaxi JSON", "certmanager_old_letsencrypt_app_detected": "\nYunohost a detectat que l’aplicacion ’letsencrypt’ es installada, aquò es en conflicte amb las novèlas foncionalitats integradas de gestion dels certificats de Yunohost. Se volètz utilizar aquelas foncionalitats integradas, mercés de lançar las comandas seguentas per migrar vòstra installacion :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : aquò provarà de tornar installar los certificats de totes los domenis amb un certificat Let’s Encrypt o las auto-signats", "diagnosis_monitor_disk_error": "Impossible de supervisar los disques : {error}", @@ -378,7 +378,7 @@ "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", "migrations_skip_migration": "Passatge de la migracion {number} {name}…", - "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisias > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", + "migrations_to_be_ran_manually": "La migracion {number} {name} deu èsser lançada manualament. Mercés d’anar a Aisinas > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations migrate ».", "migrations_need_to_accept_disclaimer": "Per lançar la migracion {number} {name} , avètz d’acceptar aquesta clausa de non-responsabilitat :\n---\n{disclaimer}\n---\nS’acceptatz de lançar la migracion, mercés de tornar executar la comanda amb l’opcion accept-disclaimer.", "monitor_disabled": "La supervision del servidor es desactivada", "monitor_enabled": "La supervision del servidor es activada", From e59f1427d2790c41183fa144db3721aad3116598 Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 30 May 2018 17:48:04 +0000 Subject: [PATCH 0966/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index f5f9f911d..979afcd13 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -239,14 +239,14 @@ "certmanager_domain_not_resolved_locally": "Lo domeni {domain:s} pòt pas èsser determinat dins de vòstre servidor YunoHost. Pòt arribar s’avètz recentament modificat vòstre enregistrament DNS. Dins aqueste cas, mercés d’esperar unas oras per l’espandiment. Se lo problèma dura, consideratz ajustar {domain:s} a /etc/hosts. (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_error_no_A_record": "Cap d’enregistrament DNS « A » pas trobat per {domain:s}. Vos cal indicar que lo nom de domeni mene a vòstra maquina per poder installar un certificat Let’S Encrypt ! (Se sabètz çò que fasètz, utilizatz --no-checks per desactivar las verificacions.)", "certmanager_hit_rate_limit": "Tròp de certificats son ja estats demandats recentament per aqueste ensem de domeni {domain:s}. Mercés de tornar ensajar mai tard. Legissètz https://letsencrypt.org/docs/rate-limits/ per mai detalhs", - "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafòc/router amont de vòstre servidor es mal configurat.", + "certmanager_http_check_timeout": "Expiracion del relambi d’ensag del servidor de se contactar via HTTP amb son adreça IP publica {domain:s} amb l’adreça {ip:s}. Coneissètz benlèu de problèmas d’hairpinning o lo parafuòc/router amont de vòstre servidor es mal configurat.", "domain_dns_conf_is_just_a_recommendation": "Aqueste pagina mòstra la configuracion *recomandada*. Non configura *pas* lo DNS per vos. Sètz responsable de la configuracion de vòstra zòna DNS en çò de vòstre registrar DNS amb aquesta recomandacion.", "domain_dyndns_already_subscribed": "Avètz ja soscrich a un domeni DynDNS", "domain_dyndns_dynette_is_unreachable": "Impossible de contactar la dynette YunoHost, siá YunoHost pas es pas corrèctament connectat a Internet, siá lo servidor de la dynett es arrestat. Error : {error}", "domain_uninstall_app_first": "Una o mantuna aplicacions son installadas sus aqueste domeni. Mercés de las desinstallar d’en primièr abans de suprimir aqueste domeni", - "firewall_reload_failed": "Impossible de recargar lo parafòc", - "firewall_reloaded": "Lo parafòc es estat recargat", - "firewall_rules_cmd_failed": "Unas règlas del parafòc an fracassat. Per mai informacions, consultatz lo jornal.", + "firewall_reload_failed": "Impossible de recargar lo parafuòc", + "firewall_reloaded": "Lo parafuòc es estat recargat", + "firewall_rules_cmd_failed": "Unas règlas del parafuòc an fracassat. Per mai informacions, consultatz lo jornal.", "global_settings_bad_choice_for_enum": "La valor del paramètre {setting:s} es incorrècta. Recebut : {received_type:s}, esperat {expected_type:s}", "global_settings_bad_type_for_setting": "Lo tipe del paramètre {setting:s} es incorrècte. Recebut : {received_type:s}, esperat {expected_type:s}", "global_settings_cant_write_settings": "Fracàs de l’escritura del fichièr de configuracion, rason : {reason:s}", From 5f4a38818365df20847034f377655de757c2d113 Mon Sep 17 00:00:00 2001 From: htmk Date: Wed, 30 May 2018 06:25:44 +0000 Subject: [PATCH 0967/1066] [i18n] Translated using Weblate (Portuguese) Currently translated at 35.3% (143 of 404 strings) --- locales/pt.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index e024b6787..e1db1c618 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -155,7 +155,7 @@ "backup_archive_hook_not_exec": "O gancho '{hook:s}' não foi executado neste backup", "backup_archive_name_exists": "O nome do arquivo de backup já existe", "backup_archive_open_failed": "Não é possível abrir o arquivo de backup", - "backup_cleaning_failed": "Não é possível limpar a pasta de backup temporário", + "backup_cleaning_failed": "Não é possível limpar a pasta temporária de backups", "backup_creation_failed": "A criação do backup falhou", "backup_delete_error": "Impossível apagar '{path:s}'", "backup_deleted": "O backup foi suprimido", @@ -191,5 +191,6 @@ "backup_archive_system_part_not_available": "A seção do sistema '{part:s}' está indisponivel neste backup", "backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?", "backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.", - "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido" + "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido", + "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo" } From 577cd2c85834227c8de75bf0ea6c81321c65c89a Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Wed, 30 May 2018 20:17:29 +0000 Subject: [PATCH 0968/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 91.8% (371 of 404 strings) --- locales/ar.json | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index d2bc735f2..2ec29d7e7 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -16,7 +16,7 @@ "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", - "app_incompatible": "The app {app} is incompatible with your YunoHost version", + "app_incompatible": "إن التطبيق {app} غير متوافق مع إصدار واي يونوهوست YunoHost الخاص بك", "app_install_files_invalid": "Invalid installation files", "app_location_already_used": "The app '{app}' is already installed on that location ({path})", "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", @@ -26,7 +26,7 @@ "app_no_upgrade": "البرمجيات لا تحتاج إلى تحديث", "app_not_correctly_installed": "يبدو أن التطبيق {app:s} لم يتم تنصيبه بشكل صحيح", "app_not_installed": "إنّ التطبيق {app:s} غير مُنصَّب", - "app_not_properly_removed": "{app:s} has not been properly removed", + "app_not_properly_removed": "لم يتم حذف تطبيق {app:s} بشكلٍ جيّد", "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", "app_removed": "تمت إزالة تطبيق {app:s}", "app_requirements_checking": "Checking required packages for {app}...", @@ -129,7 +129,7 @@ "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", "certmanager_no_cert_file": "تعذرت عملية قراءة شهادة نطاق {domain:s} (الملف : {file:s})", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", + "certmanager_old_letsencrypt_app_detected": "", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", @@ -324,7 +324,7 @@ "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}", "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", "service_remove_failed": "Unable to remove service '{service:s}'", - "service_removed": "The service '{service:s}' has been removed", + "service_removed": "تمت إزالة خدمة '{service:s}'", "service_start_failed": "", "service_started": "The service '{service:s}' has been started", "service_status_failed": "Unable to determine status of service '{service:s}'", @@ -364,10 +364,21 @@ "yunohost_ca_creation_success": "تم إنشاء هيئة الشهادات المحلية.", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "عملية تنصيب يونوهوست جارية …", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'", + "yunohost_not_installed": "إنَّ واي يونوهوست ليس مُنَصَّب أو هو مثبت حاليا بشكل خاطئ. قم بتنفيذ الأمر 'yunohost tools postinstall'", "migration_description_0003_migrate_to_stretch": "تحديث النظام إلى ديبيان ستريتش و واي يونوهوست 3.0", "migration_0003_patching_sources_list": "عملية تعديل ملف المصادر sources.lists جارية ...", "migration_0003_main_upgrade": "بداية عملية التحديث الأساسية ...", "migration_0003_fail2ban_upgrade": "بداية عملية تحديث fail2ban ...", - "migration_0003_not_jessie": "إن توزيعة ديبيان الحالية تختلف عن جيسي !" + "migration_0003_not_jessie": "إن توزيعة ديبيان الحالية تختلف عن جيسي !", + "migration_description_0002_migrate_to_tsig_sha256": "يقوم بتحسين أمان TSIG لنظام أسماء النطاقات الديناميكة باستخدام SHA512 بدلًا مِن MD5", + "migration_0003_backward_impossible": "لا يُمكن إلغاء عملية الإنتقال إلى ستريتش.", + "migration_0003_system_not_fully_up_to_date": "إنّ نظامك غير مُحدَّث بعدُ لذا يرجى القيام بتحديث عادي أولا قبل إطلاق إجراء الإنتقال إلى نظام ستريتش.", + "migrations_list_conflict_pending_done": "لا يمكنك استخدام --previous و --done معًا على نفس سطر الأوامر.", + "service_description_avahi-daemon": "يسمح لك بالنفاذ إلى خادومك عبر الشبكة المحلية باستخدام yunohost.local", + "service_description_glances": "يقوم بمراقبة معلومات النظام على خادومك", + "service_description_metronome": "يُدير حسابات الدردشة الفورية XMPP", + "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", + "service_description_php5-fpm": "يقوم بتشغيل تطبيقات الـ PHP مع خادوم الويب nginx", + "service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية", + "service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام" } From ff9e9a10789572047eff32a052c74a20886ee9c4 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 10 Jun 2018 01:28:14 +0000 Subject: [PATCH 0969/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 91.8% (371 of 404 strings) --- locales/ar.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 2ec29d7e7..c6f576536 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -29,7 +29,7 @@ "app_not_properly_removed": "لم يتم حذف تطبيق {app:s} بشكلٍ جيّد", "app_package_need_update": "The app {app} package needs to be updated to follow YunoHost changes", "app_removed": "تمت إزالة تطبيق {app:s}", - "app_requirements_checking": "Checking required packages for {app}...", + "app_requirements_checking": "جار فحص الحزم اللازمة لـ {app} ...", "app_requirements_failed": "Unable to meet requirements for {app}: {error}", "app_requirements_unmeet": "Requirements are not met for {app}, the package {pkgname} ({version}) must be {spec}", "app_sources_fetch_failed": "تعذرت عملية جلب مصادر الملفات", From 1c7b5a7ac977b0e304f20b9f77d1cd623bb2a866 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 12 Jun 2018 21:16:54 +0000 Subject: [PATCH 0970/1066] [i18n] Translated using Weblate (French) Currently translated at 100.0% (404 of 404 strings) --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 3939503f9..ad04bc46f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -393,7 +393,7 @@ "migration_0003_not_jessie": "La distribution Debian actuelle n’est pas Jessie !", "migration_0003_system_not_fully_up_to_date": "Votre système n’est pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer à migration à Stretch.", "migration_0003_still_on_jessie_after_main_upgrade": "Quelque chose s’est ma passé pendant la mise à niveau principale : le système est toujours sur Jessie ?!? Pour investiguer le problème, veuillez regarder {log} 🙁…", - "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu'à quelques heures pour que tout soit à niveau.", + "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus d’informations sur https://yunohost.org/backup ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu'à quelques heures pour que tout soit à niveau.\n\nDe plus, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). L’ancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !", "migration_0003_problematic_apps_warning": "Veuillez noter que les applications suivantes, éventuellement problématiques, ont été détectées. Il semble qu’elles n’aient pas été installées depuis une liste d’application ou qu’elles ne soit pas marquées «working ». En conséquence, nous ne pouvons pas garantir qu’elles fonctionneront après la mise à niveau : {problematic_apps}", "migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", From 8c6e91ceed9c64eedc5579e45ca2e09053a545be Mon Sep 17 00:00:00 2001 From: Quenti Date: Tue, 12 Jun 2018 19:50:12 +0000 Subject: [PATCH 0971/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 979afcd13..c68c51b4b 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -373,7 +373,7 @@ "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.", "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", - "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.", + "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas. Mai d’informacions a https://yunohost.org/backup ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.\n\nEn mai, lo pòrt per SMTP, utilizat pels clients de corrièls extèrns coma (Thunderbird o K9-Mail) foguèt cambiat de 465 (SSL/TLS) per 587 (STARTTLS). L’ancian pòrt 465 serà automaticament tampat e lo nòu pòrt 587 serà dobèrt dins lo parafuòc. Vosautres e vòstres utilizaires *auretz* d’adaptar la configuracion de vòstre client de corrièl segon aqueles cambiaments !", "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", From 4e135b6546665234bd1d2c638eeada0ce765f605 Mon Sep 17 00:00:00 2001 From: Passaire Date: Wed, 13 Jun 2018 17:44:27 +0000 Subject: [PATCH 0972/1066] [i18n] Translated using Weblate (Occitan) Currently translated at 100.0% (404 of 404 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index c68c51b4b..103c0d3e6 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -373,7 +373,7 @@ "migration_description_0001_change_cert_group_to_sslcert": "Càmbia las permissions de grop dels certificats de « metronome » per « ssl-cert »", "migration_0003_restoring_origin_nginx_conf": "Vòstre fichièr /etc/nginx/nginx.conf es estat modificat manualament. La migracion reïnicializarà d’en primièr son estat origina… Lo fichièr precedent serà disponible coma {backup_dest}.", "migration_0003_still_on_jessie_after_main_upgrade": "Quicòm a trucat pendent la mesa a nivèl màger : lo sistèma es encara jos Jessie ?!? Per trobar lo problèma, agachatz {log} …", - "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas. Mai d’informacions a https://yunohost.org/backup ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.\n\nEn mai, lo pòrt per SMTP, utilizat pels clients de corrièls extèrns coma (Thunderbird o K9-Mail) foguèt cambiat de 465 (SSL/TLS) per 587 (STARTTLS). L’ancian pòrt 465 serà automaticament tampat e lo nòu pòrt 587 serà dobèrt dins lo parafuòc. Vosautres e vòstres utilizaires *auretz* d’adaptar la configuracion de vòstre client de corrièl segon aqueles cambiaments !", + "migration_0003_general_warning": "Notatz qu’aquesta migracion es una operacion delicata. Encara que la còla YunoHost aguèsse fach çò melhor per la tornar legir e provar, la migracion poiriá copar de parts del sistèma o de las aplicacions.\n\nEn consequéncia, vos recomandam :\n· · · · - de lançar una salvagarda de vòstras donadas o aplicacions criticas. Mai d’informacions a https://yunohost.org/backup ;\n· · · · - d’èsser pacient aprèp aver lançat la migracion : segon vòstra connexion Internet e material, pòt trigar qualques oras per que tot siá mes al nivèl.\n\nEn mai, lo pòrt per SMTP, utilizat pels clients de corrièls extèrns (coma Thunderbird o K9-Mail per exemple) foguèt cambiat de 465 (SSL/TLS) per 587 (STARTTLS). L’ancian pòrt 465 serà automaticament tampat e lo nòu pòrt 587 serà dobèrt dins lo parafuòc. Vosautres e vòstres utilizaires *auretz* d’adaptar la configuracion de vòstre client de corrièl segon aqueles cambiaments !", "migration_0003_problematic_apps_warning": "Notatz que las aplicacions seguentas, saique problematicas, son estadas desactivadas. Semblan d’aver estadas installadas d’una lista d’aplicacions o que son pas marcadas coma «working ». En consequéncia, podèm pas assegurar que tendràn de foncionar aprèp la mesa a nivèl : {problematic_apps}", "migrations_bad_value_for_target": "Nombre invalid pel paramètre « target », los numèros de migracion son 0 o {}", "migrations_migration_has_failed": "La migracion {number} {name} a pas capitat amb l’excepcion {exception}, anullacion", From ac5183665a88ce30b007b8c106b2f925bbdd635c Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 14 Jun 2018 16:05:46 +0000 Subject: [PATCH 0973/1066] [i18n] Translated using Weblate (Arabic) Currently translated at 91.8% (371 of 404 strings) --- locales/ar.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index c6f576536..cda9c2c8b 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -14,10 +14,10 @@ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", - "app_extraction_failed": "Unable to extract installation files", + "app_extraction_failed": "تعذر فك الضغط عن ملفات التنصيب", "app_id_invalid": "Invalid app id", "app_incompatible": "إن التطبيق {app} غير متوافق مع إصدار واي يونوهوست YunoHost الخاص بك", - "app_install_files_invalid": "Invalid installation files", + "app_install_files_invalid": "ملفات التنصيب خاطئة", "app_location_already_used": "The app '{app}' is already installed on that location ({path})", "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", @@ -41,7 +41,7 @@ "app_upgraded": "تم تحديث التطبيق {app:s}", "appslist_corrupted_json": "Could not load the application lists. It looks like {filename:s} is corrupted.", "appslist_could_not_migrate": "Could not migrate app list {appslist:s} ! Unable to parse the url... The old cron job has been kept in {bkp_file:s}.", - "appslist_fetched": "The application list {appslist:s} has been fetched", + "appslist_fetched": "تم جلب قائمة تطبيقات {appslist:s}", "appslist_migrating": "Migrating application list {appslist:s} ...", "appslist_name_already_tracked": "There is already a registered application list with name {name:s}.", "appslist_removed": "تم حذف قائمة البرمجيات {appslist:s}", @@ -91,16 +91,16 @@ "backup_deleted": "The backup has been deleted", "backup_extracting_archive": "Extracting the backup archive...", "backup_hook_unknown": "Backup hook '{hook:s}' unknown", - "backup_invalid_archive": "Invalid backup archive", + "backup_invalid_archive": "نسخة إحتياطية غير صالحة", "backup_method_borg_finished": "Backup into borg finished", - "backup_method_copy_finished": "Backup copy finished", + "backup_method_copy_finished": "إنتهت عملية النسخ الإحتياطي", "backup_method_custom_finished": "Custom backup method '{method:s}' finished", "backup_method_tar_finished": "Backup tar archive created", "backup_no_uncompress_archive_dir": "Uncompress archive directory doesn't exist", - "backup_nothings_done": "There is nothing to save", + "backup_nothings_done": "ليس هناك أي شيء للحفظ", "backup_output_directory_forbidden": "Forbidden output directory. Backups can't be created in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var or /home/yunohost.backup/archives sub-folders", "backup_output_directory_not_empty": "The output directory is not empty", - "backup_output_directory_required": "You must provide an output directory for the backup", + "backup_output_directory_required": "يتوجب عليك تحديد مجلد لتلقي النسخ الإحتياطية", "backup_output_symlink_dir_broken": "You have a broken symlink instead of your archives directory '{path:s}'. You may have a specific setup to backup your data on an other filesystem, in this case you probably forgot to remount or plug your hard dirve or usb key.", "backup_running_app_script": "Running backup script of app '{app:s}'...", "backup_running_hooks": "Running backup hooks...", @@ -113,7 +113,7 @@ "certmanager_attempt_to_renew_valid_cert": "The certificate for domain {domain:s} is not about to expire! Use --force to bypass", "certmanager_attempt_to_replace_valid_cert": "You are attempting to overwrite a good and valid certificate for domain {domain:s}! (Use --force to bypass)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file: {file:s}), reason: {reason:s}", - "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain:s}!", + "certmanager_cert_install_success": "تمت عملية تنصيب شهادة Let's Encrypt بنجاح على النطاق {domain:s} !", "certmanager_cert_install_success_selfsigned": "Successfully installed a self-signed certificate for domain {domain:s}!", "certmanager_cert_renew_success": "نجحت عملية تجديد شهادة Let's Encrypt الخاصة باسم النطاق {domain:s} !", "certmanager_cert_signing_failed": "فشل إجراء توقيع الشهادة الجديدة", @@ -318,15 +318,15 @@ "service_disable_failed": "", "service_disabled": "The service '{service:s}' has been disabled", "service_enable_failed": "", - "service_enabled": "The service '{service:s}' has been enabled", - "service_no_log": "No log to display for service '{service:s}'", + "service_enabled": "تم تنشيط خدمة '{service:s}'", + "service_no_log": "ليس لخدمة '{service:s}' أي سِجلّ للعرض", "service_regenconf_dry_pending_applying": "Checking pending configuration which would have been applied for service '{service}'...", "service_regenconf_failed": "Unable to regenerate the configuration for service(s): {services}", "service_regenconf_pending_applying": "Applying pending configuration for service '{service}'...", "service_remove_failed": "Unable to remove service '{service:s}'", "service_removed": "تمت إزالة خدمة '{service:s}'", "service_start_failed": "", - "service_started": "The service '{service:s}' has been started", + "service_started": "تم إطلاق تشغيل خدمة '{service:s}'", "service_status_failed": "Unable to determine status of service '{service:s}'", "service_stop_failed": "", "service_stopped": "The service '{service:s}' has been stopped", From 5e77e6e1083d513adb74c2f74e8759a7b8ea59a4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 17 Jun 2018 01:39:22 +0200 Subject: [PATCH 0974/1066] [fix] local variables and various fix on psql helpers --- data/helpers.d/psql | 52 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/data/helpers.d/psql b/data/helpers.d/psql index ddacbef8c..2ef13482a 100644 --- a/data/helpers.d/psql +++ b/data/helpers.d/psql @@ -7,16 +7,15 @@ ynh_psql_test_if_first_run() { then echo "PostgreSQL is already installed, no need to create master password" else - pgsql=$(ynh_string_random) - pg_hba="" - echo "$pgsql" >> /etc/yunohost/psql + local pgsql="$(ynh_string_random)" + echo "$pgsql" > /etc/yunohost/psql if [ -e /etc/postgresql/9.4/ ] then - pg_hba=/etc/postgresql/9.4/main/pg_hba.conf + local pg_hba=/etc/postgresql/9.4/main/pg_hba.conf elif [ -e /etc/postgresql/9.6/ ] then - pg_hba=/etc/postgresql/9.6/main/pg_hba.conf + local pg_hba=/etc/postgresql/9.6/main/pg_hba.conf else ynh_die "postgresql shoud be 9.4 or 9.6" fi @@ -29,7 +28,7 @@ ynh_psql_test_if_first_run() { # Note: we can't use peer since YunoHost create users with nologin # See: https://github.com/YunoHost/yunohost/blob/unstable/data/helpers.d/user sed -i '/local\s*all\s*all\s*peer/i \ - local all all password' "$pg_hba" + local all all password' "$pg_hba" systemctl enable postgresql systemctl reload postgresql fi @@ -45,9 +44,9 @@ ynh_psql_test_if_first_run() { # | arg: pwd - the user password # | arg: db - the database to connect to ynh_psql_connect_as() { - user="$1" - pwd="$2" - db="$3" + local user="$1" + local pwd="$2" + local db="$3" sudo --login --user=postgres PGUSER="$user" PGPASSWORD="$pwd" psql "$db" } @@ -55,9 +54,8 @@ ynh_psql_connect_as() { # # usage: ynh_psql_execute_as_root sql [db] # | arg: sql - the SQL command to execute -# | arg: db - the database to connect to ynh_psql_execute_as_root () { - sql="$1" + local sql="$1" sudo --login --user=postgres psql <<< "$sql" } @@ -67,8 +65,8 @@ ynh_psql_execute_as_root () { # | arg: file - the file containing SQL commands # | arg: db - the database to connect to ynh_psql_execute_file_as_root() { - file="$1" - db="$2" + local file="$1" + local db="$2" sudo --login --user=postgres psql "$db" < "$file" } @@ -82,11 +80,11 @@ ynh_psql_execute_file_as_root() { # | arg: name - Name of the database # | arg: pwd - Password of the database. If not given, a password will be generated ynh_psql_setup_db () { - db_user="$1" - db_name="$2" - new_db_pwd=$(ynh_string_random) # Generate a random password + local db_user="$1" + local db_name="$2" + local new_db_pwd=$(ynh_string_random) # Generate a random password # If $3 is not given, use new_db_pwd instead for db_pwd. - db_pwd="${3:-$new_db_pwd}" + local db_pwd="${3:-$new_db_pwd}" ynh_psql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database ynh_app_setting_set "$app" psqlpwd "$db_pwd" # Store the password in the app's config } @@ -98,9 +96,9 @@ ynh_psql_setup_db () { # | arg: user - the user to grant privilegies # | arg: pwd - the user password ynh_psql_create_db() { - db="$1" - user="$2" - pwd="$3" + local db="$1" + local user="$2" + local pwd="$3" ynh_psql_create_user "$user" "$pwd" sudo --login --user=postgres createdb --owner="$user" "$db" } @@ -111,8 +109,8 @@ ynh_psql_create_db() { # | arg: db - the database name to drop # | arg: user - the user to drop ynh_psql_remove_db() { - db="$1" - user="$2" + local db="$1" + local user="$2" sudo --login --user=postgres dropdb "$db" ynh_psql_drop_user "$user" } @@ -125,7 +123,7 @@ ynh_psql_remove_db() { # | arg: db - the database name to dump # | ret: the psqldump output ynh_psql_dump_db() { - db="$1" + local db="$1" sudo --login --user=postgres pg_dump "$db" } @@ -135,9 +133,9 @@ ynh_psql_dump_db() { # usage: ynh_psql_create_user user pwd [host] # | arg: user - the user name to create ynh_psql_create_user() { - user="$1" - pwd="$2" - sudo --login --user=postgres psql -c"CREATE USER $user WITH PASSWORD '$pwd'" postgres + local user="$1" + local pwd="$2" + sudo --login --user=postgres psql -c"CREATE USER $user WITH PASSWORD '$pwd'" postgres } # Drop a user @@ -145,6 +143,6 @@ ynh_psql_create_user() { # usage: ynh_psql_drop_user user # | arg: user - the user name to drop ynh_psql_drop_user() { - user="$1" + local user="$1" sudo --login --user=postgres dropuser "$user" } From e240fe8659180a3c4477295c5fa4dd94e6ce6cd5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 17 Jun 2018 01:17:26 +0000 Subject: [PATCH 0975/1066] Update changelog for 2.7.14 release --- debian/changelog | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 2e480e1bb..c9f5408dd 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (2.7.14) stable; urgency=low + + * Last minute fix : install php7.0-acpu to hopefully make stretch still work after the upgrade + * Improve Occitan, French, Portuguese, Arabic translations + * [fix] local variables and various fix on psql helpers + + -- Alexandre Aubin Sun, 17 Jun 2018 01:16:13 +0000 yunohost (2.7.13.6) testing; urgency=low @@ -5,7 +12,7 @@ yunohost (2.7.13.6) testing; urgency=low * [stretch-migration] Disable predictable network interface names Fixes by Bram and Aleks - + -- Alexandre Aubin Fri, 15 Jun 2018 16:20:00 +0000 yunohost (2.7.13.5) testing; urgency=low From b3b3062d62642dd6c00aa44484401e94ac9e6128 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 17 Jun 2018 01:26:50 +0000 Subject: [PATCH 0976/1066] Update changelog --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index bca1f40f8..849a7c312 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (3.0.0) stable; urgency=low + + * Merge with jessie's branches + * Release as stable + + -- Alexandre Aubin Sun, 17 Jun 2018 03:25:00 +0000 + yunohost (3.0.0~beta1.7) testing; urgency=low * Merge with jessie's branches From e55cbb401fe840b0611fb0283cbe30d2121afdc4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 18 Jun 2018 18:28:02 +0200 Subject: [PATCH 0977/1066] [fix] [microdecision] Remove --verbose and --ignore-system in backup/restore during app upgrades --- data/helpers.d/utils | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 33d6bffd4..07b4d4bb1 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -48,7 +48,7 @@ ynh_restore_upgradebackup () { # Remove the application then restore it sudo yunohost app remove $app # Restore the backup - sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force --verbose + sudo yunohost backup restore $app_bck-pre-upgrade$backup_number --apps $app --force ynh_die "The app was restored to the way it was before the failed upgrade." fi else @@ -87,7 +87,7 @@ ynh_backup_before_upgrade () { fi # Create backup - sudo BACKUP_CORE_ONLY=1 yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number --verbose + sudo BACKUP_CORE_ONLY=1 yunohost backup create --apps $app --name $app_bck-pre-upgrade$backup_number if [ "$?" -eq 0 ] then # If the backup succeeded, remove the previous backup From 933c56bb7ec3d0f9a5fd54b1f360ff4ab26dfbaa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 18 Jun 2018 18:32:00 +0200 Subject: [PATCH 0978/1066] Upgrade changelog --- debian/changelog | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/debian/changelog b/debian/changelog index 849a7c312..a47b0b42d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +yunohost (3.0.0.1) stable; urgency=low + + * Fix remaining use of --verbose and --ignore-system during backup/restore + of app upgrades + + -- Alexandre Aubin Mon, 18 Jun 2018 18:31:00 +0000 + yunohost (3.0.0) stable; urgency=low * Merge with jessie's branches From ff565355689db47159241521f7d750f8cb309e58 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 21 Jun 2018 12:48:52 +0200 Subject: [PATCH 0979/1066] [fix] Mail permission issue after restore --- data/hooks/restore/23-data_mail | 1 + 1 file changed, 1 insertion(+) diff --git a/data/hooks/restore/23-data_mail b/data/hooks/restore/23-data_mail index 995308273..81b9b923f 100644 --- a/data/hooks/restore/23-data_mail +++ b/data/hooks/restore/23-data_mail @@ -1,6 +1,7 @@ backup_dir="$1/data/mail" sudo cp -a $backup_dir/. /var/mail/ || echo 'No mail found' +sudo chown -R vmail:mail /var/mail/ # Restart services to use migrated certs sudo service postfix restart From 274a219fdcbf3b4ec86d888d5b79e549cc654162 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 8 Jun 2018 23:55:42 +0200 Subject: [PATCH 0980/1066] [enh] allow hook_exec to have custom callbacks --- src/yunohost/hook.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 32570ab57..4688b25b3 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -297,7 +297,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="admin"): + chdir=None, env=None, user="admin", stdout_callback=None, + stderr_callback=None): """ Execute hook from a file with arguments @@ -361,8 +362,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, # Define output callbacks and call command callbacks = ( - lambda l: logger.debug(l.rstrip()), - lambda l: logger.warning(l.rstrip()), + stdout_callback if stdout_callback else lambda l: logger.debug(l.rstrip()), + stderr_callback if stderr_callback else lambda l: logger.warning(l.rstrip()), ) returncode = call_async_output( command, callbacks, shell=False, cwd=chdir From c0ec14b79be267e8b4dd9419e21e5d3ae4f43fa4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 8 Jun 2018 23:56:24 +0200 Subject: [PATCH 0981/1066] [enh] first working prototype of config-panel show --- data/actionsmap/yunohost.yml | 14 +++++++ src/yunohost/app.py | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 7db57b0e6..eca8e87f7 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -771,6 +771,20 @@ app: apps: nargs: "+" + subcategories: + + config: + subcategory_help: Applications configuration panel + actions: + + ### app_config_show_panel() + show-panel: + action_help: show config panel for the application + api: GET //config/panel + arguments: + app_id: + help: App ID + ############################# # Backup # ############################# diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 72a53da1f..137a9dd3e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -40,6 +40,7 @@ from collections import OrderedDict from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_json from yunohost.service import service_log, _run_service_command from yunohost.utils import packages @@ -1360,6 +1361,85 @@ def app_change_label(auth, app, new_label): app_ssowatconf(auth) +def app_config_show_panel(app_id): + from yunohost.hook import hook_exec + + installed = _is_installed(app_id) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app_id)) + + config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') + + if not os.path.exists(config_panel) or not os.path.exists(config_script): + return { + "config_panel": [], + } + + config_panel = read_json(config_panel) + + env = {"YNH_APP_ID": app_id} + parsed_values = {} + + # I need to parse stdout to communicate between scripts because I can't + # read the child environment :( (that would simplify things so much) + # after hours of research this is apparently quite a standard way, another + # option would be to add an explicite pipe or a named pipe for that + # a third option would be to write in a temporary file but I don't like + # that because that could expose sensitive data + def parse_stdout(line): + line = line.rstrip() + logger.info(line) + + if line.strip().startswith("YNH_CONFIG_") and "=" in line: + # XXX error handling? + # XXX this might not work for multilines stuff :( (but echo without + # formatting should do it no?) + key, value = line.strip().split("=", 1) + logger.debug("config script declared: %s -> %s", key, value) + parsed_values[key] = value + print "in parse_stdout", parsed_values + print [line] + + hook_exec(config_script, + args=[], + env=env, + user="root", + stdout_callback=parse_stdout, + ) + + # logger.debug("Env after running config script %s", env) + + logger.debug("Generating global variables:") + for tab in config_panel.get("panel", []): + tab_id = tab["id"] # this makes things easier to debug on crash + for section in tab.get("sections", []): + section_id = section["id"] + for option in section.get("options", []): + option_id = option["id"] + variable_name = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper() + logger.debug(" * '%s'.'%s'.'%s' -> %s", tab.get("name"), section.get("name"), option.get("name"), variable_name) + + if variable_name in parsed_values: + # XXX we should probably uses the one of install here but it's at a POC state right now + option_type = option["type"] + if option_type == "bool": + assert parsed_values[variable_name].lower() in ("true", "false") + option["value"] = True if parsed_values[variable_name].lower() == "true" else False + elif option_type == "integer": + option["value"] = int(parsed_values[variable_name]) + elif option_type == "text": + option["value"] = parsed_values[variable_name] + else: + logger.debug("Variable '%s' is not declared by config script, using default", variable_name) + option["value"] = option["default"] + + return { + "config_panel": config_panel, + } + + def _get_app_settings(app_id): """ Get settings of an installed app From 1c16693cdcabbd48e9de5ee3f9335b08d10e9301 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 9 Jun 2018 23:18:47 +0200 Subject: [PATCH 0982/1066] [mod] explicitely tells script/config that I want the show behavior --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 137a9dd3e..696b79db8 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1403,7 +1403,7 @@ def app_config_show_panel(app_id): print [line] hook_exec(config_script, - args=[], + args=["show"], env=env, user="root", stdout_callback=parse_stdout, From 359ef6d3d73fdaa2adc60f19cfddc3a06e2a5b42 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 9 Jun 2018 23:21:09 +0200 Subject: [PATCH 0983/1066] [enh] handle failure on 'config/script show' --- src/yunohost/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 696b79db8..7154883eb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1402,14 +1402,15 @@ def app_config_show_panel(app_id): print "in parse_stdout", parsed_values print [line] - hook_exec(config_script, + return_code = hook_exec(config_script, args=["show"], env=env, user="root", stdout_callback=parse_stdout, ) - # logger.debug("Env after running config script %s", env) + if return_code != 0: + raise Exception("script/config show return value code: %s (considered as an error)", return_code) logger.debug("Generating global variables:") for tab in config_panel.get("panel", []): From 7d634a3085a42b0f50b4de83cb8feb4d978a2075 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 9 Jun 2018 23:21:22 +0200 Subject: [PATCH 0984/1066] [mod] s/variable_name/generated_id --- src/yunohost/app.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7154883eb..0cc966801 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1419,21 +1419,22 @@ def app_config_show_panel(app_id): section_id = section["id"] for option in section.get("options", []): option_id = option["id"] - variable_name = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper() - logger.debug(" * '%s'.'%s'.'%s' -> %s", tab.get("name"), section.get("name"), option.get("name"), variable_name) + generated_id = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper() + option["id"] = generated_id + logger.debug(" * '%s'.'%s'.'%s' -> %s", tab.get("name"), section.get("name"), option.get("name"), generated_id) - if variable_name in parsed_values: + if generated_id in parsed_values: # XXX we should probably uses the one of install here but it's at a POC state right now option_type = option["type"] if option_type == "bool": - assert parsed_values[variable_name].lower() in ("true", "false") - option["value"] = True if parsed_values[variable_name].lower() == "true" else False + assert parsed_values[generated_id].lower() in ("true", "false") + option["value"] = True if parsed_values[generated_id].lower() == "true" else False elif option_type == "integer": - option["value"] = int(parsed_values[variable_name]) + option["value"] = int(parsed_values[generated_id]) elif option_type == "text": - option["value"] = parsed_values[variable_name] + option["value"] = parsed_values[generated_id] else: - logger.debug("Variable '%s' is not declared by config script, using default", variable_name) + logger.debug("Variable '%s' is not declared by config script, using default", generated_id) option["value"] = option["default"] return { From ff3913adbb7d2b05c11a1896c5aeb06839ab96da Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 9 Jun 2018 23:21:39 +0200 Subject: [PATCH 0985/1066] [enh] first version of script/config apply --- data/actionsmap/yunohost.yml | 11 ++++++++ src/yunohost/app.py | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index eca8e87f7..03f9a214c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -785,6 +785,17 @@ app: app_id: help: App ID + ### app_config_apply() + apply: + action_help: apply the new configuration + api: POST //config + arguments: + app_id: + help: App ID + -a: + full: --args + help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") + ############################# # Backup # ############################# diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0cc966801..65438dadf 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1442,6 +1442,57 @@ def app_config_show_panel(app_id): } +def app_config_apply(app_id, args): + from yunohost.hook import hook_exec + + installed = _is_installed(app_id) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app_id)) + + config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') + + if not os.path.exists(config_panel) or not os.path.exists(config_script): + # XXX real exception + raise Exception("Not config-panel.json nor scripts/config") + + config_panel = read_json(config_panel) + + env = {} + args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) + + for tab in config_panel.get("panel", []): + tab_id = tab["id"] # this makes things easier to debug on crash + for section in tab.get("sections", []): + section_id = section["id"] + for option in section.get("options", []): + option_id = option["id"] + generated_id = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_id)).upper() + + if generated_id in args: + logger.debug("include into env %s=%s", generated_id, args[generated_id]) + env[generated_id] = args[generated_id] + else: + logger.debug("no value for key id %s", generated_id) + + # for debug purpose + for key in args: + if key not in env: + logger.warning("Ignore key '%s' from arguments because it is not in the config", key) + + return_code = hook_exec(config_script, + args=["apply"], + env=env, + user="root", + ) + + if return_code != 0: + raise Exception("'script/config apply' return value code: %s (considered as an error)", return_code) + + logger.success("Config updated as expected") + + def _get_app_settings(app_id): """ Get settings of an installed app From ffbe059d9138bf89e8e5bf2516c77215a42f7494 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 10 Jun 2018 01:01:52 +0200 Subject: [PATCH 0986/1066] [mod] remove debug output --- src/yunohost/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 65438dadf..ca98d7a14 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1399,8 +1399,6 @@ def app_config_show_panel(app_id): key, value = line.strip().split("=", 1) logger.debug("config script declared: %s -> %s", key, value) parsed_values[key] = value - print "in parse_stdout", parsed_values - print [line] return_code = hook_exec(config_script, args=["show"], From 27dfd6af6e4e504793093c9db6d09025e36303c6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 10 Jun 2018 20:35:27 +0200 Subject: [PATCH 0987/1066] [fix] missing YNH_APP_ID on scripts/config apply --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ca98d7a14..7cbc8922b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1457,7 +1457,7 @@ def app_config_apply(app_id, args): config_panel = read_json(config_panel) - env = {} + env = {"YNH_APP_ID": app_id} args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) for tab in config_panel.get("panel", []): From 67ba031eabe70e5ce67f4961959b6008a25ecd7b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 10 Jun 2018 20:39:20 +0200 Subject: [PATCH 0988/1066] [mod] copy config_panel.json on install/upgrade --- src/yunohost/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7cbc8922b..64806fbcf 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -641,6 +641,9 @@ def app_upgrade(auth, app=[], url=None, file=None): os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): + os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "conf")): os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) @@ -758,6 +761,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): + os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "conf")): os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) From 6a1b8fdfa360a6bf7edf0216be4a1bb4224b4f87 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 18 Jun 2018 04:02:23 +0200 Subject: [PATCH 0989/1066] [mod] better API paths for configpanel --- data/actionsmap/yunohost.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 03f9a214c..d276a6cef 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -780,7 +780,7 @@ app: ### app_config_show_panel() show-panel: action_help: show config panel for the application - api: GET //config/panel + api: GET /apps//config-panel arguments: app_id: help: App ID @@ -788,7 +788,7 @@ app: ### app_config_apply() apply: action_help: apply the new configuration - api: POST //config + api: POST /apps//config arguments: app_id: help: App ID From 6fb19edbb3b1dcbf037ad58a83475713a831cc4d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 18 Jun 2018 04:03:00 +0200 Subject: [PATCH 0990/1066] [enh] send app name and id to config-panel-show --- src/yunohost/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 64806fbcf..32ecff82a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1370,10 +1370,8 @@ def app_change_label(auth, app, new_label): def app_config_show_panel(app_id): from yunohost.hook import hook_exec - installed = _is_installed(app_id) - if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app_id)) + # this will take care of checking if the app is installed + app_info_dict = app_info(app_id) config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') @@ -1442,6 +1440,8 @@ def app_config_show_panel(app_id): option["value"] = option["default"] return { + "app_id": app_id, + "app_name": app_info_dict["name"], "config_panel": config_panel, } From 8a33199d916027d6596c65c0df1a2533fcbbd179 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 04:31:13 +0200 Subject: [PATCH 0991/1066] [mod] add experimental warning for config panel --- locales/en.json | 1 + src/yunohost/app.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/locales/en.json b/locales/en.json index dc7a5203a..19213c372 100644 --- a/locales/en.json +++ b/locales/en.json @@ -179,6 +179,7 @@ "executing_command": "Executing command '{command:s}'...", "executing_script": "Executing script '{script:s}'...", "extracting": "Extracting...", + "experimental_feature": "Warning: this feature is experimental and not consider stable, you shouldn't be using it except if you know what you are doing.", "field_invalid": "Invalid field '{:s}'", "firewall_reload_failed": "Unable to reload the firewall", "firewall_reloaded": "The firewall has been reloaded", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 32ecff82a..2a7b6bf74 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1367,7 +1367,12 @@ def app_change_label(auth, app, new_label): app_ssowatconf(auth) +# Config panel todo list: +# * docstrings +# * merge translations on the json once the workflow is in place def app_config_show_panel(app_id): + logger.warning(m18n.n('experimental_feature')) + from yunohost.hook import hook_exec # this will take care of checking if the app is installed @@ -1447,6 +1452,8 @@ def app_config_show_panel(app_id): def app_config_apply(app_id, args): + logger.warning(m18n.n('experimental_feature')) + from yunohost.hook import hook_exec installed = _is_installed(app_id) From c14077dc3656b7e9f42cf6eb579e1daa1e0c1766 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 04:31:51 +0200 Subject: [PATCH 0992/1066] [fix] handle empty args case --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2a7b6bf74..5f52a8628 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1471,7 +1471,7 @@ def app_config_apply(app_id, args): config_panel = read_json(config_panel) env = {"YNH_APP_ID": app_id} - args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) + args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} for tab in config_panel.get("panel", []): tab_id = tab["id"] # this makes things easier to debug on crash From eef1b6d65818b682f44e919450960187a377c288 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 30 May 2018 10:38:30 +0200 Subject: [PATCH 0993/1066] [enh] new command to list apps action --- data/actionsmap/yunohost.yml | 12 ++++++++++++ src/yunohost/app.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index d276a6cef..298332c51 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -773,6 +773,18 @@ app: subcategories: + action: + subcategory_help: Handle apps actions + actions: + + ### app_action_list() + list: + action_help: List app actions + api: GET //actions + arguments: + app_id: + help: app id + config: subcategory_help: Applications configuration panel actions: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5f52a8628..601bc7ef1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1367,6 +1367,31 @@ def app_change_label(auth, app, new_label): app_ssowatconf(auth) +# ACTIONS todo list +# * save actions.json +# commands: +# yunohost app action list $app +# yunohost app action run $app $action -d parameters +# docstring + +def app_action_list(app_id): + installed = _is_installed(app_id) + if not installed: + raise MoulinetteError(errno.ENOPKG, + m18n.n('app_not_installed', app=app_id)) + + actions = os.path.join( APPS_SETTING_PATH, app_id, 'actions.json') + + if not os.path.exists(actions): + return { + "actions": [], + } + + return { + "actions": read_json(actions), + } + + # Config panel todo list: # * docstrings # * merge translations on the json once the workflow is in place From f35e3ef0551d0f37a26736e31cc1f6123e0f5113 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 31 May 2018 12:40:13 +0200 Subject: [PATCH 0994/1066] [enh] can run the action of an app --- data/actionsmap/yunohost.yml | 14 +++ src/yunohost/app.py | 199 ++++++++++++++++++++++++++++++++++- src/yunohost/hook.py | 1 + 3 files changed, 212 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 298332c51..147e08b0c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -785,6 +785,20 @@ app: app_id: help: app id + ### app_action_run() + run: + action_help: Run app action + api: PUT //actions/ + arguments: + app_id: + help: app id + action: + help: action id + -a: + full: --args + help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") + + config: subcategory_help: Applications configuration panel actions: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 601bc7ef1..05acc40e3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1392,6 +1392,51 @@ def app_action_list(app_id): } +def app_action_run(app_id, action, args=None): + from yunohost.hook import hook_exec + import tempfile + + # will raise if action doesn't exist + actions = app_action_list(app_id)["actions"] + + if action not in actions: + raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app_id, ", ".join(actions.keys()))) + + action_declaration = actions[action] + + # Retrieve arguments list for install script + args_dict = {} if not args else \ + dict(urlparse.parse_qsl(args, keep_blank_values=True)) + args_odict = _parse_args_for_action(actions[action], args=args_dict) + args_list = args_odict.values() + + env_dict = _make_environment_dict(args_odict, prefix="ACTION_") + env_dict["YNH_APP_ID"] = app_id + env_dict["YNH_ACTION"] = action + + _, path = tempfile.mkstemp() + + with open(path, "w") as script: + script.write(action_declaration["command"]) + + os.chmod(path, 700) + + retcode = hook_exec( + path, + args=args_list, + env=env_dict, + chdir=action_declaration.get("cwd"), + user=action_declaration.get("user", "root"), + ) + + if retcode not in action_declaration.get("accepted_return_codes", [0]): + raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app_id, retcode)) + + os.remove(path) + + return logger.success("Action successed!") + + # Config panel todo list: # * docstrings # * merge translations on the json once the workflow is in place @@ -2105,7 +2150,157 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): return args_dict -def _make_environment_dict(args_dict): +def _parse_args_for_action(action, args={}, auth=None): + """Parse arguments needed for an action from the actions list + + Retrieve specified arguments for the action from the manifest, and parse + given args according to that. If some required arguments are not provided, + its values will be asked if interaction is possible. + Parsed arguments will be returned as an OrderedDict + + Keyword arguments: + action -- The action + args -- A dictionnary of arguments to parse + + """ + from yunohost.domain import (domain_list, _get_maindomain, + domain_url_available, _normalize_domain_path) + from yunohost.user import user_info + + args_dict = OrderedDict() + + if 'arguments' not in action: + logger.debug("no arguments found for '%s' in manifest", action) + return args_dict + + action_args = action['arguments'] + + for arg in action_args: + arg_name = arg['name'] + arg_type = arg.get('type', 'string') + arg_default = arg.get('default', None) + arg_choices = arg.get('choices', []) + arg_value = None + + # Transpose default value for boolean type and set it to + # false if not defined. + if arg_type == 'boolean': + arg_default = 1 if arg_default else 0 + + # Attempt to retrieve argument value + if arg_name in args: + arg_value = args[arg_name] + else: + if 'ask' in arg: + # Retrieve proper ask string + ask_string = _value_for_locale(arg['ask']) + + # Append extra strings + if arg_type == 'boolean': + ask_string += ' [0 | 1]' + elif arg_choices: + ask_string += ' [{0}]'.format(' | '.join(arg_choices)) + if arg_default is not None: + ask_string += ' (default: {0})'.format(arg_default) + + # Check for a password argument + is_password = True if arg_type == 'password' else False + + if arg_type == 'domain': + arg_default = _get_maindomain() + ask_string += ' (default: {0})'.format(arg_default) + msignals.display(m18n.n('domains_available')) + for domain in domain_list(auth)['domains']: + msignals.display("- {}".format(domain)) + + try: + input_string = msignals.prompt(ask_string, is_password) + except NotImplementedError: + input_string = None + if (input_string == '' or input_string is None) \ + and arg_default is not None: + arg_value = arg_default + else: + arg_value = input_string + elif arg_default is not None: + arg_value = arg_default + + # Validate argument value + if (arg_value is None or arg_value == '') \ + and not arg.get('optional', False): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_required', name=arg_name)) + elif arg_value is None: + args_dict[arg_name] = '' + continue + + # Validate argument choice + if arg_choices and arg_value not in arg_choices: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_choice_invalid', + name=arg_name, choices=', '.join(arg_choices))) + + # Validate argument type + if arg_type == 'domain': + if arg_value not in domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=m18n.n('domain_unknown'))) + elif arg_type == 'user': + try: + user_info(auth, arg_value) + except MoulinetteError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=e.strerror)) + elif arg_type == 'app': + if not _is_installed(arg_value): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=m18n.n('app_unknown'))) + elif arg_type == 'boolean': + if isinstance(arg_value, bool): + arg_value = 1 if arg_value else 0 + else: + try: + arg_value = int(arg_value) + if arg_value not in [0, 1]: + raise ValueError() + except (TypeError, ValueError): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_choice_invalid', + name=arg_name, choices='0, 1')) + args_dict[arg_name] = arg_value + + # END loop over action_args... + + # If there's only one "domain" and "path", validate that domain/path + # is an available url and normalize the path. + + domain_args = [arg["name"] for arg in action_args + if arg.get("type", "string") == "domain"] + path_args = [arg["name"] for arg in action_args + if arg.get("type", "string") == "path"] + + if len(domain_args) == 1 and len(path_args) == 1: + + domain = args_dict[domain_args[0]] + path = args_dict[path_args[0]] + domain, path = _normalize_domain_path(domain, path) + + # Check the url is available + if not domain_url_available(auth, domain, path): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_location_unavailable')) + + # (We save this normalized path so that the install script have a + # standard path format to deal with no matter what the user inputted) + args_dict[path_args[0]] = path + + return args_dict + + +def _make_environment_dict(args_dict, prefix="APP_ARG_"): """ Convert a dictionnary containing manifest arguments to a dictionnary of env. var. to be passed to scripts @@ -2116,7 +2311,7 @@ def _make_environment_dict(args_dict): """ env_dict = {} for arg_name, arg_value in args_dict.items(): - env_dict["YNH_APP_ARG_%s" % arg_name.upper()] = arg_value + env_dict["YNH_%s%s" % (prefix, arg_name.upper())] = arg_value return env_dict diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 4688b25b3..2fb179f83 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -365,6 +365,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, stdout_callback if stdout_callback else lambda l: logger.debug(l.rstrip()), stderr_callback if stderr_callback else lambda l: logger.warning(l.rstrip()), ) + logger.debug("About to run the command '%s'" % command) returncode = call_async_output( command, callbacks, shell=False, cwd=chdir ) From 9cefc601610a99477a156c089d6026fe068d7bbd Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 31 May 2018 12:50:03 +0200 Subject: [PATCH 0995/1066] [enh] store actions.json en install/upgrade --- src/yunohost/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 05acc40e3..e74bb0b78 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -641,6 +641,9 @@ def app_upgrade(auth, app=[], url=None, file=None): os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "actions.json")): + os.system('cp -R %s/actions.json %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) @@ -761,6 +764,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "actions.json")): + os.system('cp -R %s/actions.json %s' % (extracted_app_folder, app_setting_path)) + if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) From 2d0b5edd2336e674e8d1533b730198d83c24917d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 04:52:38 +0200 Subject: [PATCH 0996/1066] [mod] DRY, easier to maintain here --- src/yunohost/app.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e74bb0b78..83dd49323 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -641,14 +641,9 @@ def app_upgrade(auth, app=[], url=None, file=None): os.system('rm -rf "%s/scripts" "%s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path)) os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) - if os.path.exists(os.path.join(extracted_app_folder, "actions.json")): - os.system('cp -R %s/actions.json %s' % (extracted_app_folder, app_setting_path)) - - if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): - os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) - - if os.path.exists(os.path.join(extracted_app_folder, "conf")): - os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) + for file_to_copy in ["actions.json", "config_panel.json", "conf"]: + if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): + os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path)) # So much win upgraded_apps.append(app_instance_name) @@ -764,14 +759,9 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False): os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) - if os.path.exists(os.path.join(extracted_app_folder, "actions.json")): - os.system('cp -R %s/actions.json %s' % (extracted_app_folder, app_setting_path)) - - if os.path.exists(os.path.join(extracted_app_folder, "config_panel.json")): - os.system('cp -R %s/config_panel.json %s' % (extracted_app_folder, app_setting_path)) - - if os.path.exists(os.path.join(extracted_app_folder, "conf")): - os.system('cp -R %s/conf %s' % (extracted_app_folder, app_setting_path)) + for file_to_copy in ["actions.json", "config_panel.json", "conf"]: + if os.path.exists(os.path.join(extracted_app_folder, file_to_copy)): + os.system('cp -R %s/%s %s' % (extracted_app_folder, file_to_copy, app_setting_path)) # Execute the app install script install_retcode = 1 From 26f2741a7e001b472aeadea3aa117c569ea0b55b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 04:53:52 +0200 Subject: [PATCH 0997/1066] [mod] mark actions commands as experimental --- src/yunohost/app.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 83dd49323..bef227db7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1363,14 +1363,12 @@ def app_change_label(auth, app, new_label): app_ssowatconf(auth) -# ACTIONS todo list -# * save actions.json -# commands: -# yunohost app action list $app -# yunohost app action run $app $action -d parameters -# docstring +# actions todo list: +# * docstring def app_action_list(app_id): + logger.warning(m18n.n('experimental_feature')) + installed = _is_installed(app_id) if not installed: raise MoulinetteError(errno.ENOPKG, @@ -1389,6 +1387,8 @@ def app_action_list(app_id): def app_action_run(app_id, action, args=None): + logger.warning(m18n.n('experimental_feature')) + from yunohost.hook import hook_exec import tempfile From 64fe357995ff5b507be0e3907a28cc6ade14217d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 04:54:56 +0200 Subject: [PATCH 0998/1066] [mod] compress code a bit --- src/yunohost/app.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bef227db7..d310b44de 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1374,15 +1374,10 @@ def app_action_list(app_id): raise MoulinetteError(errno.ENOPKG, m18n.n('app_not_installed', app=app_id)) - actions = os.path.join( APPS_SETTING_PATH, app_id, 'actions.json') - - if not os.path.exists(actions): - return { - "actions": [], - } + actions = os.path.join(APPS_SETTING_PATH, app_id, 'actions.json') return { - "actions": read_json(actions), + "actions": read_json(actions) if os.path.exists(actions) else [], } From 3806e2a90833782382cc065165dcc65bf6ef6bb8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 05:27:00 +0200 Subject: [PATCH 0999/1066] [mod] style --- src/yunohost/app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d310b44de..a6c8e209e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1396,8 +1396,7 @@ def app_action_run(app_id, action, args=None): action_declaration = actions[action] # Retrieve arguments list for install script - args_dict = {} if not args else \ - dict(urlparse.parse_qsl(args, keep_blank_values=True)) + args_dict = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} args_odict = _parse_args_for_action(actions[action], args=args_dict) args_list = args_odict.values() From f8d99e52e3daadc153d82173549ec14065488c8e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 05:27:15 +0200 Subject: [PATCH 1000/1066] [mod] DRY, remove big copy/pasta --- src/yunohost/app.py | 145 ++++---------------------------------------- 1 file changed, 13 insertions(+), 132 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a6c8e209e..9c5f9ac67 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2005,139 +2005,12 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - from yunohost.domain import (domain_list, _get_maindomain, - domain_url_available, _normalize_domain_path) - from yunohost.user import user_info - - args_dict = OrderedDict() try: action_args = manifest['arguments'][action] except KeyError: logger.debug("no arguments found for '%s' in manifest", action) else: - for arg in action_args: - arg_name = arg['name'] - arg_type = arg.get('type', 'string') - arg_default = arg.get('default', None) - arg_choices = arg.get('choices', []) - arg_value = None - - # Transpose default value for boolean type and set it to - # false if not defined. - if arg_type == 'boolean': - arg_default = 1 if arg_default else 0 - - # Attempt to retrieve argument value - if arg_name in args: - arg_value = args[arg_name] - else: - if 'ask' in arg: - # Retrieve proper ask string - ask_string = _value_for_locale(arg['ask']) - - # Append extra strings - if arg_type == 'boolean': - ask_string += ' [0 | 1]' - elif arg_choices: - ask_string += ' [{0}]'.format(' | '.join(arg_choices)) - if arg_default is not None: - ask_string += ' (default: {0})'.format(arg_default) - - # Check for a password argument - is_password = True if arg_type == 'password' else False - - if arg_type == 'domain': - arg_default = _get_maindomain() - ask_string += ' (default: {0})'.format(arg_default) - msignals.display(m18n.n('domains_available')) - for domain in domain_list(auth)['domains']: - msignals.display("- {}".format(domain)) - - try: - input_string = msignals.prompt(ask_string, is_password) - except NotImplementedError: - input_string = None - if (input_string == '' or input_string is None) \ - and arg_default is not None: - arg_value = arg_default - else: - arg_value = input_string - elif arg_default is not None: - arg_value = arg_default - - # Validate argument value - if (arg_value is None or arg_value == '') \ - and not arg.get('optional', False): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_required', name=arg_name)) - elif arg_value is None: - args_dict[arg_name] = '' - continue - - # Validate argument choice - if arg_choices and arg_value not in arg_choices: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_choice_invalid', - name=arg_name, choices=', '.join(arg_choices))) - - # Validate argument type - if arg_type == 'domain': - if arg_value not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=m18n.n('domain_unknown'))) - elif arg_type == 'user': - try: - user_info(auth, arg_value) - except MoulinetteError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=e.strerror)) - elif arg_type == 'app': - if not _is_installed(arg_value): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=m18n.n('app_unknown'))) - elif arg_type == 'boolean': - if isinstance(arg_value, bool): - arg_value = 1 if arg_value else 0 - else: - try: - arg_value = int(arg_value) - if arg_value not in [0, 1]: - raise ValueError() - except (TypeError, ValueError): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_choice_invalid', - name=arg_name, choices='0, 1')) - args_dict[arg_name] = arg_value - - # END loop over action_args... - - # If there's only one "domain" and "path", validate that domain/path - # is an available url and normalize the path. - - domain_args = [arg["name"] for arg in action_args - if arg.get("type", "string") == "domain"] - path_args = [arg["name"] for arg in action_args - if arg.get("type", "string") == "path"] - - if len(domain_args) == 1 and len(path_args) == 1: - - domain = args_dict[domain_args[0]] - path = args_dict[path_args[0]] - domain, path = _normalize_domain_path(domain, path) - - # Check the url is available - if not domain_url_available(auth, domain, path): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_unavailable')) - - # (We save this normalized path so that the install script have a - # standard path format to deal with no matter what the user inputted) - args_dict[path_args[0]] = path - - return args_dict + return _parse_action_args_in_yunohost_format(args, action_args, auth) def _parse_args_for_action(action, args={}, auth=None): @@ -2153,10 +2026,6 @@ def _parse_args_for_action(action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - from yunohost.domain import (domain_list, _get_maindomain, - domain_url_available, _normalize_domain_path) - from yunohost.user import user_info - args_dict = OrderedDict() if 'arguments' not in action: @@ -2165,6 +2034,18 @@ def _parse_args_for_action(action, args={}, auth=None): action_args = action['arguments'] + return _parse_action_args_in_yunohost_format(args, action_args, auth) + + +def _parse_action_args_in_yunohost_format(args, action_args, auth=None): + """Parse arguments store in either manifest.json or actions.json + """ + from yunohost.domain import (domain_list, _get_maindomain, + domain_url_available, _normalize_domain_path) + from yunohost.user import user_info + + args_dict = OrderedDict() + for arg in action_args: arg_name = arg['name'] arg_type = arg.get('type', 'string') From f1181540c67938d2081dbca774a9c86e935f5ba2 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 22 Jun 2018 05:34:18 +0200 Subject: [PATCH 1001/1066] [mod] correct year for copyright stuff --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 859e4b43a..b9d31538a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -2,7 +2,7 @@ """ License - Copyright (C) 2016 YunoHost + Copyright (C) 2018 YunoHost This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published From b2d07d1c6edd1583a483e4c855a3e6aacbdc4710 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 22 Jun 2018 05:56:27 +0200 Subject: [PATCH 1002/1066] [mod] simplify code "raise" alone will do exactly the same thing. --- src/yunohost/log.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index b9d31538a..e9ce97bf8 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -252,8 +252,7 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password result = func(*args, **kwargs) except Exception as e: uo.error(e) - t, v, tb = exc_info() - raise t, v, tb + raise else: uo.success() return result From 7cbd5641bd3ed15af7fb3c4d0f035e66e86b155a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 06:03:07 +0200 Subject: [PATCH 1003/1066] [fix] fix recursive super slow import in certificate.py --- src/yunohost/certificate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 6d70b9b0a..bcd046644 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -40,7 +40,6 @@ from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -import yunohost.domain from yunohost.utils.network import get_public_ip from moulinette import m18n @@ -96,6 +95,8 @@ def certificate_status(auth, domain_list, full=False): full -- Display more info about the certificates """ + import yunohost.domain + # Check if old letsencrypt_ynh is installed # TODO / FIXME - Remove this in the future once the letsencrypt app is # not used anymore @@ -243,6 +244,8 @@ def _certificate_install_selfsigned(domain_list, force=False): def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): + import yunohost.domain + if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -309,6 +312,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal email -- Emails root if some renewing failed """ + import yunohost.domain + # Check if old letsencrypt_ynh is installed # TODO / FIXME - Remove this in the future once the letsencrypt app is # not used anymore @@ -402,6 +407,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal ############################################################################### def _check_old_letsencrypt_app(): + import yunohost.domain + installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] if "letsencrypt" not in installedAppIds: From 014a39fe95b06088d18d6997565fd31a1ff3f0fa Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 06:17:57 +0200 Subject: [PATCH 1004/1066] [ux] display human understandable choice for boolean type on installation --- src/yunohost/app.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9c5f9ac67..30a6932e8 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2068,11 +2068,15 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): # Append extra strings if arg_type == 'boolean': - ask_string += ' [0 | 1]' + ask_string += ' [yes | no]' elif arg_choices: ask_string += ' [{0}]'.format(' | '.join(arg_choices)) + if arg_default is not None: - ask_string += ' (default: {0})'.format(arg_default) + if arg_type == 'boolean': + ask_string += ' (default: {0})'.format("yes" if arg_type == 1 else "no") + else: + ask_string += ' (default: {0})'.format(arg_default) # Check for a password argument is_password = True if arg_type == 'password' else False @@ -2133,14 +2137,14 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): if isinstance(arg_value, bool): arg_value = 1 if arg_value else 0 else: - try: - arg_value = int(arg_value) - if arg_value not in [0, 1]: - raise ValueError() - except (TypeError, ValueError): + if str(arg_value).lower() in ["1", "yes", "y"]: + arg_value = 1 + elif str(arg_value).lower() in ["0", "no", "n"]: + arg_value = 0 + else: raise MoulinetteError(errno.EINVAL, m18n.n('app_argument_choice_invalid', - name=arg_name, choices='0, 1')) + name=arg_name, choices='yes, no, y, n, 1, 0')) args_dict[arg_name] = arg_value # END loop over action_args... From dc0611d91b4294050347eb6b70bc3295dbefeb76 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 07:34:10 +0200 Subject: [PATCH 1005/1066] [mod] better routes for actions --- data/actionsmap/yunohost.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 147e08b0c..53e6acaef 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -780,7 +780,7 @@ app: ### app_action_list() list: action_help: List app actions - api: GET //actions + api: GET /apps//actions arguments: app_id: help: app id @@ -788,7 +788,7 @@ app: ### app_action_run() run: action_help: Run app action - api: PUT //actions/ + api: PUT /apps//actions/ arguments: app_id: help: app id @@ -798,7 +798,6 @@ app: full: --args help: Serialized arguments for app script (i.e. "domain=domain.tld&path=/path") - config: subcategory_help: Applications configuration panel actions: From b46fb467682c6b190a90bffee7c5a9ed315b8c0b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 22 Jun 2018 07:34:24 +0200 Subject: [PATCH 1006/1066] [fix] forgot to commit actions format change --- src/yunohost/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9c5f9ac67..420a5fb3e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1389,6 +1389,7 @@ def app_action_run(app_id, action, args=None): # will raise if action doesn't exist actions = app_action_list(app_id)["actions"] + actions = {x["id"]: x for x in actions} if action not in actions: raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app_id, ", ".join(actions.keys()))) From 79f1c1a8972ba8edaaae3c39d25d357782a71a78 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 23 Jun 2018 00:42:07 +0200 Subject: [PATCH 1007/1066] [fix] referenced the wrong variable --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 30a6932e8..4f4c96770 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2074,7 +2074,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): if arg_default is not None: if arg_type == 'boolean': - ask_string += ' (default: {0})'.format("yes" if arg_type == 1 else "no") + ask_string += ' (default: {0})'.format("yes" if arg_default == 1 else "no") else: ask_string += ' (default: {0})'.format(arg_default) From a7e85dbbba32de72b402781bb7c00bd7ee52e804 Mon Sep 17 00:00:00 2001 From: pitchum Date: Sat, 16 Jun 2018 10:15:52 +0200 Subject: [PATCH 1008/1066] [enh] Add MUA autoconfig. --- data/hooks/conf_regen/15-nginx | 15 +++++++++++++++ data/templates/nginx/autoconfig.tpl.xml | 19 +++++++++++++++++++ data/templates/nginx/server.tpl.conf | 4 ++++ src/yunohost/app.py | 1 + 4 files changed, 39 insertions(+) create mode 100644 data/templates/nginx/autoconfig.tpl.xml diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index 03c769b69..7c114daa5 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -38,12 +38,19 @@ do_pre_regen() { for domain in $domain_list; do domain_conf_dir="${nginx_conf_dir}/${domain}.d" mkdir -p "$domain_conf_dir" + mail_autoconfig_dir="${pending_dir}/var/www/.well-known/${domain}/autoconfig/mail/" + mkdir -p "$mail_autoconfig_dir" # NGINX server configuration cat server.tpl.conf \ | sed "s/{{ domain }}/${domain}/g" \ > "${nginx_conf_dir}/${domain}.conf" + cat autoconfig.tpl.xml \ + | sed "s/{{ domain }}/${domain}/g" \ + > "${mail_autoconfig_dir}/config-v1.1.xml" + + [[ $main_domain != $domain ]] \ && touch "${domain_conf_dir}/yunohost_local.conf" \ || cp yunohost_local.conf "${domain_conf_dir}/yunohost_local.conf" @@ -58,6 +65,14 @@ do_pre_regen() { || touch "${nginx_conf_dir}/${file}" done + # remove old mail-autoconfig files + autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml) + for file in $autoconfig_files; do + domain=$(basename $(readlink -f $(dirname $file)/../..)) + [[ $domain_list =~ $domain ]] \ + || (mkdir -p "$(dirname ${pending_dir}/${file})" && touch "${pending_dir}/${file}") + done + # disable default site mkdir -p "${nginx_dir}/sites-enabled" touch "${nginx_dir}/sites-enabled/default" diff --git a/data/templates/nginx/autoconfig.tpl.xml b/data/templates/nginx/autoconfig.tpl.xml new file mode 100644 index 000000000..a42643198 --- /dev/null +++ b/data/templates/nginx/autoconfig.tpl.xml @@ -0,0 +1,19 @@ + + + {{ domain }} + + {{ domain }} + 993 + SSL + password-cleartext + %EMAILLOCALPART% + + + {{ domain }} + 587 + STARTTLS + password-cleartext + %EMAILLOCALPART% + + + diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 56fc13f35..78909e3f6 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -11,6 +11,10 @@ server { return 301 https://$http_host$request_uri; } + location /.well-known/autoconfig/mail { + alias /var/www/.well-known/{{ domain }}/autoconfig/mail; + } + access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; } diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 72a53da1f..f0ecbcfeb 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1321,6 +1321,7 @@ def app_ssowatconf(auth): # Authorize ACME challenge url skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$") + skipped_regex.append("^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$") conf_dict = { 'portal_domain': main_domain, From 43b10298fccc079093f20641c51d41646bf54439 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 30 Jun 2018 11:14:54 +0200 Subject: [PATCH 1009/1066] [fix] timeout on get_public_ip otherwish dyndns update is stucked --- src/yunohost/utils/network.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index dec0384bf..4398a80f6 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -21,7 +21,9 @@ import logging import re import subprocess -from urllib import urlopen +import requests + +from requests import ConnectionError logger = logging.getLogger('yunohost.utils.network') @@ -37,8 +39,8 @@ def get_public_ip(protocol=4): raise ValueError("invalid protocol version") try: - return urlopen(url).read().strip() - except IOError: + return requests.get(url, timeout=30).content.strip() + except ConnectionError: return None From 20d6c3050397547a9394f2b4bb6ec5c9ba723fe6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 30 Jun 2018 11:39:01 +0200 Subject: [PATCH 1010/1066] [fix] sometime nginx is not running --- data/hooks/conf_regen/15-nginx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index 03c769b69..a3bc8a7c5 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -77,7 +77,7 @@ do_post_regen() { done # Reload nginx configuration - sudo service nginx reload + pgrep nginx && sudo service nginx reload } FORCE=${2:-0} From 834088551f66764d889b7761be9eb6f7b27a1f3d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 30 Jun 2018 14:39:31 +0200 Subject: [PATCH 1011/1066] [fix] uses moulinette download_text utils --- src/yunohost/utils/network.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 4398a80f6..871b3e6d1 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -21,9 +21,7 @@ import logging import re import subprocess -import requests - -from requests import ConnectionError +from moulinette.utils.network import download_text logger = logging.getLogger('yunohost.utils.network') @@ -38,10 +36,7 @@ def get_public_ip(protocol=4): else: raise ValueError("invalid protocol version") - try: - return requests.get(url, timeout=30).content.strip() - except ConnectionError: - return None + return download_text(url, timeout=30).strip() def get_network_interfaces(): From 344ae6bbf261bb9d63c7e396c0abbd6652053953 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 1 Jul 2018 18:37:34 +0200 Subject: [PATCH 1012/1066] Fix nodejs removal in ynh_remove_nodejs --- data/helpers.d/nodejs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index 156507c3c..5111fa671 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -138,6 +138,8 @@ ynh_remove_nodejs () { then ynh_secure_remove "$n_install_dir" ynh_secure_remove "/usr/local/n" + sed --in-place "/N_PREFIX/d" /root/.bashrc + rm -f /etc/cron.daily/node_update fi } From 36c8d1a787b6d734e2d32e390dc8345434d9e4ce Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 2 Jul 2018 14:32:41 +0200 Subject: [PATCH 1013/1066] e.strerror don't always exists, use str(e) instead --- src/yunohost/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ad8cfd846..05504a755 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -329,7 +329,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) + m18n.n('ssowat_persistent_conf_read_error', error=str(e))) except IOError: ssowat_conf = {} @@ -343,7 +343,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: raise MoulinetteError(errno.EPERM, - m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + m18n.n('ssowat_persistent_conf_write_error', error=str(e))) os.system('chmod 644 /etc/ssowat/conf.json.persistent') From 51175db814cc7ecd23c12f9882a6de5f2021e5ca Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 2 Jul 2018 17:04:41 +0200 Subject: [PATCH 1014/1066] [mod] information needed for the admin UI --- src/yunohost/app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 420a5fb3e..637db1747 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1369,14 +1369,14 @@ def app_change_label(auth, app, new_label): def app_action_list(app_id): logger.warning(m18n.n('experimental_feature')) - installed = _is_installed(app_id) - if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app_id)) + # this will take care of checking if the app is installed + app_info_dict = app_info(app_id) actions = os.path.join(APPS_SETTING_PATH, app_id, 'actions.json') return { + "app_id": app_id, + "app_name": app_info_dict["name"], "actions": read_json(actions) if os.path.exists(actions) else [], } From 0ab51209fe9b387c369219578bac1e484b0a1288 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 3 Jul 2018 20:38:23 +0200 Subject: [PATCH 1015/1066] [enh] list available users on app installation user argument --- locales/en.json | 1 + src/yunohost/app.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 19213c372..45b3cdb1a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -404,6 +404,7 @@ "user_unknown": "Unknown user: {user:s}", "user_update_failed": "Unable to update user", "user_updated": "The user has been updated", + "users_available": "Available users:", "yunohost_already_installed": "YunoHost is already installed", "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_ca_creation_success": "The local certification authority has been created.", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 420a5fb3e..a3f593d3e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2043,7 +2043,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): """ from yunohost.domain import (domain_list, _get_maindomain, domain_url_available, _normalize_domain_path) - from yunohost.user import user_info + from yunohost.user import user_info, user_list args_dict = OrderedDict() @@ -2085,6 +2085,11 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): for domain in domain_list(auth)['domains']: msignals.display("- {}".format(domain)) + if arg_type == 'user': + msignals.display(m18n.n('users_available')) + for user in user_list(auth)['users'].keys(): + msignals.display("- {}".format(user)) + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: From 3bcbe1941aa660c628359330aa9bfe286e38a3d8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 3 Jul 2018 20:43:18 +0200 Subject: [PATCH 1016/1066] [mod] remove useless imports --- src/yunohost/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index bed5fb8c8..bbcecc8d6 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -36,7 +36,6 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file from yunohost.service import service_status logger = getActionLogger('yunohost.user') @@ -112,7 +111,6 @@ def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota -- Mailbox size quota """ - import pwd from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf From 14387c43ebd24737065514f187275c53d794291a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Jul 2018 14:08:28 +0200 Subject: [PATCH 1017/1066] [mod] uses app_list installed option instead --- src/yunohost/app.py | 51 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 420a5fb3e..686151186 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1284,7 +1284,7 @@ def app_ssowatconf(auth): redirected_urls = {} try: - apps_list = app_list()['apps'] + apps_list = app_list(installed=True)['apps'] except: apps_list = [] @@ -1293,31 +1293,30 @@ def app_ssowatconf(auth): return s.split(',') if s else [] for app in apps_list: - if _is_installed(app['id']): - with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f: - app_settings = yaml.load(f) - for item in _get_setting(app_settings, 'skipped_uris'): - if item[-1:] == '/': - item = item[:-1] - skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'skipped_regex'): - skipped_regex.append(item) - for item in _get_setting(app_settings, 'unprotected_uris'): - if item[-1:] == '/': - item = item[:-1] - unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'unprotected_regex'): - unprotected_regex.append(item) - for item in _get_setting(app_settings, 'protected_uris'): - if item[-1:] == '/': - item = item[:-1] - protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) - for item in _get_setting(app_settings, 'protected_regex'): - protected_regex.append(item) - if 'redirected_urls' in app_settings: - redirected_urls.update(app_settings['redirected_urls']) - if 'redirected_regex' in app_settings: - redirected_regex.update(app_settings['redirected_regex']) + with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f: + app_settings = yaml.load(f) + for item in _get_setting(app_settings, 'skipped_uris'): + if item[-1:] == '/': + item = item[:-1] + skipped_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) + for item in _get_setting(app_settings, 'skipped_regex'): + skipped_regex.append(item) + for item in _get_setting(app_settings, 'unprotected_uris'): + if item[-1:] == '/': + item = item[:-1] + unprotected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) + for item in _get_setting(app_settings, 'unprotected_regex'): + unprotected_regex.append(item) + for item in _get_setting(app_settings, 'protected_uris'): + if item[-1:] == '/': + item = item[:-1] + protected_urls.append(app_settings['domain'] + app_settings['path'].rstrip('/') + item) + for item in _get_setting(app_settings, 'protected_regex'): + protected_regex.append(item) + if 'redirected_urls' in app_settings: + redirected_urls.update(app_settings['redirected_urls']) + if 'redirected_regex' in app_settings: + redirected_regex.update(app_settings['redirected_regex']) for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) From d27cce4af6f3b3267e4db74a6ec83f2b498beedb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Jul 2018 18:04:22 +0200 Subject: [PATCH 1018/1066] [mod] put code closer to usage --- src/yunohost/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 686151186..a90915778 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1270,10 +1270,6 @@ def app_ssowatconf(auth): main_domain = _get_maindomain() domains = domain_list(auth)['domains'] - users = {} - for username in user_list(auth)['users'].keys(): - users[username] = app_map(user=username) - skipped_urls = [] skipped_regex = [] unprotected_urls = [] @@ -1342,7 +1338,8 @@ def app_ssowatconf(auth): 'protected_regex': protected_regex, 'redirected_urls': redirected_urls, 'redirected_regex': redirected_regex, - 'users': users, + 'users': {username: app_map(user=username) + for username in user_list(auth)['users'].keys()}, } with open('/etc/ssowat/conf.json', 'w+') as f: From 2bf327970b9f9270d6d5453a68b31fa835d22392 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 9 Jul 2018 18:30:45 +0200 Subject: [PATCH 1019/1066] [enh] allow an application to optout of sso --- src/yunohost/app.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a90915778..3c654cbe5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -399,6 +399,8 @@ def app_map(app=None, raw=False, user=None): continue if 'domain' not in app_settings: continue + if 'no_sso' in app_settings: # I don't think we need to check for the value here + continue if user is not None: if ('mode' not in app_settings or ('mode' in app_settings @@ -1291,6 +1293,10 @@ def app_ssowatconf(auth): for app in apps_list: with open(APPS_SETTING_PATH + app['id'] + '/settings.yml') as f: app_settings = yaml.load(f) + + if 'no_sso' in app_settings: + continue + for item in _get_setting(app_settings, 'skipped_uris'): if item[-1:] == '/': item = item[:-1] From 99b395d82327761b0486ed5b9e2515e096aa1a8c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 3 Jul 2018 20:43:18 +0200 Subject: [PATCH 1020/1066] [mod] remove useless imports --- src/yunohost/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index bed5fb8c8..bbcecc8d6 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -36,7 +36,6 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_file from yunohost.service import service_status logger = getActionLogger('yunohost.user') @@ -112,7 +111,6 @@ def user_create(auth, username, firstname, lastname, mail, password, mailbox_quota -- Mailbox size quota """ - import pwd from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf From fbaac17870256b13c1e3304e4bbbb730c764bd3f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 19 Jul 2018 01:03:15 +0200 Subject: [PATCH 1021/1066] [enh] default action cwd to the app yunohost app settings folder --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index cd7a5ba77..2287723bf 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1416,7 +1416,7 @@ def app_action_run(app_id, action, args=None): path, args=args_list, env=env_dict, - chdir=action_declaration.get("cwd"), + chdir=action_declaration.get("cwd", "/etc/yunohost/apps/" + app_id), user=action_declaration.get("user", "root"), ) From 3facf89c7ed320a3d6ec6afb6e4aca46b4c89c08 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 19 Jul 2018 01:13:23 +0200 Subject: [PATCH 1022/1066] [enh] allow to uses in action cwd --- src/yunohost/app.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2287723bf..7961591e7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1412,11 +1412,16 @@ def app_action_run(app_id, action, args=None): os.chmod(path, 700) + if action_declaration.get("cwd"): + cwd = action_declaration["cwd"].replace("$app_id", app_id) + else: + cwd = "/etc/yunohost/apps/" + app_id + retcode = hook_exec( path, args=args_list, env=env_dict, - chdir=action_declaration.get("cwd", "/etc/yunohost/apps/" + app_id), + chdir=cwd, user=action_declaration.get("user", "root"), ) From 314fe830025035a777f36f76e3de7dd910028793 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Thu, 19 Jul 2018 21:17:54 +0200 Subject: [PATCH 1023/1066] Fix container detection Since LXC 3.0 it's not possible to detect if we are in a container by the init process ID --- src/yunohost/tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 05504a755..c1b76a748 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -234,14 +234,14 @@ def _is_inside_container(): Returns True or False """ - # See https://stackoverflow.com/a/37016302 - p = subprocess.Popen("sudo cat /proc/1/sched".split(), + # See https://www.2daygeek.com/check-linux-system-physical-virtual-machine-virtualization-technology/ + p = subprocess.Popen("sudo systemd-detect-virt".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out, _ = p.communicate() - - return out.split()[1] != "(1," + container = ['lxc','lxd','docker'] + return out.split()[0] in container def tools_postinstall(domain, password, ignore_dyndns=False): From d328864a43c43c827264eceb8e1ac52c67350037 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 16:26:27 +0000 Subject: [PATCH 1024/1066] Fix app name in case an url is provided (otherwise it tries to create a file with https://... in the name) --- src/yunohost/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 92f5584a6..e443f5178 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -753,6 +753,8 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False # Start register change on system uo.extra.update({'env':env_dict}) + uo.related_to = [s for s in uo.related_to if s[0] != "app"] + uo.related_to.append(("app", app_id)) uo.start() # Create app directory From fe48c37ce43b4e3817030560a3124c4a4ba5b7b2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 16:34:23 +0000 Subject: [PATCH 1025/1066] Attempt to improve wording --- locales/en.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index fbfdb18b3..97425a4bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -208,10 +208,10 @@ "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "log_corrupted_md_file": "The yaml metadata file associated with logs is corrupted : '{md_file}'", "log_category_404": "The log category '{category}' does not exist", - "log_link_to_log": "Complete log of this operation: '{desc}'", - "log_help_to_get_log": "To view complete log of the operation '{desc}', run: 'yunohost log display {name}'", - "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log", - "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help please give the complete log of the operation, you can get it by running: 'yunohost log display {name}'", + "log_link_to_log": "Full log of this operation: '{desc}'", + "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", + "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation. It can be obtained with the command 'yunohost log display {name}'", "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", From eb42a25ada17dcbd421129924600b67451d5cb65 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 17:55:05 +0000 Subject: [PATCH 1026/1066] Add python helper equivalent to yunopaste --- src/yunohost/utils/yunopaste.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/yunohost/utils/yunopaste.py diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py new file mode 100644 index 000000000..c084d78ce --- /dev/null +++ b/src/yunohost/utils/yunopaste.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +import requests +import json +import errno + +from moulinette.core import MoulinetteError + +def yunopaste(data): + + paste_server = "https://paste.yunohost.org" + + try: + r = requests.post("%s/documents" % paste_server, data=data) + except Exception as e: + raise MoulinetteError(errno.EIO, + "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) + + if r.status_code != 200: + raise MoulinetteError(errno.EIO, + "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % r.text) + + try: + url = json.loads(r.text)["key"] + except: + raise MoulinetteError(errno.EIO, + "Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) + + return "%s/raw/%s" % (paste_server, url) From 8eaceefbbd1d63a2eb8b18e60db1c55764f9e1c6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 21 Jul 2018 17:59:51 +0000 Subject: [PATCH 1027/1066] Add a --share option to 'log display' to share the log on yunopaste --- data/actionsmap/yunohost.yml | 3 +++ locales/en.json | 2 +- src/yunohost/log.py | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8e3c2ed54..209525de0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1693,3 +1693,6 @@ log: help: Number of lines to display default: 50 type: int + --share: + help: Share the full log using yunopaste + action: store_true diff --git a/locales/en.json b/locales/en.json index 97425a4bd..c6310f6fb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -211,7 +211,7 @@ "log_link_to_log": "Full log of this operation: '{desc}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation", - "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation. It can be obtained with the command 'yunohost log display {name}'", + "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please share the full log of this operation using the command 'yunohost log display {name} --share'", "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", "log_operation_unit_unclosed_properly": "Operation unit has not been closed properly", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index e9ce97bf8..f079a3894 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -36,6 +36,7 @@ from sys import exc_info from moulinette import m18n, msettings from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import read_file CATEGORIES_PATH = '/var/log/yunohost/categories/' OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' @@ -103,7 +104,7 @@ def log_list(category=[], limit=None): return result -def log_display(path, number=50): +def log_display(path, number=50, share=False): """ Display a log file enriched with metadata if any. @@ -113,6 +114,7 @@ def log_display(path, number=50): Argument: file_name number + share """ # Normalize log/metadata paths and filenames @@ -147,6 +149,17 @@ def log_display(path, number=50): infos["description"] = _get_description_from_name(base_filename) infos['name'] = base_filename + if share: + from yunohost.utils.yunopaste import yunopaste + content = "" + if os.path.exists(md_path): + content += read_file(md_path) + content += "\n============\n\n" + if os.path.exists(log_path): + content += read_file(log_path) + + return yunopaste(content) + # Display metadata if exist if os.path.exists(md_path): with open(md_path, "r") as md_file: From f528893b4d1a7b5edd70d215d35ebc8fa1f488ef Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 22 Jul 2018 11:24:32 +0200 Subject: [PATCH 1028/1066] [mod] propagate the no_checks logic to acme-tiny code --- src/yunohost/certificate.py | 7 ++++--- src/yunohost/vendor/acme_tiny/acme_tiny.py | 23 +++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index bcd046644..930bc0293 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -289,7 +289,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F _check_domain_is_ready_for_ACME(domain) _configure_for_acme_challenge(auth, domain) - _fetch_and_enable_new_certificate(domain, staging) + _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) _install_cron() logger.success( @@ -383,7 +383,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if not no_checks: _check_domain_is_ready_for_ACME(domain) - _fetch_and_enable_new_certificate(domain, staging) + _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) logger.success( m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -521,7 +521,7 @@ def _check_acme_challenge_configuration(domain): return True -def _fetch_and_enable_new_certificate(domain, staging=False): +def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") @@ -562,6 +562,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): domain_csr_file, WEBROOT_FOLDER, log=logger, + no_checks=no_checks, CA=certification_authority) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py index fa1ee4dc5..f36aef877 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -12,7 +12,7 @@ LOGGER = logging.getLogger(__name__) LOGGER.addHandler(logging.StreamHandler()) LOGGER.setLevel(logging.INFO) -def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, no_checks=False): # helper function base64 encode for jose spec def _b64(b): return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") @@ -111,16 +111,17 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): with open(wellknown_path, "w") as wellknown_file: wellknown_file.write(keyauthorization) - # check that the file is in place - wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) - try: - resp = urlopen(wellknown_url) - resp_data = resp.read().decode('utf8').strip() - assert resp_data == keyauthorization - except (IOError, AssertionError): - os.remove(wellknown_path) - raise ValueError("Wrote file to {0}, but couldn't download {1}".format( - wellknown_path, wellknown_url)) + if not no_checks: # sometime the local g + # check that the file is in place + wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) + try: + resp = urlopen(wellknown_url) + resp_data = resp.read().decode('utf8').strip() + assert resp_data == keyauthorization + except (IOError, AssertionError): + os.remove(wellknown_path) + raise ValueError("Wrote file to {0}, but couldn't download {1}".format( + wellknown_path, wellknown_url)) # notify challenge are met code, result = _send_signed_request(challenge['uri'], { From 886afd4d69b7d1de847057cfdce96eecfb454dc7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 22 Jul 2018 13:38:43 +0200 Subject: [PATCH 1029/1066] [enh] after postinstall tell admin to had its first user --- locales/en.json | 1 + src/yunohost/tools.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/locales/en.json b/locales/en.json index 19213c372..6b1de237b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -304,6 +304,7 @@ "port_already_opened": "Port {port:d} is already opened for {ip_version:s} connections", "port_available": "Port {port:d} is available", "port_unavailable": "Port {port:d} is not available", + "recommend_to_add_first_user": "The post-install is finished but YunoHost needs at least one user to work correctly, you should add one using 'yunohost user create' or the admin interface.", "restore_action_required": "You must specify something to restore", "restore_already_installed_app": "An app is already installed with the id '{app:s}'", "restore_app_failed": "Unable to restore the app '{app:s}'", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 05504a755..b7fbf5d71 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -408,6 +408,8 @@ def tools_postinstall(domain, password, ignore_dyndns=False): service_regen_conf(force=True) logger.success(m18n.n('yunohost_configured')) + logger.warning(m18n.n('recommend_to_add_first_user')) + def tools_update(ignore_apps=False, ignore_packages=False): """ From 0b86ae0467da773f4b224b9f46075ab34a8f31b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 16:55:41 +0200 Subject: [PATCH 1030/1066] Remove remaining mention of 'rmilter' in domain_add --- src/yunohost/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 545408592..913b7868e 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -110,7 +110,7 @@ def domain_add(auth, domain, dyndns=False): # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): - service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'rmilter']) + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) app_ssowatconf(auth) except: From f52f81cba64e4618e79dc655c6ef31590a10314b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 16:38:01 +0000 Subject: [PATCH 1031/1066] Fix _tail function, hopefully that's the right fix --- src/yunohost/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 21b74c559..b5a7a5c11 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -745,7 +745,8 @@ def _tail(file, n): previous_log_file = _find_previous_log_file(file) if previous_log_file is not None: lines = _tail(previous_log_file, to_read - len(lines)) + lines - return lines[-to_read:offset and -offset or None] + + return lines def _find_previous_log_file(file): From f89c41f3e472b13e9b51e31024e2465a546d17d9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 16:38:53 +0000 Subject: [PATCH 1032/1066] Improve behavior of yunopaste() for integration with the webadmin --- locales/en.json | 1 + src/yunohost/log.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index c6310f6fb..52e102af4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -225,6 +225,7 @@ "log_app_remove": "Remove '{}' application", "log_app_upgrade": "Upgrade '{}' application", "log_app_makedefault": "Make '{}' as default application", + "log_available_on_yunopaste": "This log is now available via {url}", "log_backup_restore_system": "Restore system from a backup archive", "log_backup_restore_app": "Restore '{}' from a backup archive", "log_remove_on_failed_restore": "Remove '{}' after a failed restore from a backup archive", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index f079a3894..5443c6beb 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -158,7 +158,13 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): content += read_file(log_path) - return yunopaste(content) + url = yunopaste(content) + + logger.info(m18n.n("log_available_on_yunopaste", url=url)) + if msettings.get('interface') == 'api': + return { "url" : url } + else: + return # Display metadata if exist if os.path.exists(md_path): From 6d6d778a7b041ea772373785b90e2257ec880ae2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 17:06:09 +0000 Subject: [PATCH 1033/1066] Display log list in chronological order in CLI to avoid having to scroll to get the last action --- src/yunohost/log.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 5443c6beb..35cd80fe2 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -57,10 +57,10 @@ def log_list(category=[], limit=None): """ categories = category + is_api = msettings.get('interface') == 'api' # In cli we just display `operation` logs by default if not categories: - is_api = msettings.get('interface') == 'api' categories = ["operation"] if not is_api else CATEGORIES result = collections.OrderedDict() @@ -101,6 +101,11 @@ def log_list(category=[], limit=None): result[category].append(entry) + # Reverse the order of log when in cli, more comfortable to read (avoid unecessary scrolling) + if not is_api: + for category in result: + result[category] = list(reversed(result[category])) + return result From 581eb066b970e408f14d1e8cb081d6078c8ba093 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 18:38:57 +0000 Subject: [PATCH 1034/1066] [fix] Microdecision : dyndns update was broken if no IPv6 on the server --- src/yunohost/utils/network.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 871b3e6d1..a9602ff56 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -36,7 +36,11 @@ def get_public_ip(protocol=4): else: raise ValueError("invalid protocol version") - return download_text(url, timeout=30).strip() + try: + return download_text(url, timeout=30).strip() + except Exception as e: + logger.debug("Could not get public IPv%s : %s" % (str(protocol), str(e))) + return None def get_network_interfaces(): From 1de1b43e2f61023bab5551dbf55c6449bdd47397 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 25 Jul 2018 19:14:51 +0000 Subject: [PATCH 1035/1066] Avoid breaking the regen-conf if there's no .well-known mail autoconfig.xml to list --- data/hooks/conf_regen/15-nginx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index 0fb2e6a21..1aafcbfa2 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -66,7 +66,7 @@ do_pre_regen() { done # remove old mail-autoconfig files - autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml) + autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml 2>/dev/null || true) for file in $autoconfig_files; do domain=$(basename $(readlink -f $(dirname $file)/../..)) [[ $domain_list =~ $domain ]] \ From 7ab13b56c9f598a4f57b1485b49097d534eec09e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:16:10 -0400 Subject: [PATCH 1036/1066] Add postsrsd as dependency --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 256038598..3c74fea76 100644 --- a/debian/control +++ b/debian/control @@ -18,7 +18,7 @@ Depends: ${python:Depends}, ${misc:Depends} , ca-certificates, netcat-openbsd, iproute , mariadb-server, php-mysql | php-mysqlnd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd - , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils + , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils, postsrsd , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban , nginx-extras (>=1.6.2), php-fpm, php-ldap, php-intl From 240158ffaf1eab8e298c589e250858e3a4757c25 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:16:47 -0400 Subject: [PATCH 1037/1066] Link postfix to postsrsd for SRS --- data/templates/postfix/main.cf | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index 2cb1d8d72..c38896a3f 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -137,8 +137,10 @@ smtpd_recipient_restrictions = permit # SRS -sender_canonical_maps = regexp:/etc/postfix/sender_canonical +sender_canonical_maps = tcp:localhost:10001 sender_canonical_classes = envelope_sender +recipient_canonical_maps = tcp:localhost:10002 +recipient_canonical_classes= envelope_recipient,header_recipient # Ignore some headers smtp_header_checks = regexp:/etc/postfix/header_checks From b1fdb39880dd00afc2b1b38a7d1a6611e67445e2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:17:25 -0400 Subject: [PATCH 1038/1066] Tweaking postfix regen-conf to handle postsrsd conf --- data/hooks/conf_regen/19-postfix | 12 ++++++++- data/templates/postfix/postsrsd | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 data/templates/postfix/postsrsd diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 3cb5cdf50..2133c1bd5 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -10,15 +10,25 @@ do_pre_regen() { postfix_dir="${pending_dir}/etc/postfix" mkdir -p "$postfix_dir" + default_dir="${pending_dir}/etc/default/" + mkdir -p "$default_dir" + # install plain conf files cp plain/* "$postfix_dir" # prepare main.cf conf file main_domain=$(cat /etc/yunohost/current_host) + domain_list=$(sudo yunohost domain list --output-as plain --quiet) + cat main.cf \ | sed "s/{{ main_domain }}/${main_domain}/g" \ > "${postfix_dir}/main.cf" + cat postsrsd \ + | sed "s/{{ main_domain }}/${main_domain}/g" \ + | sed "s/{{ domain_list }}/${domain_list}/g" \ + > "${default_dir}/postsrsd" + # adapt it for IPv4-only hosts if [ ! -f /proc/net/if_inet6 ]; then sed -i \ @@ -34,7 +44,7 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || sudo service postfix restart + || { sudo service postfix restart && sudo service postsrsd restart } } FORCE=${2:-0} diff --git a/data/templates/postfix/postsrsd b/data/templates/postfix/postsrsd new file mode 100644 index 000000000..56bfd091e --- /dev/null +++ b/data/templates/postfix/postsrsd @@ -0,0 +1,43 @@ +# Default settings for postsrsd + +# Local domain name. +# Addresses are rewritten to originate from this domain. The default value +# is taken from `postconf -h mydomain` and probably okay. +# +SRS_DOMAIN={{ main_domain }} + +# Exclude additional domains. +# You may list domains which shall not be subjected to address rewriting. +# If a domain name starts with a dot, it matches all subdomains, but not +# the domain itself. Separate multiple domains by space or comma. +# We have to put some "dummy" stuff at start and end... see this comment : +# https://github.com/roehling/postsrsd/issues/64#issuecomment-284003762 +SRS_EXCLUDE_DOMAINS=dummy {{ domain_list }} dummy + +# First separator character after SRS0 or SRS1. +# Can be one of: -+= +SRS_SEPARATOR== + +# Secret key to sign rewritten addresses. +# When postsrsd is installed for the first time, a random secret is generated +# and stored in /etc/postsrsd.secret. For most installations, that's just fine. +# +SRS_SECRET=/etc/postsrsd.secret + +# Local ports for TCP list. +# These ports are used to bind the TCP list for postfix. If you change +# these, you have to modify the postfix settings accordingly. The ports +# are bound to the loopback interface, and should never be exposed on +# the internet. +# +SRS_FORWARD_PORT=10001 +SRS_REVERSE_PORT=10002 + +# Drop root privileges and run as another user after initialization. +# This is highly recommended as postsrsd handles untrusted input. +# +RUN_AS=postsrsd + +# Jail daemon in chroot environment +CHROOT=/var/lib/postsrsd + From 8e49f9db6d5809815612bc15ee744283a67b924a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 17:35:17 -0400 Subject: [PATCH 1039/1066] Fix after test --- data/hooks/conf_regen/19-postfix | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 2133c1bd5..a3ad70327 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -18,7 +18,7 @@ do_pre_regen() { # prepare main.cf conf file main_domain=$(cat /etc/yunohost/current_host) - domain_list=$(sudo yunohost domain list --output-as plain --quiet) + domain_list=$(sudo yunohost domain list --output-as plain --quiet | tr '\n' ' ') cat main.cf \ | sed "s/{{ main_domain }}/${main_domain}/g" \ @@ -44,7 +44,8 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || { sudo service postfix restart && sudo service postsrsd restart } + || { sudo service postfix restart && sudo service postsrsd restart; } + } FORCE=${2:-0} From 226ee1abcbfeba863b93339882fef821dd87b074 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 16 Jul 2017 18:03:12 -0400 Subject: [PATCH 1040/1066] Adding/removing a domain now affect postfix/postsrsd conf --- src/yunohost/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 913b7868e..52660bc48 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -110,7 +110,7 @@ def domain_add(auth, domain, dyndns=False): # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): - service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) except: @@ -162,7 +162,7 @@ def domain_remove(auth, domain, force=False): else: raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed')) - service_regen_conf(names=['nginx', 'metronome', 'dnsmasq']) + service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) hook_callback('post_domain_remove', args=[domain]) From 4f73f1aed2a7eeddc59192fdc9078c8105b5c323 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 28 Jul 2018 22:05:29 +0200 Subject: [PATCH 1041/1066] [fix] avoid returning None when an empty dict is expected --- src/yunohost/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5d11b75c9..5c377059e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2018,6 +2018,7 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): action_args = manifest['arguments'][action] except KeyError: logger.debug("no arguments found for '%s' in manifest", action) + return OrderedDict() else: return _parse_action_args_in_yunohost_format(args, action_args, auth) From df2033227bf3fcd2a5d23160ae85c5dd0da6c1db Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 28 Jul 2018 22:06:52 +0200 Subject: [PATCH 1042/1066] [mod] refactor, don't use try/except when not needed --- src/yunohost/app.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5c377059e..cc37051e7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2014,13 +2014,12 @@ def _parse_args_from_manifest(manifest, action, args={}, auth=None): args -- A dictionnary of arguments to parse """ - try: - action_args = manifest['arguments'][action] - except KeyError: + if action not in manifest['arguments']: logger.debug("no arguments found for '%s' in manifest", action) return OrderedDict() - else: - return _parse_action_args_in_yunohost_format(args, action_args, auth) + + action_args = manifest['arguments'][action] + return _parse_action_args_in_yunohost_format(args, action_args, auth) def _parse_args_for_action(action, args={}, auth=None): From 3bd14aa6fd1ec423e55289440a9d4221c2935fe4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:22:36 +0200 Subject: [PATCH 1043/1066] Remove old 'list of dict' style, only use 'list of 2-tuples' --- src/yunohost/certificate.py | 4 ++-- src/yunohost/log.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index eeefc7a3f..4584203e6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [{'domain': domain}], + uo = UnitOperation('letsencrypt_cert_install', [('domain': domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -385,7 +385,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [{'domain': domain}], + uo = UnitOperation('letsencrypt_cert_renew', [('domain': domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 35cd80fe2..f098e523d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -354,10 +354,7 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] if self.related_to: - if isinstance(self.related_to[0], tuple): - name += [self.related_to[0][1]] - else: - name += self.related_to[0].values() + name += [self.related_to[0][1]] self._name = '-'.join(name) return self._name From c069f8621c324aedf95ffacb611d57f8f168964d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:28:16 +0200 Subject: [PATCH 1044/1066] Since info is now always displayed, move this message to debug instead --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index f098e523d..cbde6ab9e 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -412,7 +412,7 @@ class UnitOperation(object): msg = m18n.n('log_link_to_log', name=self.name, desc=desc) else: msg = m18n.n('log_help_to_get_log', name=self.name, desc=desc) - logger.info(msg) + logger.debug(msg) else: if is_api: msg = "" + m18n.n('log_link_to_failed_log', From 934a19cea2ae9eaf93a0135aeb7edc79dbcb8ea1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 2 Aug 2018 16:30:00 +0200 Subject: [PATCH 1045/1066] Display this as info, makes more sense --- src/yunohost/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index cbde6ab9e..50d48e9a0 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -419,7 +419,7 @@ class UnitOperation(object): name=self.name, desc=desc) + "" else: msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) - logger.warning(msg) + logger.info(msg) self.flush() return msg From 05e1be86c6cbb896991284087daafdcb758d1075 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 5 Aug 2018 20:29:10 +0200 Subject: [PATCH 1046/1066] [mod] remove unused variable --- src/yunohost/domain.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 913b7868e..bea496d44 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -242,20 +242,17 @@ def domain_url_available(auth, domain, path): apps_map = app_map(raw=True) # Loop through all apps to check if path is taken by one of them - available = True if domain in apps_map: # Loop through apps for p, a in apps_map[domain].items(): if path == p: - available = False - break + return False # We also don't want conflicts with other apps starting with # same name elif path.startswith(p) or p.startswith(path): - available = False - break + return False - return available + return True def _get_maindomain(): From 363dfcd0b289a164f5f2cb3561fa5e6af9be4d74 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 5 Aug 2018 22:17:15 +0000 Subject: [PATCH 1047/1066] Alex was drunk :| --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4584203e6..741e66f1f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [('domain': domain)], + uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) uo.start() @@ -385,7 +385,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [('domain': domain)], + uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) uo.start() From 66f92449616e8c12b05b817caa484c2e7e33f31d Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Aug 2018 01:10:14 +0200 Subject: [PATCH 1048/1066] [enh] display which app conflict on domain/path not available --- locales/en.json | 2 +- src/yunohost/app.py | 16 ++++++++++------ src/yunohost/domain.py | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index edea4eed6..b4648577e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -22,7 +22,7 @@ "app_location_already_used": "The app '{app}' is already installed on that location ({path})", "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", - "app_location_unavailable": "This url is not available or conflicts with an already installed app", + "app_location_unavailable": "This url is not available or conflicts with the already installed app '{app_label:s}' ({app_id:s}) on '{domain:s}{path:s}'", "app_manifest_invalid": "Invalid app manifest: {error}", "app_no_upgrade": "No app to upgrade", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index cc37051e7..ab5809d37 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1147,7 +1147,7 @@ def app_register_url(auth, app, domain, path): # This line can't be moved on top of file, otherwise it creates an infinite # loop of import with tools.py... - from domain import domain_url_available, _normalize_domain_path + from domain import _get_conflicting_app, _normalize_domain_path domain, path = _normalize_domain_path(domain, path) @@ -1163,9 +1163,11 @@ def app_register_url(auth, app, domain, path): m18n.n('app_already_installed_cant_change_url')) # Check the url is available - if not domain_url_available(auth, domain, path): + path_and_app = _get_conflicting_app(auth, domain, path) + if not path_and_app: + path, app_id, app_label = path_and_app raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_unavailable')) + m18n.n('app_location_unavailable', app_id=app_id, app_label=app_label, path=path, domain=domain)) app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path) @@ -2050,7 +2052,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): """Parse arguments store in either manifest.json or actions.json """ from yunohost.domain import (domain_list, _get_maindomain, - domain_url_available, _normalize_domain_path) + _get_conflicting_app, _normalize_domain_path) from yunohost.user import user_info, user_list args_dict = OrderedDict() @@ -2178,9 +2180,11 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): domain, path = _normalize_domain_path(domain, path) # Check the url is available - if not domain_url_available(auth, domain, path): + path_and_app = _get_conflicting_app(auth, domain, path) + if not path_and_app: + path, app_id, app_label = path_and_app raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_unavailable')) + m18n.n('app_location_unavailable', app_id=app_id, app_label=app_label, path=path, domain=domain)) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index bea496d44..9a1f0f1cf 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -219,7 +219,7 @@ def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=Fal return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) -def domain_url_available(auth, domain, path): +def _get_conflicting_app(auth, domain, path): """ Check availability of a web path @@ -246,13 +246,25 @@ def domain_url_available(auth, domain, path): # Loop through apps for p, a in apps_map[domain].items(): if path == p: - return False + return (p, a["id"], a["label"]) # We also don't want conflicts with other apps starting with # same name elif path.startswith(p) or p.startswith(path): - return False + return (p, a["id"], a["label"]) - return True + return None + + +def domain_url_available(auth, domain, path): + """ + Check availability of a web path + + Keyword argument: + domain -- The domain for the web path (e.g. your.domain.tld) + path -- The path to check (e.g. /coffee) + """ + + return bool(_get_conflicting_app(auth, domain, path)) def _get_maindomain(): From 73f97037568bf22b73f3ff22e0b98d683963f9c8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Aug 2018 01:21:38 +0200 Subject: [PATCH 1049/1066] [enh] returns ALL conflictings applications --- locales/en.json | 2 +- src/yunohost/app.py | 38 ++++++++++++++++++++++++++------------ src/yunohost/domain.py | 13 +++++++------ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/locales/en.json b/locales/en.json index b4648577e..400ff5f1b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -22,7 +22,7 @@ "app_location_already_used": "The app '{app}' is already installed on that location ({path})", "app_make_default_location_already_used": "Can't make the app '{app}' the default on the domain {domain} is already used by the other app '{other_app}'", "app_location_install_failed": "Unable to install the app in this location because it conflit with the app '{other_app}' already installed on '{other_path}'", - "app_location_unavailable": "This url is not available or conflicts with the already installed app '{app_label:s}' ({app_id:s}) on '{domain:s}{path:s}'", + "app_location_unavailable": "This url is not available or conflicts with the already installed app(s):\n{apps:s}", "app_manifest_invalid": "Invalid app manifest: {error}", "app_no_upgrade": "No app to upgrade", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ab5809d37..8604c6c41 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1147,7 +1147,7 @@ def app_register_url(auth, app, domain, path): # This line can't be moved on top of file, otherwise it creates an infinite # loop of import with tools.py... - from domain import _get_conflicting_app, _normalize_domain_path + from domain import _get_conflicting_apps, _normalize_domain_path domain, path = _normalize_domain_path(domain, path) @@ -1163,11 +1163,18 @@ def app_register_url(auth, app, domain, path): m18n.n('app_already_installed_cant_change_url')) # Check the url is available - path_and_app = _get_conflicting_app(auth, domain, path) - if not path_and_app: - path, app_id, app_label = path_and_app - raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_unavailable', app_id=app_id, app_label=app_label, path=path, domain=domain)) + conflicts = _get_conflicting_apps(auth, domain, path) + if conflicts: + apps = [] + for path, app_id, app_label in conflicts: + apps.append(" * {domain:s}{path:s} → {app_label:s} ({app_id:s})".format( + domain=domain, + path=path, + app_id=app_id, + app_label=app_label, + )) + + raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps))) app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path) @@ -2052,7 +2059,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): """Parse arguments store in either manifest.json or actions.json """ from yunohost.domain import (domain_list, _get_maindomain, - _get_conflicting_app, _normalize_domain_path) + _get_conflicting_apps, _normalize_domain_path) from yunohost.user import user_info, user_list args_dict = OrderedDict() @@ -2180,11 +2187,18 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): domain, path = _normalize_domain_path(domain, path) # Check the url is available - path_and_app = _get_conflicting_app(auth, domain, path) - if not path_and_app: - path, app_id, app_label = path_and_app - raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_unavailable', app_id=app_id, app_label=app_label, path=path, domain=domain)) + conflicts = _get_conflicting_apps(auth, domain, path) + if conflicts: + apps = [] + for path, app_id, app_label in conflicts: + apps.append(" * {domain:s}{path:s} → {app_label:s} ({app_id:s})".format( + domain=domain, + path=path, + app_id=app_id, + app_label=app_label, + )) + + raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', "\n".join(apps=apps))) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 9a1f0f1cf..8220f9022 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -219,9 +219,9 @@ def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=Fal return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) -def _get_conflicting_app(auth, domain, path): +def _get_conflicting_apps(auth, domain, path): """ - Check availability of a web path + Return a list of all conflicting apps with a domain/path (it can be empty) Keyword argument: domain -- The domain for the web path (e.g. your.domain.tld) @@ -242,17 +242,18 @@ def _get_conflicting_app(auth, domain, path): apps_map = app_map(raw=True) # Loop through all apps to check if path is taken by one of them + conflicts = [] if domain in apps_map: # Loop through apps for p, a in apps_map[domain].items(): if path == p: - return (p, a["id"], a["label"]) + conflicts.append((p, a["id"], a["label"])) # We also don't want conflicts with other apps starting with # same name elif path.startswith(p) or p.startswith(path): - return (p, a["id"], a["label"]) + conflicts.append((p, a["id"], a["label"])) - return None + return conflicts def domain_url_available(auth, domain, path): @@ -264,7 +265,7 @@ def domain_url_available(auth, domain, path): path -- The path to check (e.g. /coffee) """ - return bool(_get_conflicting_app(auth, domain, path)) + return bool(_get_conflicting_apps(auth, domain, path)) def _get_maindomain(): From 9a73e34c9672aaae26b2deb912c8d1d50a2481ac Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 6 Aug 2018 01:22:43 +0200 Subject: [PATCH 1050/1066] [fix] test was bad, we don't want any conflicts --- src/yunohost/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 8220f9022..08d74185b 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -265,7 +265,7 @@ def domain_url_available(auth, domain, path): path -- The path to check (e.g. /coffee) """ - return bool(_get_conflicting_apps(auth, domain, path)) + return len(_get_conflicting_apps(auth, domain, path)) == 0 def _get_maindomain(): From 5e5ac0be507542a4c9cc9c2dfb6eb1c7c0887bbe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 5 Aug 2018 23:34:13 +0000 Subject: [PATCH 1051/1066] =?UTF-8?q?Clarify=20handling=20of=20'not-really?= =?UTF-8?q?-unitary'=20operations=20=C3=B4.o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/yunohost/log.py | 21 ++++++++++++++++----- src/yunohost/service.py | 4 +++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 50d48e9a0..4c3a334a5 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -353,8 +353,16 @@ class UnitOperation(object): name = [self.started_at.strftime("%Y%m%d-%H%M%S")] name += [self.operation] - if self.related_to: - name += [self.related_to[0][1]] + + if hasattr(self, "name_parameter_override"): + # This is for special cases where the operation is not really unitary + # For instance, the regen conf cannot be logged "per service" because of + # the way it's built + name.append(self.name_parameter_override) + elif self.related_to: + # We use the name of the first related thing + name.append(self.related_to[0][1]) + self._name = '-'.join(name) return self._name @@ -432,13 +440,16 @@ class UnitOperation(object): self.error(m18n.n('log_operation_unit_unclosed_properly')) def _get_description_from_name(name): - parts = name.split("-") + parts = name.split("-", 3) try: try: datetime.strptime(" ".join(parts[:2]), "%Y%m%d %H%M%S") except ValueError: - return m18n.n("log_" + parts[0], *parts[1:]) + key = "log_" + parts[0] + args = parts[1:] else: - return m18n.n("log_" + parts[2], *parts[3:]) + key = "log_" + parts[2] + args = parts[3:] + return m18n.n(key, *args) except IndexError: return name diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b5a7a5c11..9f135fe35 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -381,7 +381,9 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False if not dry_run: uo.related_to = [('service', x) for x in names] if not names: - uo.related_to = [('service', 'all')] + uo.name_parameter_override = 'all' + elif len(names) != 1: + uo.name_parameter_override = str(len(uo.related_to))+'_services' uo.start() # Clean pending conf directory From 37c55477553f75e7ebb0ee808f02be19ae6010c7 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 6 Aug 2018 02:19:23 +0200 Subject: [PATCH 1052/1066] [enh] Don't display log advertisement if not needed --- src/yunohost/certificate.py | 16 ++++++++++------ src/yunohost/utils/errors.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/yunohost/utils/errors.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 741e66f1f..ee7972249 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,6 +42,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.utils.network import get_public_ip +from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf @@ -839,13 +840,15 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain), + log_advertisement=False) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_http_not_working', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_domain_http_not_working', domain=domain), + log_advertisement=False) def _get_dns_ip(domain): @@ -854,8 +857,9 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_error_no_A_record', domain=domain)) + raise YunoHostError(m18n.n( + 'certmanager_error_no_A_record', domain=domain), + log_advertisement=False) return str(answers[0]) diff --git a/src/yunohost/utils/errors.py b/src/yunohost/utils/errors.py new file mode 100644 index 000000000..9359c605b --- /dev/null +++ b/src/yunohost/utils/errors.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" +import errno + +from moulinette.core import MoulinetteError + + +class YunoHostError(MoulinetteError): + """ + YunoHostError allows to indicate if we should or shouldn't display a message + to users about how to display logs about this error. + """ + + def __init__(self, message, log_advertisement=True): + self.log_advertisement = log_advertisement + super(YunoHostError, self).__init__(errno.EINVAL, message) From a637be20bf60ae59d483620280590369a73a2073 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Aug 2018 16:05:14 +0200 Subject: [PATCH 1053/1066] More agressive deprecation messages for app checkurl and initdb --- locales/en.json | 1 - src/yunohost/app.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index edea4eed6..25c8eda2e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,7 +14,6 @@ "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", - "app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !", "app_extraction_failed": "Unable to extract installation files", "app_id_invalid": "Invalid app id", "app_incompatible": "The app {app} is incompatible with your YunoHost version", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index cc37051e7..944a2c7e8 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1181,7 +1181,7 @@ def app_checkurl(auth, url, app=None): """ - logger.warning(m18n.n("app_checkurl_is_deprecated")) + logger.error("Packagers /!\\ : 'app checkurl' is deprecated ! Please use the helper 'ynh_webpath_register' instead !") from yunohost.domain import domain_list @@ -1238,6 +1238,9 @@ def app_initdb(user, password=None, db=None, sql=None): sql -- Initial SQL file """ + + logger.error("Packagers /!\\ : 'app initdb' is deprecated ! Please use the helper 'ynh_mysql_setup_db' instead !") + if db is None: db = user From 5e99db7c63fa8901abc558d1b1d02c756718925c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 8 Aug 2018 17:05:52 +0000 Subject: [PATCH 1054/1066] Explicitely tell when to start the uo logging to avoid displaying the message about logs when not needed --- src/yunohost/app.py | 6 ++++-- src/yunohost/certificate.py | 10 ++++++---- src/yunohost/domain.py | 11 +++++++---- src/yunohost/dyndns.py | 11 +++++------ src/yunohost/tools.py | 5 +++-- src/yunohost/user.py | 12 ++++++++---- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e443f5178..bd892fddc 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -191,8 +191,8 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) -@is_unit_operation() -def app_removelist(name): +@is_unit_operation(auto=False) +def app_removelist(uo, name): """ Remove list from the repositories @@ -206,6 +206,8 @@ def app_removelist(name): if name not in appslists.keys(): raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) + uo.start() + # Remove json json_path = '%s/%s.json' % (REPO_PATH, name) if os.path.exists(json_path): diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ee7972249..9a3bf3a34 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -162,7 +162,6 @@ def _certificate_install_selfsigned(domain_list, force=False): uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], args={'force': force}) - uo.start() # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") @@ -186,6 +185,8 @@ def _certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_replace_valid_cert', domain=domain)) + uo.start() + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -288,8 +289,6 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) - uo.start() - logger.info( "Now attempting install of certificate for domain %s!", domain) @@ -297,6 +296,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not no_checks: _check_domain_is_ready_for_ACME(domain) + uo.start() + _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain, staging) _install_cron() @@ -389,7 +390,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) - uo.start() logger.info( "Now attempting renewing of certificate for domain %s !", domain) @@ -398,6 +398,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if not no_checks: _check_domain_is_ready_for_ACME(domain) + uo.start() + _fetch_and_enable_new_certificate(domain, staging) logger.success( diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index cf4093c2a..d9da30d26 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -62,8 +62,8 @@ def domain_list(auth): return {'domains': result_list} -@is_unit_operation() -def domain_add(auth, domain, dyndns=False): +@is_unit_operation(auto=False) +def domain_add(uo, auth, domain, dyndns=False): """ Create a custom domain @@ -80,6 +80,8 @@ def domain_add(auth, domain, dyndns=False): except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) + uo.start() + # DynDNS domain if dyndns: @@ -131,8 +133,8 @@ def domain_add(auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) -@is_unit_operation() -def domain_remove(auth, domain, force=False): +@is_unit_operation(auto=False) +def domain_remove(uo, auth, domain, force=False): """ Delete domains @@ -163,6 +165,7 @@ def domain_remove(auth, domain, force=False): raise MoulinetteError(errno.EPERM, m18n.n('domain_uninstall_app_first')) + uo.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d9860c318..d4e6a99b7 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -127,7 +127,6 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= if domain is None: domain = _get_maindomain() uo.related_to.append(('domain', domain)) - uo.start() # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): @@ -140,6 +139,8 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= raise MoulinetteError(errno.ENOENT, m18n.n('dyndns_unavailable', domain=domain)) + uo.start() + if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: if not os.path.exists('/etc/yunohost/dyndns'): @@ -217,17 +218,12 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, return else: logger.info("Updated needed, going on...") - if domain is not None: - uo.related_to.append(('domain', domain)) # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) - uo.related_to.append(('domain', domain)) - uo.start() # If key is not given, pick the first file we find with the domain given else: - uo.start() if key is None: keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) @@ -236,6 +232,9 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] + uo.related_to.append(('domain', domain)) + uo.start() + # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. # The actual update will be done in next run. diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index db4946c38..f02ba0414 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -249,8 +249,8 @@ def _is_inside_container(): return out.split()[1] != "(1," -@is_unit_operation() -def tools_postinstall(domain, password, ignore_dyndns=False): +@is_unit_operation(auto=False) +def tools_postinstall(uo, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -299,6 +299,7 @@ def tools_postinstall(domain, password, ignore_dyndns=False): else: dyndns = False + uo.start() logger.info(m18n.n('yunohost_installing')) service_regen_conf(['nslcd', 'nsswitch'], force=True) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 8efabd9bb..4b83b3a3e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,8 +99,8 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user') -def user_create(auth, username, firstname, lastname, mail, password, +@is_unit_operation('username:user', auto=False) +def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user @@ -136,6 +136,8 @@ def user_create(auth, username, firstname, lastname, mail, password, m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) + uo.start() + # Get random UID/GID all_uid = {x.pw_uid for x in pwd.getpwall()} all_gid = {x.pw_gid for x in pwd.getpwall()} @@ -257,8 +259,8 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password') -def user_update(auth, username, firstname=None, lastname=None, mail=None, +@is_unit_operation('username:user', exclude='auth,change_password', auto=False) +def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ @@ -359,6 +361,8 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota + uo.start() + if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) app_ssowatconf(auth) From 77b7f96b331a09a839ae9ae83c3fe3c2e99a5ba0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 9 Aug 2018 00:24:31 +0000 Subject: [PATCH 1055/1066] Remove 'auto' option, essentially irrelevant if we want to properly manage errors --- src/yunohost/app.py | 10 +++++----- src/yunohost/backup.py | 2 +- src/yunohost/certificate.py | 2 +- src/yunohost/domain.py | 4 ++-- src/yunohost/dyndns.py | 4 ++-- src/yunohost/log.py | 14 ++++---------- src/yunohost/service.py | 5 +++-- src/yunohost/tools.py | 10 +++++----- src/yunohost/user.py | 7 ++++--- 9 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bd892fddc..fa106d689 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -191,7 +191,7 @@ def app_fetchlist(url=None, name=None): _write_appslist_list(appslists) -@is_unit_operation(auto=False) +@is_unit_operation() def app_removelist(uo, name): """ Remove list from the repositories @@ -429,7 +429,7 @@ def app_map(app=None, raw=False, user=None): return result -@is_unit_operation(auto=False) +@is_unit_operation() def app_change_url(uo, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -681,7 +681,7 @@ def app_upgrade(auth, app=[], url=None, file=None): return {"log": service_log('yunohost-api', number="100").values()[0]} -@is_unit_operation(auto=False) +@is_unit_operation() def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -862,7 +862,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False hook_callback('post_app_install', args=args_list, env=env_dict) -@is_unit_operation(auto=False) +@is_unit_operation() def app_remove(uo, auth, app): """ Remove app @@ -1103,7 +1103,7 @@ def app_debug(app): } -@is_unit_operation(auto=False) +@is_unit_operation() def app_makedefault(uo, auth, app, domain=None): """ Redirect domain root to an app diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 3880f94cc..eb2a91cab 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,7 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf -from yunohost.log import UnitOperation, is_unit_operation +from yunohost.log import UnitOperation BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9a3bf3a34..4e40fe0f5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -47,7 +47,7 @@ from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import UnitOperation logger = getActionLogger('yunohost.certmanager') diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index d9da30d26..a42b99a89 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -62,7 +62,7 @@ def domain_list(auth): return {'domains': result_list} -@is_unit_operation(auto=False) +@is_unit_operation() def domain_add(uo, auth, domain, dyndns=False): """ Create a custom domain @@ -133,7 +133,7 @@ def domain_add(uo, auth, domain, dyndns=False): logger.success(m18n.n('domain_created')) -@is_unit_operation(auto=False) +@is_unit_operation() def domain_remove(uo, auth, domain, force=False): """ Delete domains diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d4e6a99b7..263e2a123 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -113,7 +113,7 @@ def _dyndns_available(provider, domain): return r == u"Domain %s is available" % domain -@is_unit_operation(auto=False) +@is_unit_operation() def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -175,7 +175,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= dyndns_installcron() -@is_unit_operation(auto=False) +@is_unit_operation() def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 4c3a334a5..735ce75a3 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -197,7 +197,7 @@ def log_display(path, number=50, share=False): return infos -def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None, auto=True): +def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None): """ Configure quickly a unit operation @@ -218,9 +218,6 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password well formed description you should add a translation key like this "log_" + operation_key in locales files. - auto If true, start the recording. If False, the unit operation object - created is given to the decorated function as the first argument and you can - start recording at the good time. """ def decorate(func): def func_wrapper(*args, **kwargs): @@ -266,13 +263,10 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) - # Start to record or give the unit operation in argument to let the - # developper start the record itself - if auto: - uo.start() try: - if not auto: - args = (uo,) + args + # Start the actual function, and give the unit operation + # in argument to let the developper start the record itself + args = (uo,) + args result = func(*args, **kwargs) except Exception as e: uo.error(e) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 9f135fe35..71b554785 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -152,7 +152,7 @@ def service_stop(names): logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() -def service_enable(names): +def service_enable(uo, names): """ Enable one or more services @@ -160,6 +160,7 @@ def service_enable(names): names -- Services name to enable """ + uo.start() if isinstance(names, str): names = [names] for name in names: @@ -344,7 +345,7 @@ def service_log(name, number=50): return result -@is_unit_operation('names:service', auto=False) +@is_unit_operation('names:service') def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f02ba0414..1e87d98e5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -139,7 +139,7 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -@is_unit_operation(auto=False) +@is_unit_operation() def tools_maindomain(uo, auth, new_domain=None): """ Check the current main domain, or change it @@ -249,7 +249,7 @@ def _is_inside_container(): return out.split()[1] != "(1," -@is_unit_operation(auto=False) +@is_unit_operation() def tools_postinstall(uo, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -473,7 +473,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): return {'packages': packages, 'apps': apps} -@is_unit_operation(auto=False) +@is_unit_operation() def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -713,7 +713,7 @@ def tools_port_available(port): return False -@is_unit_operation(auto=False) +@is_unit_operation() def tools_shutdown(uo, force=False): shutdown = force if not shutdown: @@ -732,7 +732,7 @@ def tools_shutdown(uo, force=False): subprocess.check_call(['systemctl', 'poweroff']) -@is_unit_operation(auto=False) +@is_unit_operation() def tools_reboot(uo, force=False): reboot = force if not reboot: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 4b83b3a3e..e92256994 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user', auto=False) +@is_unit_operation('username:user') def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -224,7 +224,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, @is_unit_operation('username:user') -def user_delete(auth, username, purge=False): +def user_delete(uo, auth, username, purge=False): """ Delete user @@ -236,6 +236,7 @@ def user_delete(auth, username, purge=False): from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback + uo.start() if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) @@ -259,7 +260,7 @@ def user_delete(auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password', auto=False) +@is_unit_operation('username:user', exclude='auth,change_password') def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From 625e87000867affe188b616346b54ae7a9753630 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 10 Aug 2018 00:52:46 +0200 Subject: [PATCH 1056/1066] Revert "[enh] Don't display log advertisement if not needed" This reverts commit 37c55477553f75e7ebb0ee808f02be19ae6010c7. --- src/yunohost/certificate.py | 16 ++++++---------- src/yunohost/utils/errors.py | 34 ---------------------------------- 2 files changed, 6 insertions(+), 44 deletions(-) delete mode 100644 src/yunohost/utils/errors.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4e40fe0f5..d530568dc 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,7 +42,6 @@ from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.utils.network import get_public_ip -from yunohost.utils.errors import YunoHostError from moulinette import m18n from yunohost.app import app_ssowatconf @@ -842,15 +841,13 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise YunoHostError(m18n.n( - 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise YunoHostError(m18n.n( - 'certmanager_domain_http_not_working', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_http_not_working', domain=domain)) def _get_dns_ip(domain): @@ -859,9 +856,8 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise YunoHostError(m18n.n( - 'certmanager_error_no_A_record', domain=domain), - log_advertisement=False) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_error_no_A_record', domain=domain)) return str(answers[0]) diff --git a/src/yunohost/utils/errors.py b/src/yunohost/utils/errors.py deleted file mode 100644 index 9359c605b..000000000 --- a/src/yunohost/utils/errors.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- - -""" License - - Copyright (C) 2018 YUNOHOST.ORG - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program; if not, see http://www.gnu.org/licenses - -""" -import errno - -from moulinette.core import MoulinetteError - - -class YunoHostError(MoulinetteError): - """ - YunoHostError allows to indicate if we should or shouldn't display a message - to users about how to display logs about this error. - """ - - def __init__(self, message, log_advertisement=True): - self.log_advertisement = log_advertisement - super(YunoHostError, self).__init__(errno.EINVAL, message) From 6c18e51b823ebd986e6e9c7484f340ddac06570a Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 10 Aug 2018 15:40:00 +0200 Subject: [PATCH 1057/1066] [enh] Replace string strange list --- src/yunohost/log.py | 46 +++++++++++++++++++++-------------------- src/yunohost/service.py | 2 +- src/yunohost/user.py | 6 +++--- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 735ce75a3..707a0b356 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -197,42 +197,41 @@ def log_display(path, number=50, share=False): return infos -def is_unit_operation(entities='app,domain,service,user', exclude='auth,password', operation_key=None): +def is_unit_operation(entities=['app', 'domain', 'service', 'user'], + exclude=['auth', 'password'], operation_key=None): """ Configure quickly a unit operation This decorator help you to configure quickly the record of a unit operations. Argument: - entities A list seperated by coma of entity types related to the unit - operation. The entity type is searched inside argument's names of the - decorated function. If something match, the argument value is added as - related entity. + entities A list of entity types related to the unit operation. The entity + type is searched inside argument's names of the decorated function. If + something match, the argument value is added as related entity. If the + argument name is different you can specify it with a tuple + (argname, entity_type) instead of just put the entity type. exclude Remove some arguments from the context. By default, arguments called 'password' and 'auth' are removed. If an argument is an object, you need to exclude it or create manually the unit operation without this decorator. - operation_key Key describing the unit operation. If you want to display a - well formed description you should add a translation key like this - "log_" + operation_key in locales files. + operation_key A key to describe the unit operation log used to create the + filename and search a translation. Please ensure that this key prefixed by + 'log_' is present in locales/en.json otherwise it won't be translatable. """ def decorate(func): def func_wrapper(*args, **kwargs): - # For a strange reason we can't use directly the arguments from - # is_unit_operation function. We need to store them in a var before. - entities_list = entities.split(',') - exclude_list = exclude.split(',') op_key = operation_key - related_to = [] - if op_key is None: op_key = func.__name__ - # In case the function is called directly from an other part of the - # code + # If the function is called directly from an other part of the code + # and not by the moulinette framework, we need to complete kwargs + # dictionnary with the args list. + # Indeed, we use convention naming in this decorator and we need to + # know name of each args (so we need to use kwargs instead of args) if len(args) > 0: from inspect import getargspec keys = getargspec(func).args @@ -243,10 +242,13 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password args = () # Search related entity in arguments of the decorated function - for entity in entities_list: - entity = entity.split(':') - entity_type = entity[-1] - entity = entity[0] + related_to = [] + for entity in entities: + if isinstance(entity, tuple): + entity_type = entity[1] + entity = entity[0] + else: + entity_type = entity if entity in kwargs and kwargs[entity] is not None: if isinstance(kwargs[entity], basestring): @@ -258,13 +260,13 @@ def is_unit_operation(entities='app,domain,service,user', exclude='auth,password context = kwargs.copy() # Exclude unappropriate data from the context - for field in exclude_list: + for field in exclude: if field in context: context.pop(field, None) uo = UnitOperation(op_key, related_to, args=context) try: - # Start the actual function, and give the unit operation + # Start the actual function, and give the unit operation # in argument to let the developper start the record itself args = (uo,) + args result = func(*args, **kwargs) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 71b554785..ba9c4450e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -345,7 +345,7 @@ def service_log(name, number=50): return result -@is_unit_operation('names:service') +@is_unit_operation([('names', 'service')]) def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ diff --git a/src/yunohost/user.py b/src/yunohost/user.py index e92256994..9fb3f184a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): return {'users': users} -@is_unit_operation('username:user') +@is_unit_operation([('username', 'user')]) def user_create(uo, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ @@ -223,7 +223,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, raise MoulinetteError(169, m18n.n('user_creation_failed')) -@is_unit_operation('username:user') +@is_unit_operation([('username', 'user')]) def user_delete(uo, auth, username, purge=False): """ Delete user @@ -260,7 +260,7 @@ def user_delete(uo, auth, username, purge=False): logger.success(m18n.n('user_deleted')) -@is_unit_operation('username:user', exclude='auth,change_password') +@is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): From a43b3dfa18529f698a285073d624ba42e759ddbf Mon Sep 17 00:00:00 2001 From: ljf Date: Fri, 10 Aug 2018 15:59:36 +0200 Subject: [PATCH 1058/1066] [fix] Pep8 --- src/yunohost/log.py | 51 +++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 707a0b356..ecf0e5e9e 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -40,7 +40,7 @@ from moulinette.utils.filesystem import read_file CATEGORIES_PATH = '/var/log/yunohost/categories/' OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' -CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', \ +CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', 'app'] METADATA_FILE_EXT = '.yml' LOG_FILE_EXT = '.log' @@ -48,6 +48,7 @@ RELATED_CATEGORIES = ['app', 'domain', 'service', 'user'] logger = getActionLogger('yunohost.log') + def log_list(category=[], limit=None): """ List available logs @@ -73,7 +74,8 @@ def log_list(category=[], limit=None): continue - logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), os.listdir(category_path)) + logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), + os.listdir(category_path)) logs = reversed(sorted(logs)) if limit is not None: @@ -93,7 +95,8 @@ def log_list(category=[], limit=None): } entry["description"] = _get_description_from_name(base_filename) try: - log_datetime = datetime.strptime(" ".join(log[:2]), "%Y%m%d %H%M%S") + log_datetime = datetime.strptime(" ".join(log[:2]), + "%Y%m%d %H%M%S") except ValueError: pass else: @@ -101,7 +104,8 @@ def log_list(category=[], limit=None): result[category].append(entry) - # Reverse the order of log when in cli, more comfortable to read (avoid unecessary scrolling) + # Reverse the order of log when in cli, more comfortable to read (avoid + # unecessary scrolling) if not is_api: for category in result: result[category] = list(reversed(result[category])) @@ -131,7 +135,7 @@ def log_display(path, number=50, share=False): if os.path.exists(abs_path) or os.path.exists(abs_path + METADATA_FILE_EXT): break - if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT) : + if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT): log_path = abs_path if abs_path.endswith(METADATA_FILE_EXT) or abs_path.endswith(LOG_FILE_EXT): @@ -167,7 +171,7 @@ def log_display(path, number=50, share=False): logger.info(m18n.n("log_available_on_yunopaste", url=url)) if msettings.get('interface') == 'api': - return { "url" : url } + return {"url": url} else: return @@ -191,27 +195,27 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): from yunohost.service import _tail logs = _tail(log_path, int(number)) - #logs = [{"datetime": x.split(": ", 1)[0].replace("_", " "), "line": x.split(": ", 1)[1]} for x in logs if x] infos['log_path'] = log_path infos['logs'] = logs return infos + def is_unit_operation(entities=['app', 'domain', 'service', 'user'], exclude=['auth', 'password'], operation_key=None): """ Configure quickly a unit operation - This decorator help you to configure quickly the record of a unit operations. + This decorator help you to configure the record of a unit operations. Argument: - entities A list of entity types related to the unit operation. The entity + entities A list of entity types related to the unit operation. The entity type is searched inside argument's names of the decorated function. If something match, the argument value is added as related entity. If the argument name is different you can specify it with a tuple (argname, entity_type) instead of just put the entity type. - exclude Remove some arguments from the context. By default, arguments + exclude Remove some arguments from the context. By default, arguments called 'password' and 'auth' are removed. If an argument is an object, you need to exclude it or create manually the unit operation without this decorator. @@ -279,14 +283,15 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], return func_wrapper return decorate + class UnitOperation(object): """ - Instances of this class represents unit operation the yunohost admin as done. + Instances of this class represents unit operation done on the ynh instance. - Each time an action of the yunohost cli/api change the system, one or several - unit operations should be registered. + Each time an action of the yunohost cli/api change the system, one or + several unit operations should be registered. - This class record logs and some metadata like context or start time/end time. + This class record logs and metadata like context or start time/end time. """ def __init__(self, operation, related_to=None, **kwargs): @@ -351,9 +356,9 @@ class UnitOperation(object): name += [self.operation] if hasattr(self, "name_parameter_override"): - # This is for special cases where the operation is not really unitary - # For instance, the regen conf cannot be logged "per service" because of - # the way it's built + # This is for special cases where the operation is not really + # unitary. For instance, the regen conf cannot be logged "per + # service" because of the way it's built name.append(self.name_parameter_override) elif self.related_to: # We use the name of the first related thing @@ -379,7 +384,7 @@ class UnitOperation(object): data['success'] = self._success if self.error is not None: data['error'] = self._error - # TODO: detect if 'extra' erase some key of 'data' + # TODO: detect if 'extra' erase some key of 'data' data.update(self.extra) return data @@ -420,9 +425,10 @@ class UnitOperation(object): else: if is_api: msg = "" + m18n.n('log_link_to_failed_log', - name=self.name, desc=desc) + "" + name=self.name, desc=desc) + "" else: - msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) + msg = m18n.n('log_help_to_get_failed_log', name=self.name, + desc=desc) logger.info(msg) self.flush() return msg @@ -435,7 +441,12 @@ class UnitOperation(object): """ self.error(m18n.n('log_operation_unit_unclosed_properly')) + def _get_description_from_name(name): + """ + Return the translated description from the filename + """ + parts = name.split("-", 3) try: try: From cc33e493f1a46f10b686752efd8047f79babfa87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 15 Aug 2018 19:36:19 +0000 Subject: [PATCH 1059/1066] Update changelog for 3.1.0 --- debian/changelog | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/debian/changelog b/debian/changelog index a47b0b42d..3cf70252d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,31 @@ +yunohost (3.1.0) stable; urgency=low + + Highlights + ========== + + * Add MUA autoconfiguration (e.g. for Thunderbird) (#495) + * Experimental : Configuration panel for applications (#488) + * Experimental : Allow applications to ship custom actions (#486, #505) + + Other fixes / improvements + ========================== + + * Fix an issue with mail permission after restoring them (#496) + * Optimize imports in certificate.py (#497) + * Add timeout to get_public_ip so that 'dyndns update' don't get stuck (#502) + * Use human-friendly choices for booleans during apps installations (#498) + * Fix the way we detect we're inside a container (#508) + * List existing users during app install if the app ask for a user (#506) + * Allow apps to tell they don't want to be displayed in the SSO (#507) + * After postinstall, advice the admin to create a first user (#510) + * Disable checks in acme_tiny lib is --no-checks is used (#509) + * Better UX in case of url conflicts when installing app (#512) + * Misc fixes / improvements + + Thanks to all contributors : pitchum, ljf, Bram, Josue, Aleks ! + + -- Alexandre Aubin Wed, 15 Aug 2018 21:34:00 +0000 + yunohost (3.0.0.1) stable; urgency=low * Fix remaining use of --verbose and --ignore-system during backup/restore From b09d71f40ba5450dfa9f7c27c8c198c87a45d052 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 22 Aug 2018 16:26:44 +0000 Subject: [PATCH 1060/1066] Add timeout to POST request --- src/yunohost/utils/yunopaste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index c084d78ce..6df4d0ca6 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -11,7 +11,7 @@ def yunopaste(data): paste_server = "https://paste.yunohost.org" try: - r = requests.post("%s/documents" % paste_server, data=data) + r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: raise MoulinetteError(errno.EIO, "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) From 93cb07ed61f17a1debbe353963120ab117598f3f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 22 Aug 2018 16:27:11 +0000 Subject: [PATCH 1061/1066] Add status code to error message --- src/yunohost/utils/yunopaste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 6df4d0ca6..2b53062d1 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -18,7 +18,7 @@ def yunopaste(data): if r.status_code != 200: raise MoulinetteError(errno.EIO, - "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % r.text) + "Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) try: url = json.loads(r.text)["key"] From 860f7408103dab24bd89d5428224490176c17d92 Mon Sep 17 00:00:00 2001 From: Bram Date: Wed, 22 Aug 2018 19:30:58 +0200 Subject: [PATCH 1062/1066] [fix] bad call --- src/yunohost/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2c737da82..e3e5bb959 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2201,7 +2201,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): app_label=app_label, )) - raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', "\n".join(apps=apps))) + raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps))) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) From 775a16d883694293f17bd2a17ecb212510a22c9e Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 23 Aug 2018 18:59:44 +0200 Subject: [PATCH 1063/1066] [enh] Rename uo in operation_logger --- src/yunohost/app.py | 92 ++++++++++++++++++------------------- src/yunohost/backup.py | 34 +++++++------- src/yunohost/certificate.py | 26 +++++------ src/yunohost/domain.py | 8 ++-- src/yunohost/dyndns.py | 12 ++--- src/yunohost/log.py | 14 +++--- src/yunohost/service.py | 20 ++++---- src/yunohost/tools.py | 40 ++++++++-------- src/yunohost/user.py | 12 ++--- 9 files changed, 129 insertions(+), 129 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0c35fa810..86abac7b3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -44,7 +44,7 @@ from moulinette.utils.filesystem import read_json from yunohost.service import service_log, _run_service_command from yunohost.utils import packages -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import is_unit_operation, OperationLogger logger = getActionLogger('yunohost.app') @@ -110,13 +110,13 @@ def app_fetchlist(url=None, name=None): # the fetch only this list if url is not None: if name: - uo = UnitOperation('app_fetchlist') - uo.start() + operation_logger = OperationLogger('app_fetchlist') + operation_logger.start() _register_new_appslist(url, name) # Refresh the appslists dict appslists = _read_appslist_list() appslists_to_be_fetched = [name] - uo.success() + operation_logger.success() else: raise MoulinetteError(errno.EINVAL, m18n.n('custom_appslist_name_required')) @@ -193,7 +193,7 @@ def app_fetchlist(url=None, name=None): @is_unit_operation() -def app_removelist(uo, name): +def app_removelist(operation_logger, name): """ Remove list from the repositories @@ -207,7 +207,7 @@ def app_removelist(uo, name): if name not in appslists.keys(): raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) - uo.start() + operation_logger.start() # Remove json json_path = '%s/%s.json' % (REPO_PATH, name) @@ -433,7 +433,7 @@ def app_map(app=None, raw=False, user=None): @is_unit_operation() -def app_change_url(uo, auth, app, domain, path): +def app_change_url(operation_logger, auth, app, domain, path): """ Modify the URL at which an application is installed. @@ -491,9 +491,9 @@ def app_change_url(uo, auth, app, domain, path): env_dict["YNH_APP_NEW_PATH"] = path.rstrip("/") if domain != old_domain: - uo.related_to.append(('domain', old_domain)) - uo.extra.update({'env': env_dict}) - uo.start() + operation_logger.related_to.append(('domain', old_domain)) + operation_logger.extra.update({'env': env_dict}) + operation_logger.start() if os.path.exists(os.path.join(APP_TMP_FOLDER, "scripts")): shutil.rmtree(os.path.join(APP_TMP_FOLDER, "scripts")) @@ -515,7 +515,7 @@ def app_change_url(uo, auth, app, domain, path): if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), args=args_list, env=env_dict, user="root") != 0: msg = "Failed to change '%s' url." % app logger.error(msg) - uo.error(msg) + operation_logger.error(msg) # restore values modified by app_checkurl # see begining of the function @@ -630,8 +630,8 @@ def app_upgrade(auth, app=[], url=None, file=None): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('app_upgrade', related_to, env=env_dict) - uo.start() + operation_logger = OperationLogger('app_upgrade', related_to, env=env_dict) + operation_logger.start() # Apply dirty patch to make php5 apps compatible with php7 _patch_php5(extracted_app_folder) @@ -641,7 +641,7 @@ def app_upgrade(auth, app=[], url=None, file=None): if hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict, user="root") != 0: msg = m18n.n('app_upgrade_failed', app=app_instance_name) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) else: now = int(time.time()) # TODO: Move install_time away from app_setting @@ -671,7 +671,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.success(m18n.n('app_upgraded', app=app_instance_name)) hook_callback('post_app_upgrade', args=args_list, env=env_dict) - uo.success() + operation_logger.success() if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -686,7 +686,7 @@ def app_upgrade(auth, app=[], url=None, file=None): @is_unit_operation() -def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False): +def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False): """ Install apps @@ -698,7 +698,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback - from yunohost.log import UnitOperation + from yunohost.log import OperationLogger # Fetch or extract sources @@ -758,10 +758,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Start register change on system - uo.extra.update({'env':env_dict}) - uo.related_to = [s for s in uo.related_to if s[0] != "app"] - uo.related_to.append(("app", app_id)) - uo.start() + operation_logger.extra.update({'env':env_dict}) + operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"] + operation_logger.related_to.append(("app", app_id)) + operation_logger.start() # Create app directory app_setting_path = os.path.join(APPS_SETTING_PATH, app_instance_name) @@ -806,7 +806,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False logger.exception(m18n.n('unexpected_error')) finally: if install_retcode != 0: - error_msg = uo.error(m18n.n('unexpected_error')) + error_msg = operation_logger.error(m18n.n('unexpected_error')) if not no_remove_on_failure: # Setup environment for remove script env_dict_remove = {} @@ -815,10 +815,10 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Execute remove script - uo_remove = UnitOperation('remove_on_failed_install', + operation_logger_remove = OperationLogger('remove_on_failed_install', [('app', app_instance_name)], env=env_dict_remove) - uo_remove.start() + operation_logger_remove.start() remove_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/remove'), @@ -828,9 +828,9 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) - uo_remove.error(msg) + operation_logger_remove.error(msg) else: - uo_remove.success() + operation_logger_remove.success() # Clean tmp folders shutil.rmtree(app_setting_path) @@ -868,7 +868,7 @@ def app_install(uo, auth, app, label=None, args=None, no_remove_on_failure=False @is_unit_operation() -def app_remove(uo, auth, app): +def app_remove(operation_logger, auth, app): """ Remove app @@ -881,7 +881,7 @@ def app_remove(uo, auth, app): raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app=app)) - uo.start() + operation_logger.start() app_setting_path = APPS_SETTING_PATH + app @@ -906,8 +906,8 @@ def app_remove(uo, auth, app): env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - uo.extra.update({'env': env_dict}) - uo.flush() + operation_logger.extra.update({'env': env_dict}) + operation_logger.flush() if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0: logger.success(m18n.n('app_removed', app=app)) @@ -957,8 +957,8 @@ def app_addaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_addaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_addaccess', related_to) + operation_logger.start() allowed_users = set() if 'allowed_users' in app_settings: @@ -972,14 +972,14 @@ def app_addaccess(auth, apps, users=[]): logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) - uo.related_to.append(('user', allowed_user)) + operation_logger.related_to.append(('user', allowed_user)) - uo.flush() + operation_logger.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) - uo.success() + operation_logger.success() result[app] = allowed_users @@ -1020,8 +1020,8 @@ def app_removeaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_removeaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_removeaccess', related_to) + operation_logger.start() if remove_all: pass @@ -1034,15 +1034,15 @@ def app_removeaccess(auth, apps, users=[]): if allowed_user not in users: allowed_users.append(allowed_user) - uo.related_to += [ ('user', x) for x in allowed_users ] - uo.flush() + operation_logger.related_to += [ ('user', x) for x in allowed_users ] + operation_logger.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) result[app] = allowed_users - uo.success() + operation_logger.success() app_ssowatconf(auth) @@ -1069,8 +1069,8 @@ def app_clearaccess(auth, apps): # Start register change on system related_to = [('app', app)] - uo= UnitOperation('app_clearaccess', related_to) - uo.start() + operation_logger= OperationLogger('app_clearaccess', related_to) + operation_logger.start() if 'mode' in app_settings: app_setting(app, 'mode', delete=True) @@ -1080,7 +1080,7 @@ def app_clearaccess(auth, apps): hook_callback('post_app_clearaccess', args=[app]) - uo.success() + operation_logger.success() app_ssowatconf(auth) @@ -1109,7 +1109,7 @@ def app_debug(app): @is_unit_operation() -def app_makedefault(uo, auth, app, domain=None): +def app_makedefault(operation_logger, auth, app, domain=None): """ Redirect domain root to an app @@ -1126,11 +1126,11 @@ def app_makedefault(uo, auth, app, domain=None): if domain is None: domain = app_domain - uo.related_to.append(('domain',domain)) + operation_logger.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.start() + operation_logger.start() if '/' in app_map(raw=True)[domain]: raise MoulinetteError(errno.EEXIST, m18n.n('app_make_default_location_already_used', diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index eb2a91cab..88959cc2f 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -51,7 +51,7 @@ from yunohost.hook import ( from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf -from yunohost.log import UnitOperation +from yunohost.log import OperationLogger BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH @@ -1174,14 +1174,14 @@ class RestoreManager(): return # Start register change on system - uo = UnitOperation('backup_restore_system') - uo.start() + operation_logger = OperationLogger('backup_restore_system') + operation_logger.start() logger.debug(m18n.n('restore_running_hooks')) env_dict = self._get_env_var() - uo.extra['env'] = env_dict - uo.flush() + operation_logger.extra['env'] = env_dict + operation_logger.flush() ret = hook_callback('restore', system_targets, args=[self.work_dir], @@ -1198,9 +1198,9 @@ class RestoreManager(): error_part.append(part) if ret['failed']: - uo.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) + operation_logger.error(m18n.n('restore_system_part_failed', part=', '.join(error_part))) else: - uo.success() + operation_logger.success() service_regen_conf() @@ -1250,8 +1250,8 @@ class RestoreManager(): # Start register change on system related_to = [('app', app_instance_name)] - uo = UnitOperation('backup_restore_app', related_to) - uo.start() + operation_logger = OperationLogger('backup_restore_app', related_to) + operation_logger.start() # Check if the app is not already installed if _is_installed(app_instance_name): @@ -1302,8 +1302,8 @@ class RestoreManager(): # Prepare env. var. to pass to script env_dict = self._get_env_var(app_instance_name) - uo.extra['env'] = env_dict - uo.flush() + operation_logger.extra['env'] = env_dict + operation_logger.flush() # Execute app restore script hook_exec(restore_script, @@ -1315,7 +1315,7 @@ class RestoreManager(): except: msg = m18n.n('restore_app_failed',app=app_instance_name) logger.exception(msg) - uo.error(msg) + operation_logger.error(msg) self.targets.set_result("apps", app_instance_name, "Error") @@ -1328,10 +1328,10 @@ class RestoreManager(): env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) - uo = UnitOperation('remove_on_failed_restore', + operation_logger = OperationLogger('remove_on_failed_restore', [('app', app_instance_name)], env=env_dict_remove) - uo.start() + operation_logger.start() # Execute remove script # TODO: call app_remove instead @@ -1339,9 +1339,9 @@ class RestoreManager(): env=env_dict_remove, user="root") != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) - uo.error(msg) + operation_logger.error(msg) else: - uo.success() + operation_logger.success() # Cleaning app directory shutil.rmtree(app_settings_new_path, ignore_errors=True) @@ -1349,7 +1349,7 @@ class RestoreManager(): # TODO Cleaning app hooks else: self.targets.set_result("apps", app_instance_name, "Success") - uo.success() + operation_logger.success() finally: # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 091c20d1a..1b80b6b49 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -45,7 +45,7 @@ from yunohost.utils.network import get_public_ip from moulinette import m18n from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command, service_regen_conf -from yunohost.log import UnitOperation +from yunohost.log import OperationLogger logger = getActionLogger('yunohost.certmanager') @@ -160,7 +160,7 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: - uo = UnitOperation('selfsigned_cert_install', [('domain', domain)], + operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)], args={'force': force}) # Paths of files and folder we'll need @@ -185,7 +185,7 @@ def _certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_replace_valid_cert', domain=domain)) - uo.start() + operation_logger.start() # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -243,11 +243,11 @@ def _certificate_install_selfsigned(domain_list, force=False): if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success( m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) - uo.success() + operation_logger.success() else: msg = "Installation of self-signed certificate installation for %s failed !" % (domain) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -288,7 +288,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F # Actual install steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_install', [('domain', domain)], + operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging}) logger.info( @@ -298,7 +298,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not no_checks: _check_domain_is_ready_for_ACME(domain) - uo.start() + operation_logger.start() _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) @@ -307,12 +307,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.success( m18n.n("certmanager_cert_install_success", domain=domain)) - uo.success() + operation_logger.success() except Exception as e: _display_debug_information(domain) msg = "Certificate installation for %s failed !\nException: %s" % (domain, e) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ @@ -391,7 +391,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Actual renew steps for domain in domain_list: - uo = UnitOperation('letsencrypt_cert_renew', [('domain', domain)], + operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)], args={'force': force, 'no_checks': no_checks, 'staging': staging, 'email': email}) @@ -402,14 +402,14 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if not no_checks: _check_domain_is_ready_for_ACME(domain) - uo.start() + operation_logger.start() _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) logger.success( m18n.n("certmanager_cert_renew_success", domain=domain)) - uo.success() + operation_logger.success() except Exception as e: import traceback @@ -418,7 +418,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal traceback.print_exc(file=stack) msg = "Certificate renewing for %s failed !" % (domain) logger.error(msg) - uo.error(msg) + operation_logger.error(msg) logger.error(stack.getvalue()) logger.error(str(e)) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 0881619ee..ddb046569 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -63,7 +63,7 @@ def domain_list(auth): @is_unit_operation() -def domain_add(uo, auth, domain, dyndns=False): +def domain_add(operation_logger, auth, domain, dyndns=False): """ Create a custom domain @@ -80,7 +80,7 @@ def domain_add(uo, auth, domain, dyndns=False): except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) - uo.start() + operation_logger.start() # DynDNS domain if dyndns: @@ -134,7 +134,7 @@ def domain_add(uo, auth, domain, dyndns=False): @is_unit_operation() -def domain_remove(uo, auth, domain, force=False): +def domain_remove(operation_logger, auth, domain, force=False): """ Delete domains @@ -165,7 +165,7 @@ def domain_remove(uo, auth, domain, force=False): raise MoulinetteError(errno.EPERM, m18n.n('domain_uninstall_app_first')) - uo.start() + operation_logger.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 263e2a123..88547b4db 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -114,7 +114,7 @@ def _dyndns_available(provider, domain): @is_unit_operation() -def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key=None): +def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", domain=None, key=None): """ Subscribe to a DynDNS service @@ -126,7 +126,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= """ if domain is None: domain = _get_maindomain() - uo.related_to.append(('domain', domain)) + operation_logger.related_to.append(('domain', domain)) # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): @@ -139,7 +139,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= raise MoulinetteError(errno.ENOENT, m18n.n('dyndns_unavailable', domain=domain)) - uo.start() + operation_logger.start() if key is None: if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: @@ -176,7 +176,7 @@ def dyndns_subscribe(uo, subscribe_host="dyndns.yunohost.org", domain=None, key= @is_unit_operation() -def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, +def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key=None, ipv4=None, ipv6=None): """ Update IP on DynDNS platform @@ -232,8 +232,8 @@ def dyndns_update(uo, dyn_host="dyndns.yunohost.org", domain=None, key=None, key = keys[0] - uo.related_to.append(('domain', domain)) - uo.start() + operation_logger.related_to.append(('domain', domain)) + operation_logger.start() # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. diff --git a/src/yunohost/log.py b/src/yunohost/log.py index ecf0e5e9e..c105b8279 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -239,8 +239,8 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], if len(args) > 0: from inspect import getargspec keys = getargspec(func).args - if 'uo' in keys: - keys.remove('uo') + if 'operation_logger' in keys: + keys.remove('operation_logger') for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -267,24 +267,24 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], for field in exclude: if field in context: context.pop(field, None) - uo = UnitOperation(op_key, related_to, args=context) + operation_logger = OperationLogger(op_key, related_to, args=context) try: # Start the actual function, and give the unit operation # in argument to let the developper start the record itself - args = (uo,) + args + args = (operation_logger,) + args result = func(*args, **kwargs) except Exception as e: - uo.error(e) + operation_logger.error(e) raise else: - uo.success() + operation_logger.success() return result return func_wrapper return decorate -class UnitOperation(object): +class OperationLogger(object): """ Instances of this class represents unit operation done on the ynh instance. diff --git a/src/yunohost/service.py b/src/yunohost/service.py index ba9c4450e..66ae837a9 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -152,7 +152,7 @@ def service_stop(names): logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() -def service_enable(uo, names): +def service_enable(operation_logger, names): """ Enable one or more services @@ -160,7 +160,7 @@ def service_enable(uo, names): names -- Services name to enable """ - uo.start() + operation_logger.start() if isinstance(names, str): names = [names] for name in names: @@ -346,7 +346,7 @@ def service_log(name, number=50): @is_unit_operation([('names', 'service')]) -def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False, +def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False, list_pending=False): """ Regenerate the configuration file(s) for a service @@ -380,12 +380,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False return pending_conf if not dry_run: - uo.related_to = [('service', x) for x in names] + operation_logger.related_to = [('service', x) for x in names] if not names: - uo.name_parameter_override = 'all' + operation_logger.name_parameter_override = 'all' elif len(names) != 1: - uo.name_parameter_override = str(len(uo.related_to))+'_services' - uo.start() + operation_logger.name_parameter_override = str(len(operation_logger.related_to))+'_services' + operation_logger.start() # Clean pending conf directory if os.path.isdir(PENDING_CONF_DIR): @@ -425,12 +425,12 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True - uo.related_to = [] + operation_logger.related_to = [] # Iterate over services and process pending conf for service, conf_files in _get_pending_conf(names).items(): if not dry_run: - uo.related_to.append(('service', service)) + operation_logger.related_to.append(('service', service)) logger.debug(m18n.n( 'service_regenconf_pending_applying' if not dry_run else @@ -580,7 +580,7 @@ def service_regen_conf(uo, names=[], with_diff=False, force=False, dry_run=False hook_callback('conf_regen', names, pre_callback=_pre_call) - uo.success() + operation_logger.success() return result diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e0e041886..321a18d5b 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -52,7 +52,7 @@ from yunohost.service import service_status, service_regen_conf, service_log, se from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip -from yunohost.log import is_unit_operation, UnitOperation +from yunohost.log import is_unit_operation, OperationLogger # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -140,7 +140,7 @@ def tools_adminpw(auth, new_password): @is_unit_operation() -def tools_maindomain(uo, auth, new_domain=None): +def tools_maindomain(operation_logger, auth, new_domain=None): """ Check the current main domain, or change it @@ -157,8 +157,8 @@ def tools_maindomain(uo, auth, new_domain=None): if new_domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) - uo.related_to.append(('domain', new_domain)) - uo.start() + operation_logger.related_to.append(('domain', new_domain)) + operation_logger.start() # Apply changes to ssl certs ssl_key = "/etc/ssl/private/yunohost_key.pem" @@ -250,7 +250,7 @@ def _is_inside_container(): @is_unit_operation() -def tools_postinstall(uo, domain, password, ignore_dyndns=False): +def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): """ YunoHost post-install @@ -299,7 +299,7 @@ def tools_postinstall(uo, domain, password, ignore_dyndns=False): else: dyndns = False - uo.start() + operation_logger.start() logger.info(m18n.n('yunohost_installing')) service_regen_conf(['nslcd', 'nsswitch'], force=True) @@ -476,7 +476,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): @is_unit_operation() -def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): +def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=False): """ Update apps & package cache, then display changelog @@ -517,7 +517,7 @@ def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): if cache.get_changes(): logger.info(m18n.n('upgrading_packages')) - uo.start() + operation_logger.start() try: # Apply APT changes # TODO: Logs output for the API @@ -527,10 +527,10 @@ def tools_upgrade(uo, auth, ignore_apps=False, ignore_packages=False): failure = True logger.warning('unable to upgrade packages: %s' % str(e)) logger.error(m18n.n('packages_upgrade_failed')) - uo.error(m18n.n('packages_upgrade_failed')) + operation_logger.error(m18n.n('packages_upgrade_failed')) else: logger.info(m18n.n('done')) - uo.success() + operation_logger.success() else: logger.info(m18n.n('packages_no_upgrade')) @@ -716,7 +716,7 @@ def tools_port_available(port): @is_unit_operation() -def tools_shutdown(uo, force=False): +def tools_shutdown(operation_logger, force=False): shutdown = force if not shutdown: try: @@ -729,13 +729,13 @@ def tools_shutdown(uo, force=False): shutdown = True if shutdown: - uo.start() + operation_logger.start() logger.warn(m18n.n('server_shutdown')) subprocess.check_call(['systemctl', 'poweroff']) @is_unit_operation() -def tools_reboot(uo, force=False): +def tools_reboot(operation_logger, force=False): reboot = force if not reboot: try: @@ -747,7 +747,7 @@ def tools_reboot(uo, force=False): if i.lower() == 'y' or i.lower() == 'yes': reboot = True if reboot: - uo.start() + operation_logger.start() logger.warn(m18n.n('server_reboot')) subprocess.check_call(['systemctl', 'reboot']) @@ -870,8 +870,8 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai for migration in migrations: # Start register change on system - uo= UnitOperation('tools_migrations_migrate_' + mode) - uo.start() + operation_logger= OperationLogger('tools_migrations_migrate_' + mode) + operation_logger.start() if not skip: @@ -881,11 +881,11 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai try: if mode == "forward": m = migration["module"].MyMigration() - m.uo = uo + m.operation_logger = operation_logger m.migrate() elif mode == "backward": m = migration["module"].MyMigration() - m.uo = uo + m.operation_logger = operation_logger m.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) @@ -897,7 +897,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai number=migration.number, name=migration.name) logger.error(msg, exc_info=1) - uo.error(msg) + operation_logger.error(msg) break else: # if skip @@ -911,7 +911,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai "name": migration.name } - uo.success() + operation_logger.success() # special case where we want to go back from the start if target == 0: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c3fdf266c..48065f70a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -99,7 +99,7 @@ def user_list(auth, fields=None): @is_unit_operation([('username', 'user')]) -def user_create(uo, auth, username, firstname, lastname, mail, password, +def user_create(operation_logger, auth, username, firstname, lastname, mail, password, mailbox_quota="0"): """ Create user @@ -134,7 +134,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) - uo.start() + operation_logger.start() # Get random UID/GID all_uid = {x.pw_uid for x in pwd.getpwall()} @@ -222,7 +222,7 @@ def user_create(uo, auth, username, firstname, lastname, mail, password, @is_unit_operation([('username', 'user')]) -def user_delete(uo, auth, username, purge=False): +def user_delete(operation_logger, auth, username, purge=False): """ Delete user @@ -234,7 +234,7 @@ def user_delete(uo, auth, username, purge=False): from yunohost.app import app_ssowatconf from yunohost.hook import hook_callback - uo.start() + operation_logger.start() if auth.remove('uid=%s,ou=users' % username): # Invalidate passwd to take user deletion into account subprocess.call(['nscd', '-i', 'passwd']) @@ -259,7 +259,7 @@ def user_delete(uo, auth, username, purge=False): @is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) -def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, +def user_update(operation_logger, auth, username, firstname=None, lastname=None, mail=None, change_password=None, add_mailforward=None, remove_mailforward=None, add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ @@ -360,7 +360,7 @@ def user_update(uo, auth, username, firstname=None, lastname=None, mail=None, if mailbox_quota is not None: new_attr_dict['mailuserquota'] = mailbox_quota - uo.start() + operation_logger.start() if auth.update('uid=%s,ou=users' % username, new_attr_dict): logger.success(m18n.n('user_updated')) From dc19646504f3bc097bf2f56896934fe76a7c9b8e Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 23 Aug 2018 19:16:27 +0200 Subject: [PATCH 1064/1066] [fix] Remove old migration call --- src/yunohost/tools.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 321a18d5b..f9ee14994 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -879,14 +879,11 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai number=migration.number, name=migration.name)) try: + migration.operation_logger = operation_logger if mode == "forward": - m = migration["module"].MyMigration() - m.operation_logger = operation_logger - m.migrate() + migration.migrate() elif mode == "backward": - m = migration["module"].MyMigration() - m.operation_logger = operation_logger - m.backward() + migration.backward() else: # can't happen raise Exception("Illegal state for migration: '%s', should be either 'forward' or 'backward'" % mode) except Exception as e: From 36c2876d76310e368ca468d0b738181a5aa1fc1f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 23 Aug 2018 19:45:55 +0000 Subject: [PATCH 1065/1066] Update changelog --- debian/changelog | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian/changelog b/debian/changelog index 3cf70252d..a24f5c054 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,14 @@ +yunohost (3.2.0~testing1) testing; urgency=low + + * Add logging system of every unit operation (#165) + * Add a helper `ynh_info` for apps, so that they can comment on what is going on during scripts execution (#383) + * Fix the Sender Rewriting Scheme (#331) + * Add `ynh_render_template` to be able to render Jinja 2 templates (#463) + + Thanks to all contributors : Bram, ljf, Aleks ! + + -- Alexandre Aubin Thu, 23 Aug 2018 21:45:00 +0000 + yunohost (3.1.0) stable; urgency=low Highlights From cdd510642f4268a630060afc083253cd86d2e8fb Mon Sep 17 00:00:00 2001 From: Josue-T Date: Sat, 25 Aug 2018 19:45:55 +0200 Subject: [PATCH 1066/1066] Set equiv as base dependance (#515) --- data/helpers.d/package | 4 ---- debian/control | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 6f130bfb5..db3b50e0e 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -86,10 +86,6 @@ ynh_package_autopurge() { ynh_package_install_from_equivs () { local controlfile=$1 - # Check if the equivs package is installed. Or install it. - ynh_package_is_installed 'equivs' \ - || ynh_package_install equivs - # retrieve package information local pkgname=$(grep '^Package: ' $controlfile | cut -d' ' -f 2) # Retrieve the name of the debian package local pkgversion=$(grep '^Version: ' $controlfile | cut -d' ' -f 2) # And its version number diff --git a/debian/control b/debian/control index fae93019b..cf450484e 100644 --- a/debian/control +++ b/debian/control @@ -26,6 +26,7 @@ Depends: ${python:Depends}, ${misc:Depends} , metronome , rspamd (>= 1.6.0), redis-server, opendkim-tools , haveged + , equivs Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper