From cb1d02243e10c31c4dd59d1391068a82bab663a5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 14 Oct 2020 01:31:01 +0200 Subject: [PATCH 001/363] Do not advertise upgrades for bad-quality apps --- src/yunohost/app.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7d5d36c4d..8039d5a7f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -182,19 +182,29 @@ def _app_upgradable(app_infos): from packaging import version # Determine upgradability - # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - # Firstly use the version to know if an upgrade is available - app_is_in_catalog = bool(app_infos.get("from_catalog")) + app_in_catalog = app_infos.get("from_catalog") installed_version = version.parse(app_infos.get("version", "0~ynh0")) version_in_catalog = version.parse(app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0")) - if app_is_in_catalog and '~ynh' in str(installed_version) and '~ynh' in str(version_in_catalog): + if not app_in_catalog: + return "url_required" + + # Do not advertise upgrades for bad-quality apps + if not app_in_catalog.get("level", -1) >= 5 or app_in_catalog.get("state") != "working": + return "bad_quality" + + # If the app uses the standard version scheme, use it to determine + # upgradability + if '~ynh' in str(installed_version) and '~ynh' in str(version_in_catalog): if installed_version < version_in_catalog: return "yes" + else: + return "no" - if not app_is_in_catalog: - return "url_required" + # Legacy stuff for app with old / non-standard version numbers... + + # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"): return "url_required" From a5b282e5c65d146eb678a3517838321123bb4901 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 31 Oct 2020 19:14:33 +0100 Subject: [PATCH 002/363] Diagnosis: report usage of backports repository in apt's sources.list --- data/hooks/diagnosis/00-basesystem.py | 10 ++++++++++ locales/en.json | 1 + 2 files changed, 11 insertions(+) diff --git a/data/hooks/diagnosis/00-basesystem.py b/data/hooks/diagnosis/00-basesystem.py index d56faec98..00412cc74 100644 --- a/data/hooks/diagnosis/00-basesystem.py +++ b/data/hooks/diagnosis/00-basesystem.py @@ -92,6 +92,11 @@ class BaseSystemDiagnoser(Diagnoser): summary="diagnosis_package_installed_from_sury", details=["diagnosis_package_installed_from_sury_details"]) + if self.backports_in_sources_list(): + yield dict(meta={"test": "backports_in_sources_list"}, + status="WARNING", + summary="diagnosis_backports_in_sources_list") + def bad_sury_packages(self): packages_to_check = ["openssl", "libssl1.1", "libssl-dev"] @@ -105,6 +110,11 @@ class BaseSystemDiagnoser(Diagnoser): version_to_downgrade_to = check_output(cmd) yield (package, version_to_downgrade_to) + def backports_in_sources_list(self): + + cmd = "grep -q -nr '^ *deb .*-backports' /etc/apt/sources.list*" + return os.system(cmd) == 0 + def is_vulnerable_to_meltdown(self): # meltdown CVE: https://security-tracker.debian.org/tracker/CVE-2017-5754 diff --git a/locales/en.json b/locales/en.json index 221046382..e13c0d2b2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -147,6 +147,7 @@ "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", "diagnosis_basesystem_ynh_main_version": "Server is running YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "You are running inconsistent versions of the YunoHost packages... most probably because of a failed or partial upgrade.", + "diagnosis_backports_in_sources_list": "It looks like apt (the package manager) is configured to use the backports repository. Unless you really know what you are doing, we strongly discourage from installing packages from backports, because it's likely to create unstabilities or conflicts on your system.", "diagnosis_package_installed_from_sury": "Some system packages should be downgraded", "diagnosis_package_installed_from_sury_details": "Some packages were inadvertendly installed from a third-party repository called Sury. The Yunohost team improved the strategy that handle these packages, but it's expected that some setups that installed PHP7.3 apps while still on Stretch have some remaining inconsistencies. To fix this situation, you should try running the following command: {cmd_to_fix}", "diagnosis_display_tip": "To see the issues found, you can go to the Diagnosis section of the webadmin, or run 'yunohost diagnosis show --issues' from the command-line.", From 6b04a4cae40ebee54ef10cf59e245d80ba2179a3 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 2 Nov 2020 15:30:23 +0100 Subject: [PATCH 003/363] Define a new "yunohost app search" command --- data/actionsmap/yunohost.yml | 7 +++++++ src/yunohost/app.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5371d576d..acdd38a00 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -569,6 +569,13 @@ app: full: --with-categories help: Also return a list of app categories action: store_true + + ### app_search() + search: + action_help: Search installable apps + arguments: + string: + help: Return matching app name or description with "string" fetchlist: deprecated: true diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 7d5d36c4d..5e8ac929b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -104,6 +104,23 @@ def app_catalog(full=False, with_categories=False): return {"apps": catalog["apps"]} else: return {"apps": catalog["apps"], "categories": catalog["categories"]} + + +def app_search(string): + """ + Return a dict of apps whose description or name match the search string + """ + + # Retrieve a simple dict listing all apps + catalog_of_apps = app_catalog() + + # Selecting apps according to a match in app name or description + for app in catalog_of_apps["apps"].items(): + if not (re.search(string, app[0], flags=re.IGNORECASE) or + re.search(string, app[1]['description'], flags=re.IGNORECASE)): + del catalog_of_apps["apps"][app[0]] + + return catalog_of_apps # Old legacy function... From 1cb75497c431dd635318c17a42e1c5f90103d5e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 17 Nov 2020 15:29:24 +0100 Subject: [PATCH 004/363] Increase ldap size limit? --- data/templates/slapd/ldap.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/slapd/ldap.conf b/data/templates/slapd/ldap.conf index 09aeb8b4f..dfcb17e41 100644 --- a/data/templates/slapd/ldap.conf +++ b/data/templates/slapd/ldap.conf @@ -8,7 +8,7 @@ BASE dc=yunohost,dc=org URI ldap://localhost:389 -#SIZELIMIT 12 +SIZELIMIT 10000 #TIMELIMIT 15 #DEREF never From bfecb8b7dc23a1574e0dba15ec15edf86311d1b2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 22 Nov 2020 02:19:56 +0100 Subject: [PATCH 005/363] Support more complex errors (be able to return additional data in a json structure) --- src/yunohost/app.py | 2 +- src/yunohost/utils/error.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 076096eef..7a8fa695a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -930,7 +930,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu permission_sync_to_user() - raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + raise YunohostError(failure_message_with_debug_instructions, raw_msg=True, log_ref=operation_logger.name) # Clean hooks and add new ones hook_remove(app_instance_name) diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index f2486473b..78077b42a 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -32,11 +32,23 @@ class YunohostError(MoulinetteError): are translated via m18n.n (namespace) instead of m18n.g (global?) """ - def __init__(self, key, raw_msg=False, *args, **kwargs): + def __init__(self, key, raw_msg=False, log_ref=None, *args, **kwargs): self.key = key # Saving the key is useful for unit testing self.kwargs = kwargs # Saving the key is useful for unit testing + self.log_ref = log_ref if raw_msg: msg = key else: msg = m18n.n(key, *args, **kwargs) + super(YunohostError, self).__init__(msg, raw_msg=True) + + def content(self): + + if not self.log_ref: + return super(YunohostError, self).content() + else: + return { + "error": self.strerror, + "log_ref": self.log_ref + } From 11cef18ab9e7adccbd3c55ba1b9d0602245187d0 Mon Sep 17 00:00:00 2001 From: yalh76 Date: Tue, 1 Dec 2020 23:19:07 +0100 Subject: [PATCH 006/363] [enh] Adding composer helper --- data/helpers.d/php | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/data/helpers.d/php b/data/helpers.d/php index 95cc15402..ad6683944 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -560,3 +560,63 @@ ynh_get_scalable_phpfpm () { fi fi } + +readonly YNH_DEFAULT_COMPOSER_VERSION=1.10.17 +# Declare the actual composer version to use. +# A packager willing to use another version of composer can override the variable into its _common.sh. +YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} + +# Execute a command with Composer +# +# usage: ynh_composer_exec [--phpversion=phpversion] [--workdir=$final_path] --commands="commands" +# | arg: -v, --phpversion - PHP version to use with composer +# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -c, --commands - Commands to execute. +ynh_composer_exec () { + # Declare an array to define the options of this helper. + local legacy_args=vwc + declare -Ar args_array=( [v]=phpversion= [w]=workdir= [c]=commands= ) + local phpversion + local workdir + local commands + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + workdir="${workdir:-$final_path}" + phpversion="${phpversion:-$YNH_PHP_VERSION}" + + COMPOSER_HOME="$workdir/.composer" \ + php${phpversion} "$workdir/composer.phar" $commands \ + -d "$workdir" --quiet --no-interaction +} + +# Install and initialize Composer in the given directory +# +# usage: ynh_install_composer [--phpversion=phpversion] [--workdir=$final_path] [--install_args="--optimize-autoloader"] [--composerversion=composerversion] +# | arg: -v, --phpversion - PHP version to use with composer +# | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. +# | arg: -a, --install_args - Additional arguments provided to the composer install. Argument --no-dev already include +# | arg: -c, --composerversion - Composer version to install +ynh_install_composer () { + # Declare an array to define the options of this helper. + local legacy_args=vwac + declare -Ar args_array=( [v]=phpversion= [w]=workdir= [a]=install_args= [c]=composerversion=) + local phpversion + local workdir + local install_args + local composerversion + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + workdir="${workdir:-$final_path}" + phpversion="${phpversion:-$YNH_PHP_VERSION}" + install_args="${install_args:-}" + composerversion="${composerversion:-$YNH_COMPOSER_VERSION}" + + curl -sS https://getcomposer.org/installer \ + | COMPOSER_HOME="$workdir/.composer" \ + php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ + || ynh_die "Unable to install Composer." + + # install dependencies + ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \ + || ynh_die "Unable to install core dependencies with Composer." +} From d15ccc6c238555d24623a7083db621bd356a9b6f Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 3 Dec 2020 14:39:20 +0100 Subject: [PATCH 007/363] use organization name for LE cert --- 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 a0f9d4032..8fb9fa684 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -637,6 +637,7 @@ def _get_status(domain): cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN + organization_name = cert.get_issuer().O valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") days_remaining = (valid_up_to - datetime.utcnow()).days @@ -646,7 +647,7 @@ def _get_status(domain): "verbose": "Self-signed", } - elif cert_issuer.startswith("Let's Encrypt") or cert_issuer == "R3": + elif organization_name == "Let's Encrypt": CA_type = { "code": "lets-encrypt", "verbose": "Let's Encrypt", From 7780aa658f4b3467326383be1fcb24ab52eaad35 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 3 Dec 2020 14:39:40 +0100 Subject: [PATCH 008/363] fix staging cert url --- 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 8fb9fa684..c9451c2be 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -61,7 +61,7 @@ KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days # For tests -STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" +STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging-v02.api.letsencrypt.org" # For prod PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v02.api.letsencrypt.org" From 11fb2659c611181909684b250f1c70a7b28f8352 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 4 Dec 2020 14:13:21 +0100 Subject: [PATCH 009/363] [fix] Avoid too many recipient --- data/templates/postfix/main.cf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index 61aa27211..6a1029b71 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -182,6 +182,9 @@ milter_default_action = accept smtp_destination_concurrency_limit = 2 default_destination_rate_delay = 5s +# Avoid to be blacklisted due to too many recipient +smtpd_client_recipient_rate_limit=150 + # Avoid email adress scanning # By default it's possible to detect if the email adress exist # So it's easly possible to scan a server to know which email adress is valid From e0ac7de432c7e565fef3c07b319bffa7d8cf7945 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 20:17:58 +0100 Subject: [PATCH 010/363] We ain't using argument user nor no_trace --- src/yunohost/hook.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 22d1ef623..c505a2d30 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -211,7 +211,7 @@ def hook_list(action, list_by='name', show_info=False): return {'hooks': result} -def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, +def hook_callback(action, hooks=[], args=None, chdir=None, env=None, pre_callback=None, post_callback=None): """ Execute all scripts binded to an action @@ -220,7 +220,6 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, action -- Action name hooks -- List of hooks names to execute args -- Ordered list of arguments to pass to the scripts - no_trace -- Do not print each command that will be executed chdir -- The directory from where the scripts will be executed env -- Dictionnary of environment variables to export pre_callback -- An object to call before each script execution with @@ -281,7 +280,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_return = hook_exec(path, args=hook_args, chdir=chdir, env=env, - no_trace=no_trace, raise_on_error=True)[1] + raise_on_error=True)[1] except YunohostError as e: state = 'failed' hook_return = {} @@ -297,8 +296,8 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, return result -def hook_exec(path, args=None, raise_on_error=False, no_trace=False, - chdir=None, env=None, user="root", return_format="json"): +def hook_exec(path, args=None, raise_on_error=False, + chdir=None, env=None, return_format="json"): """ Execute hook from a file with arguments @@ -306,11 +305,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, path -- Path of the script to execute args -- Ordered list of arguments to pass to the script raise_on_error -- Raise if the script returns a non-zero exit code - 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 - """ # Validate hook path @@ -348,7 +344,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, if hook_type == 'text/x-python': returncode, returndata = _hook_exec_python(path, args, env, loggers) else: - returncode, returndata = _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers) + returncode, returndata = _hook_exec_bash(path, args, chdir, env, return_format, loggers) # Check and return process' return code if returncode is None: @@ -363,7 +359,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, return returncode, returndata -def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, loggers): +def _hook_exec_bash(path, args, chdir, env, return_format, loggers): from moulinette.utils.process import call_async_output @@ -395,16 +391,10 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge env['YNH_STDRETURN'] = stdreturn # Construct command to execute - if user == "root": - command = ['sh', '-c'] - else: - command = ['sudo', '-n', '-u', user, '-H', 'sh', '-c'] + command = ['sh', '-c'] - if no_trace: - cmd = '/bin/bash "{script}" {args}' - else: - # use xtrace on fd 7 which is redirected to stdout - cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' + # 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( From 14f6f09993d0963f7c3e1b723aec0edeb819e3ca Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 20:19:53 +0100 Subject: [PATCH 011/363] Do not write the env explicitly in the command to avoid leaking secrets in ps -ef... --- src/yunohost/hook.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index c505a2d30..039737737 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -396,17 +396,13 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): # 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)) - for k, v in env.items()]), cmd) command.append(cmd.format(script=cmd_script, args=cmd_args)) logger.debug("Executing command '%s'" % ' '.join(command)) returncode = call_async_output( command, loggers, shell=False, cwd=chdir, - stdinfo=stdinfo + stdinfo=stdinfo, env=env ) raw_content = None From d6d45cf7a6acdf2d5c3d663c8100afb8dc96264f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 20:28:45 +0100 Subject: [PATCH 012/363] Simplify command call, no need to wrap this in a dummy 'sh -c' --- src/yunohost/hook.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 039737737..af9897851 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -390,18 +390,15 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): f.write('') env['YNH_STDRETURN'] = stdreturn - # Construct command to execute - command = ['sh', '-c'] - # use xtrace on fd 7 which is redirected to stdout - cmd = 'BASH_XTRACEFD=7 /bin/bash -x "{script}" {args} 7>&1' + env['BASH_XTRACEFD'] = 7 + cmd = '/bin/bash -x "{script}" {args} 7>&1' + cmd = cmd.format(script=cmd_script, args=cmd_args) - command.append(cmd.format(script=cmd_script, args=cmd_args)) - - logger.debug("Executing command '%s'" % ' '.join(command)) + logger.debug("Executing command '%s'" % cmd) returncode = call_async_output( - command, loggers, shell=False, cwd=chdir, + cmd, loggers, shell=True, cwd=chdir, stdinfo=stdinfo, env=env ) From 2428f0152183ff791e76248f20bc3d2d171c5ebc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 20:35:53 +0100 Subject: [PATCH 013/363] Do not pass app args to the command line, they're already provided in the env... --- src/yunohost/app.py | 27 +++++++-------------------- src/yunohost/backup.py | 9 +++++---- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 71aaca316..93ffbbb5b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -356,8 +356,6 @@ def app_change_url(operation_logger, app, domain, path): # Retrieve arguments list for change_url script # TODO: Allow to specify arguments args_odict = _parse_args_from_manifest(manifest, 'change_url') - args_list = [value[0] for value in args_odict.values()] - args_list.append(app) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app, args=args_odict) @@ -389,7 +387,7 @@ def app_change_url(operation_logger, 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)[0] != 0: + env=env_dict)[0] != 0: msg = "Failed to change '%s' url." % app logger.error(msg) operation_logger.error(msg) @@ -420,7 +418,7 @@ def app_change_url(operation_logger, 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) + hook_callback('post_app_change_url', env=env_dict) def app_upgrade(app=[], url=None, file=None, force=False): @@ -518,8 +516,6 @@ def app_upgrade(app=[], url=None, file=None, force=False): # Retrieve arguments list for upgrade script # TODO: Allow to specify arguments args_odict = _parse_args_from_manifest(manifest, 'upgrade') - args_list = [value[0] for value in args_odict.values()] - args_list.append(app_instance_name) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) @@ -548,7 +544,7 @@ def app_upgrade(app=[], url=None, file=None, force=False): upgrade_failed = True try: upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade', - args=args_list, env=env_dict)[0] + env=env_dict)[0] upgrade_failed = True if upgrade_retcode != 0 else False if upgrade_failed: @@ -625,7 +621,7 @@ def app_upgrade(app=[], url=None, file=None, force=False): # So much win logger.success(m18n.n('app_upgraded', app=app_instance_name)) - hook_callback('post_app_upgrade', args=args_list, env=env_dict) + hook_callback('post_app_upgrade', env=env_dict) operation_logger.success() permission_sync_to_user() @@ -742,10 +738,6 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Validate domain / path availability for webapps _validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder) - # build arg list tq - args_list = [value[0] for value in args_odict.values()] - args_list.append(app_instance_name) - # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -818,7 +810,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu try: install_retcode = hook_exec( os.path.join(extracted_app_folder, 'scripts/install'), - args=args_list, env=env_dict + env=env_dict )[0] # "Common" app install failure : the script failed and returned exit code != 0 install_failed = True if install_retcode != 0 else False @@ -934,7 +926,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu logger.success(m18n.n('installation_complete')) - hook_callback('post_app_install', args=args_list, env=env_dict) + hook_callback('post_app_install', env=env_dict) def dump_app_log_extract_for_debugging(operation_logger): @@ -1021,8 +1013,6 @@ def app_remove(operation_logger, app): os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') - args_list = [app] - env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app) env_dict["YNH_APP_ID"] = app_id @@ -1034,7 +1024,6 @@ def app_remove(operation_logger, app): try: ret = hook_exec('/tmp/yunohost_remove/scripts/remove', - args=args_list, env=env_dict)[0] # Here again, calling hook_exec could fail miserably, or get # manually interrupted (by mistake or because script was stuck) @@ -1047,7 +1036,7 @@ def app_remove(operation_logger, app): if ret == 0: logger.success(m18n.n('app_removed', app=app)) - hook_callback('post_app_remove', args=args_list, env=env_dict) + hook_callback('post_app_remove', env=env_dict) else: logger.warning(m18n.n('app_not_properly_removed', app=app)) @@ -1460,7 +1449,6 @@ def app_action_run(operation_logger, app, action, args=None): # Retrieve arguments list for install script 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 = [value[0] for value in args_odict.values()] env_dict = _make_environment_for_app_script(app, args=args_odict, args_prefix="ACTION_") env_dict["YNH_ACTION"] = action @@ -1479,7 +1467,6 @@ def app_action_run(operation_logger, app, action, args=None): retcode = hook_exec( path, - args=args_list, env=env_dict, chdir=cwd, user=action_declaration.get("user", "root"), diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c0f11eae8..4bafa8455 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -685,8 +685,10 @@ class BackupManager(): 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)[0] + hook_exec(tmp_script, + raise_on_error=True, + chdir=tmp_app_bkp_dir, + env=env_dict)[0] self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) @@ -1382,7 +1384,6 @@ class RestoreManager(): # 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)[0] @@ -1407,7 +1408,7 @@ class RestoreManager(): operation_logger.start() # Execute remove script - if hook_exec(remove_script, args=[app_instance_name], + if hook_exec(remove_script, env=env_dict_remove)[0] != 0: msg = m18n.n('app_not_properly_removed', app=app_instance_name) logger.warning(msg) From 81f3b493e49415c3b2d7ddde82da4bd34d85dcb7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 21:06:23 +0100 Subject: [PATCH 014/363] Also copy the script env --- src/yunohost/hook.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index af9897851..1758a1167 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -397,9 +397,12 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): logger.debug("Executing command '%s'" % cmd) + _env = os.environ.copy() + _env.update(env) + returncode = call_async_output( cmd, loggers, shell=True, cwd=chdir, - stdinfo=stdinfo, env=env + stdinfo=stdinfo, env=_env ) raw_content = None From efdb2ee6b5874fd4ee7645b2a6a0d0ad7411412d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 5 Dec 2020 21:06:37 +0100 Subject: [PATCH 015/363] Gotta make sure everything is a string --- src/yunohost/app.py | 2 +- src/yunohost/hook.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 93ffbbb5b..686209225 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2761,7 +2761,7 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): } for arg_name, arg_value_and_type in args.items(): - env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = arg_value_and_type[0] + env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value_and_type[0]) return env_dict diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 1758a1167..53ed5f2e5 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -391,7 +391,7 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): env['YNH_STDRETURN'] = stdreturn # use xtrace on fd 7 which is redirected to stdout - env['BASH_XTRACEFD'] = 7 + env['BASH_XTRACEFD'] = "7" cmd = '/bin/bash -x "{script}" {args} 7>&1' cmd = cmd.format(script=cmd_script, args=cmd_args) From 2be911d369a3e824a2fb3e5a2cebf8a97dbdd472 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 12:17:59 +0100 Subject: [PATCH 016/363] [fix] Missing backup conf --- data/hooks/backup/22-conf_mail | 13 +++++++++++++ data/hooks/restore/22-conf_mail | 5 +++++ 2 files changed, 18 insertions(+) create mode 100644 data/hooks/backup/22-conf_mail create mode 100644 data/hooks/restore/22-conf_mail diff --git a/data/hooks/backup/22-conf_mail b/data/hooks/backup/22-conf_mail new file mode 100644 index 000000000..018a345d9 --- /dev/null +++ b/data/hooks/backup/22-conf_mail @@ -0,0 +1,13 @@ +#!/bin/bash + +# Exit hook on subcommand error or unset variable +set -eu + +# Source YNH helpers +source /usr/share/yunohost/helpers + +# Backup destination +backup_dir="${1}/etc/dkim" + +# Backup the configuration +ynh_backup "/etc/dkim" "$backup_dir" diff --git a/data/hooks/restore/22-conf_mail b/data/hooks/restore/22-conf_mail new file mode 100644 index 000000000..c069b53db --- /dev/null +++ b/data/hooks/restore/22-conf_mail @@ -0,0 +1,5 @@ +#!/bin/bash + +backup_dir="$1/etc/dkim" + +cp -a $backup_dir/. /etc/dkim From f1f97d0235a60ee5661937bf1f64d083b4d116ac Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 17:05:51 +0100 Subject: [PATCH 017/363] [fix] DNS conf --- src/yunohost/domain.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 88fd9e3e9..d3e8c53f0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -258,6 +258,10 @@ def domain_dns_conf(domain, ttl=None): """ + domains = domain_list() + if domain not in domain_list()['domains']: + raise YunohostError('domain_name_unknown', domain=domain) + ttl = 3600 if ttl is None else ttl dns_conf = _build_dns_conf(domain, ttl) From 6aebe6faf9ccce329a6c33dd855a4bf5c34541c7 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 17:11:10 +0100 Subject: [PATCH 018/363] [fix] Unused line --- src/yunohost/domain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index d3e8c53f0..b771d60ab 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -258,7 +258,6 @@ def domain_dns_conf(domain, ttl=None): """ - domains = domain_list() if domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) From 792643607bea29e89c4c27f60e54c56819216965 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 17:55:29 +0100 Subject: [PATCH 019/363] [fix] Avoid password are given in users hooks --- src/yunohost/user.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 67fd43a03..6ecfffb48 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -218,12 +218,28 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) + # Trigger post_user_create hooks + env_dict = { + "YNH_USER_USERNAME": username, + "YNH_USER_MAIL": mail, + "YNH_USER_PASSWORD": password, + "YNH_USER_FIRSTNAME": firstname, + "YNH_USER_LASTNAME": lastname + } + + # Put a random password instead of the true one to force + # packagers to change this + # FIXME: Remove this in future version + chars = string.ascii_letters + string.digits + string.punctuation + fake_password = "".join([random.choice(chars) for i in range(20)]) + fake_password += " num args are deprecated, please use YNH_USER_PASSWORD" + hook_callback('post_user_create', + args=[username, mail, fake_password, firstname, lastname], + env=env_dict) + # 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} @@ -311,16 +327,21 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= if not result: raise YunohostError('user_unknown', user=username) user = result[0] + env_dict = { + "YNH_USER_USERNAME": username + } # Get modifications from arguments new_attr_dict = {} if firstname: new_attr_dict['givenName'] = [firstname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]] + env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: new_attr_dict['sn'] = [lastname] # TODO: Validate new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname] + env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] @@ -330,6 +351,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= assert_password_is_strong_enough("user", change_password) new_attr_dict['userPassword'] = [_hash_user_password(change_password)] + env_dict["YNH_USER_PASSWORD"] = change_password if mail: main_domain = _get_maindomain() @@ -374,6 +396,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= raise YunohostError('mail_alias_remove_failed', mail=mail) new_attr_dict['mail'] = user['mail'] + if 'mail' in new_attr_dict: + env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail']) + if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [add_mailforward] @@ -393,8 +418,12 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= raise YunohostError('mail_forward_remove_failed', mail=mail) new_attr_dict['maildrop'] = user['maildrop'] + if 'maildrop' in new_attr_dict: + env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop']) + if mailbox_quota is not None: new_attr_dict['mailuserquota'] = [mailbox_quota] + env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota operation_logger.start() @@ -403,6 +432,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= except Exception as e: raise YunohostError('user_update_failed', user=username, error=e) + # Trigger post_user_update hooks + hook_callback('post_user_update', env=env_dict) + logger.success(m18n.n('user_updated')) app_ssowatconf() return user_info(username) From 2ebef4ebb42840c5bc623453f9378035903ab684 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 17:57:48 +0100 Subject: [PATCH 020/363] [fix] Missing hook_callback --- src/yunohost/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 6ecfffb48..c11ec58a2 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -317,6 +317,7 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= from yunohost.app import app_ssowatconf from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.ldap import _get_ldap_interface + from yunohost.hook import hook_callback domains = domain_list()['domains'] From cdb1d83eae09d8b10bfda628f9014e4ac54fd78c Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 18:06:42 +0100 Subject: [PATCH 021/363] [fix] No need for fake password, no app use ,4,5 --- src/yunohost/user.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c11ec58a2..b4e20311a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -227,15 +227,7 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor "YNH_USER_LASTNAME": lastname } - # Put a random password instead of the true one to force - # packagers to change this - # FIXME: Remove this in future version - chars = string.ascii_letters + string.digits + string.punctuation - fake_password = "".join([random.choice(chars) for i in range(20)]) - fake_password += " num args are deprecated, please use YNH_USER_PASSWORD" - hook_callback('post_user_create', - args=[username, mail, fake_password, firstname, lastname], - env=env_dict) + hook_callback('post_user_create', args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user logger.success(m18n.n('user_created')) From 2c9a668afe5da0f2ce7b1d8f2a08faa6dd3c0cfe Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 8 Dec 2020 23:45:44 +0100 Subject: [PATCH 022/363] [fix] Unix permission on dkim --- data/hooks/restore/22-conf_mail | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/hooks/restore/22-conf_mail b/data/hooks/restore/22-conf_mail index c069b53db..312b3f61b 100644 --- a/data/hooks/restore/22-conf_mail +++ b/data/hooks/restore/22-conf_mail @@ -3,3 +3,9 @@ backup_dir="$1/etc/dkim" cp -a $backup_dir/. /etc/dkim + +chown -R root:root /etc/dkim +chown _rspamd:root /etc/dkim +chown _rspamd:root /etc/dkim/*.mail.key +chmod 600 /etc/dkim/*.mail.txt +chmod 400 /etc/dkim/*.mail.key From 8b53d6562e8a948dcda824ff2480d0eb2fa66f87 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 9 Dec 2020 01:31:17 +0100 Subject: [PATCH 023/363] [fix] Add Dyndns domains keys to backup --- data/hooks/backup/42-conf_ynh_dyndns | 10 ++++++++++ data/hooks/restore/42-conf_ynh_dyndns | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 data/hooks/backup/42-conf_ynh_dyndns create mode 100644 data/hooks/restore/42-conf_ynh_dyndns diff --git a/data/hooks/backup/42-conf_ynh_dyndns b/data/hooks/backup/42-conf_ynh_dyndns new file mode 100644 index 000000000..776162ff0 --- /dev/null +++ b/data/hooks/backup/42-conf_ynh_dyndns @@ -0,0 +1,10 @@ +#!/bin/bash + +source /usr/share/yunohost/helpers +ynh_abort_if_errors +YNH_CWD="${YNH_BACKUP_DIR%/}/conf/ynh/dyndns" +cd "$YNH_CWD" + +# Backup the configuration +ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory +ynh_backup --src_path="/etc/cron.d/yunohost-dyndns" --not_mandatory diff --git a/data/hooks/restore/42-conf_ynh_dyndns b/data/hooks/restore/42-conf_ynh_dyndns new file mode 100644 index 000000000..d16d7a67c --- /dev/null +++ b/data/hooks/restore/42-conf_ynh_dyndns @@ -0,0 +1,10 @@ +#!/bin/bash + +source /usr/share/yunohost/helpers +ynh_abort_if_errors +YNH_CWD="${YNH_BACKUP_DIR%/}/conf/ynh/dyndns" +cd "$YNH_CWD" + +# Restore file if exists +ynh_restore_file --origin_path="/etc/yunohost/dyndns" --not_mandatory +ynh_restore_file --origin_path="/etc/cron.d/yunohost-dyndns" --not_mandatory From ef9cc98d5e07dcc224ead8992bafc26f83c2acbd Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 9 Dec 2020 01:35:40 +0100 Subject: [PATCH 024/363] [enh] Use ynh_backup helpers --- data/hooks/backup/22-conf_mail | 13 ++++--------- data/hooks/restore/22-conf_mail | 6 ++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/data/hooks/backup/22-conf_mail b/data/hooks/backup/22-conf_mail index 018a345d9..9db39e0c2 100644 --- a/data/hooks/backup/22-conf_mail +++ b/data/hooks/backup/22-conf_mail @@ -1,13 +1,8 @@ #!/bin/bash -# Exit hook on subcommand error or unset variable -set -eu - -# Source YNH helpers source /usr/share/yunohost/helpers +ynh_abort_if_errors +YNH_CWD="${YNH_BACKUP_DIR%/}/conf/dkim" +cd "$YNH_CWD" -# Backup destination -backup_dir="${1}/etc/dkim" - -# Backup the configuration -ynh_backup "/etc/dkim" "$backup_dir" +ynh_backup --src_path="/etc/dkim" diff --git a/data/hooks/restore/22-conf_mail b/data/hooks/restore/22-conf_mail index 312b3f61b..77e0a4d42 100644 --- a/data/hooks/restore/22-conf_mail +++ b/data/hooks/restore/22-conf_mail @@ -1,11 +1,9 @@ #!/bin/bash -backup_dir="$1/etc/dkim" +backup_dir="$1/conf/dkim" -cp -a $backup_dir/. /etc/dkim +cp -a $backup_dir/etc/dkim/. /etc/dkim chown -R root:root /etc/dkim chown _rspamd:root /etc/dkim chown _rspamd:root /etc/dkim/*.mail.key -chmod 600 /etc/dkim/*.mail.txt -chmod 400 /etc/dkim/*.mail.key From 9a87d8a52da3dc137adacf7997d836acb3dea840 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 9 Dec 2020 01:59:06 +0100 Subject: [PATCH 025/363] [fix] Missing dir --- data/hooks/backup/22-conf_mail | 1 + 1 file changed, 1 insertion(+) diff --git a/data/hooks/backup/22-conf_mail b/data/hooks/backup/22-conf_mail index 9db39e0c2..b604d8aa8 100644 --- a/data/hooks/backup/22-conf_mail +++ b/data/hooks/backup/22-conf_mail @@ -3,6 +3,7 @@ source /usr/share/yunohost/helpers ynh_abort_if_errors YNH_CWD="${YNH_BACKUP_DIR%/}/conf/dkim" +mkdir -p "$YNH_CWD" cd "$YNH_CWD" ynh_backup --src_path="/etc/dkim" From 60d838843eeb9b2ed94b19e847fc43ee30e0358b Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 9 Dec 2020 02:36:39 +0100 Subject: [PATCH 026/363] [fix] Missing mkdir --- data/hooks/backup/42-conf_ynh_dyndns | 1 + 1 file changed, 1 insertion(+) diff --git a/data/hooks/backup/42-conf_ynh_dyndns b/data/hooks/backup/42-conf_ynh_dyndns index 776162ff0..323464108 100644 --- a/data/hooks/backup/42-conf_ynh_dyndns +++ b/data/hooks/backup/42-conf_ynh_dyndns @@ -3,6 +3,7 @@ source /usr/share/yunohost/helpers ynh_abort_if_errors YNH_CWD="${YNH_BACKUP_DIR%/}/conf/ynh/dyndns" +mkdir -p $YNH_CWD cd "$YNH_CWD" # Backup the configuration From 5422a49d82d3b580fabf086ea13f1a5f55acb1ac Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 10 Dec 2020 17:25:58 +0100 Subject: [PATCH 027/363] We don't care that 'apt-key output should not be parsed' --- src/yunohost/hook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 22d1ef623..eafcaf825 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -331,7 +331,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, r"invalid value for trace file descriptor", r"Creating config file .* with new version", r"Created symlink /etc/systemd", - r"dpkg: warning: while removing .* not empty so not removed" + r"dpkg: warning: while removing .* not empty so not removed", + r"apt-key output should not be parsed" ] return all(not re.search(w, msg) for w in irrelevant_warnings) From 116a15f9f1b7facc4f127243496e862e74459d68 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 10 Dec 2020 18:18:09 +0100 Subject: [PATCH 028/363] [fix] Avoid to define the ip on wildcard subdomains too --- data/templates/dnsmasq/domain.tpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/templates/dnsmasq/domain.tpl b/data/templates/dnsmasq/domain.tpl index f4c8d6c4c..c4bb56d1d 100644 --- a/data/templates/dnsmasq/domain.tpl +++ b/data/templates/dnsmasq/domain.tpl @@ -1,8 +1,8 @@ -address=/{{ domain }}/{{ ipv4 }} -address=/xmpp-upload.{{ domain }}/{{ ipv4 }} +host-record={{ domain }},{{ ipv4 }} +host-record=xmpp-upload.{{ domain }},{{ ipv4 }} {% if ipv6 %} -address=/{{ domain }}/{{ ipv6 }} -address=/xmpp-upload.{{ domain }}/{{ ipv6 }} +host-record={{ domain }},{{ ipv6 }} +host-record=xmpp-upload.{{ domain }},{{ ipv6 }} {% endif %} txt-record={{ domain }},"v=spf1 mx a -all" mx-host={{ domain }},{{ domain }},5 From c823f5ffd5f3100f0ac936ebb550b3256de4a90b Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 11 Dec 2020 15:09:37 +0100 Subject: [PATCH 029/363] indent ssow conf --- src/yunohost/app.py | 5 ++--- src/yunohost/utils/legacy.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 71aaca316..2ccd026f5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1169,7 +1169,7 @@ def app_makedefault(operation_logger, app, domain=None): ssowat_conf['redirected_urls'][domain + '/'] = app_domain + app_path - write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf) + write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf, sort_keys=True, indent=4) os.system('chmod 644 /etc/ssowat/conf.json.persistent') logger.success(m18n.n('ssowat_conf_updated')) @@ -1405,8 +1405,7 @@ def app_ssowatconf(): 'permissions': permissions, } - with open('/etc/ssowat/conf.json', 'w+') as f: - json.dump(conf_dict, f, sort_keys=True, indent=4) + write_to_json('/etc/ssowat/conf.json', conf_dict, sort_keys=True, indent=4) from utils.legacy import translate_legacy_rules_in_ssowant_conf_json_persistent translate_legacy_rules_in_ssowant_conf_json_persistent() diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index a90b9fd74..b8b44135d 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -269,6 +269,6 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): "uris": protected_urls + persistent["permissions"].get("custom_protected", {}).get("uris", []), } - write_to_json("/etc/ssowat/conf.json.persistent", persistent) + write_to_json("/etc/ssowat/conf.json.persistent", persistent, sort_keys=True, indent=4) logger.warning("Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system") From 401778f68ed738240ef9bcd96567bb3e58bf702a Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 11 Dec 2020 16:22:29 +0100 Subject: [PATCH 030/363] fix the fixme --- 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 2ccd026f5..d6c132535 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1204,8 +1204,7 @@ def app_setting(app, key, value=None, delete=False): # GET if value is None and not delete: - # FIXME FIXME FIXME : what about the main url ...? - return ','.join(permission['additional_urls']) if permission else None + return ','.join(permission['uris'] + permission['additional_urls']) if permission else None # DELETE if delete: From c3e750a97af3919aa8f0222f0ab2326272a55778 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 11 Dec 2020 16:29:10 +0100 Subject: [PATCH 031/363] add all_users in protected_urls --- src/yunohost/utils/legacy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index b8b44135d..1cc0246f3 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -235,6 +235,8 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): protected_urls = persistent.get("protected_urls", []) + ["re:" + r for r in persistent.get("protected_regex", [])] unprotected_urls = persistent.get("unprotected_urls", []) + ["re:" + r for r in persistent.get("unprotected_regex", [])] + known_users = user_list()["users"].keys() + for legacy_rule in legacy_rules: if legacy_rule in persistent: del persistent[legacy_rule] @@ -261,7 +263,7 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): if protected_urls: persistent["permissions"]['custom_protected'] = { - "users": [], + "users": known_users, "label": "Custom permissions - protected", "show_tile": False, "auth_header": True, From b767f4b03301a0d976fb6f9bb5e8c17d3e2ba53e Mon Sep 17 00:00:00 2001 From: Kayou Date: Fri, 11 Dec 2020 17:11:44 +0100 Subject: [PATCH 032/363] fix tests --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 50772f3f7..bfea80ccd 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python:Depends}, ${misc:Depends} - , moulinette (>= 4.1), ssowat (>= 4.0) + , moulinette (>= 4.1.0.1), ssowat (>= 4.0) , python-psutil, python-requests, python-dnspython, python-openssl , python-miniupnpc, python-dbus, python-jinja2 , python-toml, python-packaging, python-publicsuffix From c5d06af20e97e5939bb338ca81e6739e32ae943d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 11 Dec 2020 20:40:45 +0100 Subject: [PATCH 033/363] Guess what ... If people manually tweaked their nginx.conf to enable gzip globally, this will break cert renewal .. --- data/templates/nginx/plain/acme-challenge.conf.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/plain/acme-challenge.conf.inc b/data/templates/nginx/plain/acme-challenge.conf.inc index aae3e0eb3..35c4b80c2 100644 --- a/data/templates/nginx/plain/acme-challenge.conf.inc +++ b/data/templates/nginx/plain/acme-challenge.conf.inc @@ -2,4 +2,5 @@ location ^~ '/.well-known/acme-challenge/' { default_type "text/plain"; alias /tmp/acme-challenge-public/; + gzip off; } From 11fe9d7e025248302177b08fafcd73080350153b Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 15 Dec 2020 22:08:49 +0100 Subject: [PATCH 034/363] [fix] Accept all kind of relay even the unencrypted --- data/templates/postfix/main.cf | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/templates/postfix/main.cf b/data/templates/postfix/main.cf index 6a1029b71..13781881f 100644 --- a/data/templates/postfix/main.cf +++ b/data/templates/postfix/main.cf @@ -53,12 +53,8 @@ smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache smtpd_tls_loglevel=1 # -- TLS for outgoing connections -{% if relay_host != "" %} -smtp_tls_security_level = encrypt -{% else %} # Use TLS if this is supported by the remote SMTP server, otherwise use plaintext. smtp_tls_security_level = may -{% endif %} smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache smtp_tls_exclude_ciphers = aNULL, MD5, DES, ADH, RC4, 3DES smtp_tls_mandatory_ciphers= high From 8cab56787ea632d01d7fa25d324436b3eac310f3 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 17 Dec 2020 16:06:45 +0100 Subject: [PATCH 035/363] handle change php version --- data/helpers.d/php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/php b/data/helpers.d/php index 95cc15402..343529d7a 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -85,6 +85,19 @@ ynh_add_fpm_config () { # Set the default PHP-FPM version by default phpversion="${phpversion:-$YNH_PHP_VERSION}" + local old_phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) + + # If the PHP version changed, remove the old fpm conf + if [ -n "$old_phpversion" ] && [ "$old_phpversion" != "$phpversion" ] + then + local old_php_fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir) + local old_php_finalphpconf="$old_php_fpm_config_dir/pool.d/$app.conf" + + ynh_backup_if_checksum_is_different --file="$old_php_finalphpconf" + + ynh_remove_fpm_config + fi + # If the requested PHP version is not the default version for YunoHost if [ "$phpversion" != "$YNH_DEFAULT_PHP_VERSION" ] then @@ -278,7 +291,7 @@ ynh_remove_fpm_config () { local dedicated_service=$(ynh_app_setting_get --app=$app --key=fpm_dedicated_service) dedicated_service=${dedicated_service:-0} # Get the version of PHP used by this app - local phpversion=$(ynh_app_setting_get $app phpversion) + local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) # Assume default PHP-FPM version by default phpversion="${phpversion:-$YNH_DEFAULT_PHP_VERSION}" @@ -377,7 +390,7 @@ ynh_install_php () { # Requires YunoHost version 3.8.1 or higher. ynh_remove_php () { # Get the version of PHP used by this app - local phpversion=$(ynh_app_setting_get $app phpversion) + local phpversion=$(ynh_app_setting_get --app=$app --key=phpversion) if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] || [ -z "$phpversion" ] then From fc1160f242b59aa01ed66e01e289a1e9d629a89f Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 17 Dec 2020 18:45:31 +0100 Subject: [PATCH 036/363] fix version number in tests --- .gitlab/ci/build.gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab/ci/build.gitlab-ci.yml b/.gitlab/ci/build.gitlab-ci.yml index d2dbbaa80..717a5ee73 100644 --- a/.gitlab/ci/build.gitlab-ci.yml +++ b/.gitlab/ci/build.gitlab-ci.yml @@ -38,7 +38,7 @@ build-ssowat: variables: PACKAGE: "ssowat" script: - - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "ssowat \([>,=,<]+ .*\)" | grep -Po "[0-9]+([.][0-9]+)?" | head -n 1) + - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "ssowat \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE - *build_script @@ -48,7 +48,7 @@ build-moulinette: variables: PACKAGE: "moulinette" script: - - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "moulinette \([>,=,<]+ .*\)" | grep -Po "[0-9]+([.][0-9]+)?" | head -n 1) + - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "moulinette \([>,=,<]+ .*\)" | grep -Po "[0-9\.]+") - git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE - *build_script From 4f1ea2405d28a5166593f0e99f6e87570317515a Mon Sep 17 00:00:00 2001 From: Kayou Date: Fri, 18 Dec 2020 15:11:37 +0100 Subject: [PATCH 037/363] fix my mess --- 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 d6c132535..d32fb59a2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1204,7 +1204,7 @@ def app_setting(app, key, value=None, delete=False): # GET if value is None and not delete: - return ','.join(permission['uris'] + permission['additional_urls']) if permission else None + return ','.join(permission.get('uris', []) + permission['additional_urls']) if permission else None # DELETE if delete: From ff5d8ebd61c5aa2d0ab25f9723f2fc1c2c5b7005 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Dec 2020 15:29:52 +0100 Subject: [PATCH 038/363] Missing ;, probably --- data/helpers.d/php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/php b/data/helpers.d/php index 343529d7a..8bb82b5c2 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -274,7 +274,7 @@ WantedBy=multi-user.target && ynh_systemd_action --service_name=$fpm_service --action=reload \ || { php-fpm${phpversion} --test || true; ynh_secure_remove --file="$finalphpconf"; - ynh_die --message="The new configuration broke php-fpm?" + ynh_die --message="The new configuration broke php-fpm?"; } ynh_systemd_action --service_name=$fpm_service --action=reload fi From 58dcf45bd904380cfeeba8efbc49ed1cb735ede1 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 18 Dec 2020 16:55:39 +0100 Subject: [PATCH 039/363] do not reload php7.3 too fast --- data/helpers.d/php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/helpers.d/php b/data/helpers.d/php index 8bb82b5c2..bd5608594 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -270,12 +270,12 @@ WantedBy=multi-user.target ynh_systemd_action --service_name=$fpm_service --action=restart else # Validate that the new php conf doesn't break php-fpm entirely - php-fpm${phpversion} --test 2>/dev/null \ - && ynh_systemd_action --service_name=$fpm_service --action=reload \ - || { php-fpm${phpversion} --test || true; - ynh_secure_remove --file="$finalphpconf"; - ynh_die --message="The new configuration broke php-fpm?"; - } + if ! php-fpm${phpversion} --test 2>/dev/null + then + php-fpm${phpversion} --test || true; + ynh_secure_remove --file="$finalphpconf"; + ynh_die --message="The new configuration broke php-fpm?"; + fi ynh_systemd_action --service_name=$fpm_service --action=reload fi } From b7c43df661cdc87f32cd424c8315b1d2ffe82378 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 18 Dec 2020 17:03:51 +0100 Subject: [PATCH 040/363] no more ; --- data/helpers.d/php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/php b/data/helpers.d/php index bd5608594..a2caf5387 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -272,9 +272,9 @@ WantedBy=multi-user.target # Validate that the new php conf doesn't break php-fpm entirely if ! php-fpm${phpversion} --test 2>/dev/null then - php-fpm${phpversion} --test || true; - ynh_secure_remove --file="$finalphpconf"; - ynh_die --message="The new configuration broke php-fpm?"; + php-fpm${phpversion} --test || true + ynh_secure_remove --file="$finalphpconf" + ynh_die --message="The new configuration broke php-fpm?" fi ynh_systemd_action --service_name=$fpm_service --action=reload fi From f9143d531f3623726e8cc364aa5ac366188d5300 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 18 Dec 2020 17:53:05 +0100 Subject: [PATCH 041/363] Fix a small issue where metadata could be None (because of empty yaml maybe?) --- 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 11b9034c6..cf108b989 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -89,14 +89,14 @@ def log_list(limit=None, with_details=False, with_suboperations=False): pass try: - metadata = read_yaml(md_path) + metadata = read_yaml(md_path) or {} # Making sure this is a dict and not None..? except Exception as e: # If we can't read the yaml for some reason, report an error and ignore this entry... logger.error(m18n.n('log_corrupted_md_file', md_file=md_path, error=e)) continue if with_details: - entry["success"] = metadata.get("success", "?") if metadata else "?" + entry["success"] = metadata.get("success", "?") entry["parent"] = metadata.get("parent") if with_suboperations: From 0c98fd0c7e9b1035ce563cc31ab95680312ec95a Mon Sep 17 00:00:00 2001 From: Eric COURTEAU Date: Fri, 4 Dec 2020 10:16:12 +0000 Subject: [PATCH 042/363] Translated using Weblate (French) Currently translated at 100.0% (630 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 6276ff6df..ea7228e41 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -129,7 +129,7 @@ "service_removed": "Le service « {service:s} » a été supprimé", "service_start_failed": "Impossible de démarrer le service '{service:s}'\n\nJournaux historisés récents : {logs:s}", "service_started": "Le service « {service:s} » a été démarré", - "service_stop_failed": "Impossible d’arrêter le service '{service:s}'\n\nJournaux historisés récents : {logs:s}", + "service_stop_failed": "Impossible d’arrêter le service '{service:s}'\n\nJournaux récents de service : {logs:s}", "service_stopped": "Le service « {service:s} » a été arrêté", "service_unknown": "Le service '{service:s}' est inconnu", "ssowat_conf_generated": "La configuration de SSOwat a été regénérée", @@ -252,7 +252,7 @@ "app_upgrade_some_app_failed": "Certaines applications n’ont pas été mises à jour", "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’application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l’application '{other_app}'", + "app_make_default_location_already_used": "Impossible de configurer l’application '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par l'application '{other_app}'", "app_upgrade_app_name": "Mise à jour de {app}...", "backup_output_symlink_dir_broken": "Votre répertoire d’archivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", @@ -404,7 +404,7 @@ "dpkg_lock_not_available": "Cette commande ne peut pas être exécutée pour le moment car un autre programme semble utiliser le verrou de dpkg (le gestionnaire de package système)", "tools_upgrade_cant_unhold_critical_packages": "Impossible d'enlever le drapeau 'hold' pour les paquets critiques…", "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à l'administrateur Web. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant la « liste des journaux yunohost » (à partir de la ligne de commande).", - "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", + "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}\n{sourceslist}", "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "backup_permission": "Permission de sauvegarde pour {app:s}", "group_created": "Le groupe '{group}' a été créé", @@ -419,7 +419,7 @@ "mailbox_disabled": "La boîte aux lettres est désactivée pour l’utilisateur {user:s}", "app_action_broke_system": "Cette action semble avoir cassé des services importants : {services}", "apps_already_up_to_date": "Toutes les applications sont déjà à jour", - "migration_0011_create_group": "Création d’un groupe pour chaque utilisateur…", + "migration_0011_create_group": "Création d'un groupe pour chaque utilisateur…", "migration_0011_done": "Migration terminée. Vous êtes maintenant en mesure de gérer des groupes d’utilisateurs.", "migrations_must_provide_explicit_targets": "Vous devez fournir des cibles explicites lorsque vous utilisez '--skip' ou '--force-rerun'", "migrations_no_such_migration": "Il n’y a pas de migration appelée '{id}'", @@ -649,8 +649,8 @@ "global_settings_setting_backup_compress_tar_archives": "Compresser les archives (.tar.gz) au lieu des archives non-compressées lors de la création des backups. N.B. : activer cette option permet d'obtenir des sauvegardes plus légères, mais leur création sera significativement plus longue et plus gourmande en CPU.", "migration_description_0018_xtable_to_nftable": "Migrer les anciennes règles de trafic réseau vers le nouveau système basé sur nftables", "service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX", - "migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable legacy a échoué :", - "migration_0018_failed_to_migrate_iptables_rules": "Échec de la migration des anciennes règles iptables vers nftables : {error}", + "migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}", + "migration_0018_failed_to_migrate_iptables_rules": "La migration des règles iptables héritées vers nftables a échoué: {error}", "migration_0017_not_enough_space": "Laissez suffisamment d'espace disponible dans {path} avant de lancer la migration.", "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 est installé mais pas posgreSQL 11 ? Il s'est sans doute passé quelque chose d'étrange sur votre système :(...", "migration_0017_postgresql_96_not_installed": "PostgreSQL n'a pas été installé sur votre système. Aucune opération à effectuer.", @@ -669,8 +669,8 @@ "diagnosis_package_installed_from_sury_details": "Certains paquets ont été installés par inadvertance à partir d'un dépôt tiers appelé Sury. L'équipe YunoHost a amélioré la stratégie de gestion de ces paquets, mais on s'attend à ce que certaines configurations qui ont installé des applications PHP7.3 tout en étant toujours sur Stretch présentent des incohérences. Pour résoudre cette situation, vous devez essayer d'exécuter la commande suivante : {cmd_to_fix} ", "app_argument_password_no_default": "Erreur lors de l'analyse de l'argument de mot de passe '{name}' : l'argument de mot de passe ne peut pas avoir de valeur par défaut pour des raisons de sécurité", "pattern_email_forward": "Il doit s'agir d'une adresse électronique valide, le symbole '+' étant accepté (par exemples : johndoe@exemple.com ou bien johndoe+yunohost@exemple.com)", - "global_settings_setting_smtp_relay_password": "Mot de passe SMTP du serveur de courrier électronique", - "diagnosis_package_installed_from_sury": "Certains paquets du système devraient être rétrograder vers une version moins récente", + "global_settings_setting_smtp_relay_password": "Mot de passe du relais de l'hôte SMTP", + "diagnosis_package_installed_from_sury": "Des paquets du système devraient être rétrogradé de version", "additional_urls_already_added": "URL supplémentaire '{url:s}' déjà ajoutée pour la permission '{permission:s}'", "unknown_main_domain_path": "Domaine ou chemin inconnu pour '{app}'. Vous devez spécifier un domaine et un chemin pour pouvoir spécifier une URL pour l'autorisation.", "show_tile_cant_be_enabled_for_regex": "Vous ne pouvez pas activer 'show_tile' pour le moment, car l'URL de l'autorisation '{permission}' est une expression régulière", @@ -687,5 +687,8 @@ "invalid_regex": "Regex non valide : '{regex:s}'", "domain_name_unknown": "Domaine '{domain}' inconnu", "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", - "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimée pour la permission '{permission:s}'" + "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimée pour la permission '{permission:s}'", + "migration_0019_rollback_success": "Retour à l'état antérieur du système.", + "invalid_number": "Doit être un nombre", + "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives" } From 3608af5912f1505423d79976a33da6e48bb7d07f Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Mon, 7 Dec 2020 16:26:32 +0000 Subject: [PATCH 043/363] Translated using Weblate (Italian) Currently translated at 41.5% (262 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index e70f00a37..892f6e738 100644 --- a/locales/it.json +++ b/locales/it.json @@ -412,5 +412,23 @@ "diagnosis_failed_for_category": "Diagnosi fallita per la categoria '{category}:{error}", "diagnosis_display_tip": "Per vedere i problemi rilevati, puoi andare alla sezione Diagnosi del amministratore, o eseguire 'yunohost diagnosis show --issues' dalla riga di comando.", "diagnosis_package_installed_from_sury_details": "Alcuni pacchetti sono stati inavvertitamente installati da un repository di terze parti chiamato Sury. Il team di Yunohost ha migliorato la gestione di tali pacchetti, ma ci si aspetta che alcuni setup di app PHP7.3 abbiano delle incompatibilità anche se sono ancora in Stretch. Per sistemare questa situazione, dovresti provare a lanciare il seguente comando: {cmd_to_fix}", - "diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade" + "diagnosis_package_installed_from_sury": "Alcuni pacchetti di sistema dovrebbero fare il downgrade", + "diagnosis_mail_ehlo_bad_answer": "Un servizio diverso da SMTP ha risposto sulla porta 25 su IPv{ipversion}", + "diagnosis_mail_ehlo_unreachable_details": "Impossibile aprire una connessione sulla porta 25 sul tuo server su IPv{ipversion}. Sembra irraggiungibile.
1. La causa più probabile di questo problema è la porta 25 non correttamente inoltrata al tuo server.
2. Dovresti esser sicuro che il servizio postfix sia attivo.
3. Su setup complessi: assicuratu che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_mail_ehlo_unreachable": "Il server SMTP non è raggiungibile dall'esterno su IPv{ipversion}. Non potrà ricevere email.", + "diagnosis_mail_ehlo_ok": "Il server SMTP è raggiungibile dall'esterno e quindi può ricevere email!", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Alcuni provider non ti permettono di aprire la porta 25 in uscita perché non gli importa della Net Neutrality.
- Alcuni mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare per un provider pro Net Neutrality", + "diagnosis_mail_outgoing_port_25_blocked_details": "Come prima cosa dovresti sbloccare la porta 25 in uscita dall'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).", + "diagnosis_mail_outgoing_port_25_blocked": "Il server SMTP non può inviare email ad altri server perché la porta 25 è bloccata in uscita su IPv{ipversion}.", + "diagnosis_mail_outgoing_port_25_ok": "Il server SMTP è abile all'invio delle email (porta 25 in uscita non bloccata).", + "diagnosis_swap_tip": "Attenzione. Sii consapevole che se il server ha lo swap su di una memoria SD o un disco SSD, potrebbe drasticamente ridurre la durata di vita del dispositivo.", + "diagnosis_swap_ok": "Il sistema ha {total} di memoria swap!", + "diagnosis_swap_notsomuch": "Il sistema ha solo {total} di swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.", + "diagnosis_swap_none": "Il sistema non ha lo swap. Dovresti considerare almeno di aggiungere {recommended} di memoria swap per evitare situazioni dove il sistema esaurisce la memoria.", + "diagnosis_ram_ok": "Il sistema ha ancora {available} ({available_percent}%) di RAM disponibile su {total}.", + "diagnosis_ram_low": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total}). Fa attenzione.", + "diagnosis_ram_verylow": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total})", + "diagnosis_diskusage_ok": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total})!", + "diagnosis_diskusage_low": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Fa attenzione.", + "diagnosis_diskusage_verylow": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Dovresti seriamente considerare di fare un po' di pulizia!" } From 1044e826556fa686e9cdd6a91816448b8ecaf931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Wed, 9 Dec 2020 16:35:27 +0000 Subject: [PATCH 044/363] Translated using Weblate (Occitan) Currently translated at 54.2% (342 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/oc/ --- locales/oc.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 1ef8348bd..17201fefe 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -590,5 +590,8 @@ "app_manifest_install_ask_password": "Causissètz lo senhal administrator per aquesta aplicacion", "app_manifest_install_ask_path": "Causissètz lo camin ont volètz installar aquesta aplicacion", "app_manifest_install_ask_domain": "Causissètz lo domeni ont volètz installar aquesta aplicacion", - "app_argument_password_no_default": "Error pendent l’analisi de l’argument del senhal « {name} » : l’argument de senhal pòt pas aver de valor per defaut per de rason de seguretat" + "app_argument_password_no_default": "Error pendent l’analisi de l’argument del senhal « {name} » : l’argument de senhal pòt pas aver de valor per defaut per de rason de seguretat", + "app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication", + "additional_urls_already_removed": "URL addicionala {url:s} es ja estada elimida per la permission «#permission:s»", + "additional_urls_already_added": "URL addicionadal «{url:s}'» es ja estada aponduda per la permission «{permission:s}»" } From 9fb7684e0ffc2f912c30cbfa4fba9f7af16aa118 Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Fri, 11 Dec 2020 11:33:43 +0000 Subject: [PATCH 045/363] Translated using Weblate (Italian) Currently translated at 53.1% (335 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 99 ++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/locales/it.json b/locales/it.json index 892f6e738..085bd506a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -9,8 +9,8 @@ "backup_created": "Backup completo", "backup_invalid_archive": "Archivio di backup non valido", "backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota", - "domain_created": "Il dominio è stato creato", - "domain_exists": "Il dominio è già esistente", + "domain_created": "Dominio creato", + "domain_exists": "Il dominio esiste già", "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", @@ -23,7 +23,7 @@ "service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs: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…", + "upgrading_packages": "Aggiornamento dei pacchetti...", "user_deleted": "L'utente è stato cancellato", "admin_password": "Password dell'amministrazione", "admin_password_change_failed": "Impossibile cambiare la password", @@ -63,34 +63,34 @@ "backup_output_directory_required": "Devi fornire una directory di output per il backup", "backup_running_hooks": "Esecuzione degli hook di backup…", "custom_app_url_required": "Devi fornire un URL per essere in grado di aggiornare l'applicazione personalizzata {app:s}", - "domain_creation_failed": "Impossibile creare un dominio", - "domain_deleted": "Il dominio è stato cancellato", - "domain_deletion_failed": "Impossibile cancellare il dominio", + "domain_creation_failed": "Impossibile creare il dominio {domain}: {error}", + "domain_deleted": "Dominio cancellato", + "domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}", "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_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio: {apps}. Disinstallale prima di procedere alla cancellazione di un dominio", "domain_unknown": "Dominio sconosciuto", "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_cron_installed": "Cronjob DynDNS creato", + "dyndns_cron_remove_failed": "Impossibile rimuovere il cronjob DynDNS perchè: {error}", + "dyndns_cron_removed": "Cronjob DynDNS 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": "Si sta generando la chiave DNS, potrebbe richiedere del tempo…", + "dyndns_ip_updated": "Il tuo indirizzo IP è stato aggiornato su DynDNS", + "dyndns_key_generating": "Generando la chiave DNS... 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_no_domain_registered": "Nessuno dominio registrato con DynDNS", + "dyndns_registered": "Dominio DynDNS registrato", "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}", - "dyndns_unavailable": "Dominio {domain:s} non disponibile.", + "dyndns_unavailable": "Il dominio {domain:s} non disponibile.", "executing_command": "Esecuzione del comando '{command:s}'…", "executing_script": "Esecuzione dello script '{script:s}'…", - "extracting": "Estrazione…", + "extracting": "Estrazione...", "field_invalid": "Campo '{:s}' non valido", "firewall_reload_failed": "Impossibile ricaricare il firewall", - "firewall_reloaded": "Il firewall è stato ricaricato", + "firewall_reloaded": "Firewall ricaricato", "firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.", "hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}", "hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}", @@ -128,7 +128,7 @@ "user_update_failed": "Impossibile aggiornare l'utente", "restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", "restore_nothings_done": "Non è stato ripristinato nulla", - "restore_running_app_script": "Esecuzione dello script di ripristino dell'applicazione '{app:s}'…", + "restore_running_app_script": "Ripristino dell'app '{app:s}'…", "restore_running_hooks": "Esecuzione degli hook di ripristino…", "service_added": "Il servizio '{service:s}' è stato aggiunto", "service_already_started": "Il servizio '{service:s}' è già stato avviato", @@ -146,7 +146,7 @@ "unbackup_app": "L'applicazione '{app:s}' non verrà salvata", "unexpected_error": "Un'errore inaspettata si è verificata", "unlimit": "Nessuna quota", - "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema…", + "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema...", "upgrade_complete": "Aggiornamento completo", "upnp_dev_not_found": "Nessuno supporto UPnP trovato", "upnp_disabled": "UPnP è stato disattivato", @@ -161,7 +161,7 @@ "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_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)", @@ -232,7 +232,7 @@ "password_too_simple_3": "La password deve essere lunga almeno 8 caratteri e contenere numeri, maiuscole e minuscole e simboli", "password_too_simple_4": "La password deve essere lunga almeno 12 caratteri e contenere numeri, maiuscole e minuscole", "users_available": "Utenti disponibili:", - "yunohost_ca_creation_success": "L'autorità di certificazione locale è stata creata.", + "yunohost_ca_creation_success": "Autorità di certificazione locale creata.", "app_action_cannot_be_ran_because_required_services_down": "I seguenti servizi dovrebbero essere in funzione per completare questa azione: {services}. Prova a riavviarli per proseguire (e possibilmente cercare di capire come ma non funzionano più).", "backup_output_symlink_dir_broken": "La tua cartella d'archivio '{path:s}' è un link simbolico interrotto. Probabilmente hai dimenticato di montare o montare nuovamente il supporto al quale punta il link.", "certmanager_conflicting_nginx_file": "Impossibile preparare il dominio per il controllo ACME: il file di configurazione nginx {filepath:s} è in conflitto e dovrebbe essere prima rimosso", @@ -247,47 +247,47 @@ "confirm_app_install_warning": "Attenzione: Questa applicazione potrebbe funzionare, ma non è ben integrata in YunoHost. Alcune funzionalità come il single sign-on e il backup/ripristino potrebbero non essere disponibili. Installare comunque? [{answers:s}] ", "confirm_app_install_danger": "ATTENZIONE! Questa applicazione è ancora sperimentale (se non esplicitamente dichiarata non funzionante)! Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio,digita '{answers:s}'", "confirm_app_install_thirdparty": "PERICOLO! Quest'applicazione non fa parte del catalogo Yunohost. Installando app di terze parti potresti compromettere l'integrita e la sicurezza del tuo sistema. Probabilmente NON dovresti installarla a meno che tu non sappia cosa stai facendo. NESSUN SUPPORTO verrà dato se quest'app non funziona o se rompe il tuo sistema... Se comunque accetti di prenderti questo rischio, digita '{answers:s}'", - "dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/apt (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo dpkg --configure -a`.", - "domain_cannot_remove_main": "Non è possibile rimuovere il dominio principale ora. Prima imposta un nuovo dominio principale", - "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra qual è la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", + "dpkg_is_broken": "Non puoi eseguire questo ora perchè dpkg/APT (i gestori di pacchetti del sistema) sembrano essere in stato danneggiato... Puoi provare a risolvere il problema connettendoti via SSH ed eseguire `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", + "domain_cannot_remove_main": "Non puoi rimuovere '{domain:s}' essendo il dominio principale, prima devi impostare un nuovo dominio principale con il comando 'yunohost domain main-domain -n '; ecco la lista dei domini candidati: {other_domains:s}", + "domain_dns_conf_is_just_a_recommendation": "Questo comando ti mostra la configurazione *raccomandata*. Non ti imposta la configurazione DNS al tuo posto. È tua responsabilità configurare la tua zona DNS nel tuo registrar in accordo con queste raccomandazioni.", "dyndns_could_not_check_provide": "Impossibile controllare se {provider:s} possano fornire {domain:s}.", "dyndns_could_not_check_available": "Impossibile controllare se {domain:s} è disponibile su {provider:s}.", - "dyndns_domain_not_provided": "Il fornitore Dyndns {provider:s} non può fornire il dominio {domain:s}.", - "experimental_feature": "Attenzione: questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.", + "dyndns_domain_not_provided": "Il fornitore DynDNS {provider:s} non può fornire il dominio {domain:s}.", + "experimental_feature": "Attenzione: Questa funzionalità è sperimentale e non è considerata stabile, non dovresti utilizzarla a meno che tu non sappia cosa stai facendo.", "file_does_not_exist": "Il file {path:s} non esiste.", - "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting:s}, ricevuta '{choice:s}' ma le scelte disponibili sono : {available_choices:s}", + "global_settings_bad_choice_for_enum": "Scelta sbagliata per l'impostazione {setting:s}, ricevuta '{choice:s}', ma le scelte disponibili sono: {available_choices:s}", "global_settings_bad_type_for_setting": "Tipo errato per l'impostazione {setting:s}, ricevuto {received_type:s}, atteso {expected_type:s}", "global_settings_cant_open_settings": "Apertura del file delle impostazioni non riuscita, motivo: {reason:s}", "global_settings_cant_serialize_settings": "Serializzazione dei dati delle impostazioni non riuscita, motivo: {reason:s}", "global_settings_cant_write_settings": "Scrittura del file delle impostazioni non riuscita, motivo: {reason:s}", "global_settings_key_doesnt_exists": "La chiave '{settings_key:s}' non esiste nelle impostazioni globali, puoi vedere tutte le chiavi disponibili eseguendo 'yunohost settings list'", - "global_settings_reset_success": "Successo. Le tue impostazioni precedenti sono state salvate in {path:s}", + "global_settings_reset_success": "Le impostazioni precedenti sono state salvate in {path:s}", "global_settings_setting_example_bool": "Esempio di opzione booleana", "global_settings_setting_example_enum": "Esempio di opzione enum", "already_up_to_date": "Niente da fare. Tutto è già aggiornato.", "global_settings_setting_example_int": "Esempio di opzione int", "global_settings_setting_example_string": "Esempio di opzione string", - "global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web nginx. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", + "global_settings_setting_security_nginx_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server web NGIX. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "global_settings_setting_security_password_admin_strength": "Complessità della password di amministratore", "global_settings_setting_security_password_user_strength": "Complessità della password utente", "global_settings_setting_security_ssh_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server SSH. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "global_settings_unknown_setting_from_settings_file": "Chiave sconosciuta nelle impostazioni: '{setting_key:s}', scartata e salvata in /etc/yunohost/settings-unknown.json", - "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del (deprecato) hostkey DSA per la configurazione del demone SSH", + "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Consenti l'uso del hostkey DSA (deprecato) per la configurazione del demone SSH", "global_settings_unknown_type": "Situazione inaspettata, l'impostazione {setting:s} sembra essere di tipo {unknown_type:s} ma non è un tipo supportato dal sistema.", "good_practices_about_admin_password": "Stai per definire una nuova password di amministratore. La password deve essere almeno di 8 caratteri - anche se è buona pratica utilizzare password più lunghe (es. una frase, una serie di parole) e/o utilizzare vari tipi di caratteri (maiuscole, minuscole, numeri e simboli).", - "log_corrupted_md_file": "Il file dei metadati yaml associato con i registri è corrotto: '{md_file}'", + "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", "log_category_404": "La categoria di registrazione '{category}' non esiste", "log_link_to_log": "Registro completo di questa operazione: '{desc}'", "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log display {name}'", "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", - "log_link_to_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", - "log_help_to_get_failed_log": "L'operazione '{desc}' è fallita! Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log display {name} --share'", + "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", + "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log display {name} --share'", "log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili", - "log_app_change_url": "Cambia l'url dell'applicazione '{}'", - "log_app_install": "Installa l'applicazione '{}'", - "log_app_remove": "Rimuovi l'applicazione '{}'", - "log_app_upgrade": "Aggiorna l'applicazione '{}'", - "log_app_makedefault": "Rendi predefinita l'applicazione '{}'", + "log_app_change_url": "Cambia l'URL dell'app '{}'", + "log_app_install": "Installa l'app '{}'", + "log_app_remove": "Rimuovi l'app '{}'", + "log_app_upgrade": "Aggiorna l'app '{}'", + "log_app_makedefault": "Rendi '{}' l'app predefinita", "log_available_on_yunopaste": "Questo registro è ora disponibile via {url}", "log_backup_restore_system": "Ripristina sistema da un archivio di backup", "log_backup_restore_app": "Ripristina '{}' da un archivio di backup", @@ -296,16 +296,16 @@ "log_domain_add": "Aggiungi il dominio '{}' nella configurazione di sistema", "log_domain_remove": "Rimuovi il dominio '{}' dalla configurazione di sistema", "log_dyndns_subscribe": "Sottoscrivi un sottodominio YunoHost '{}'", - "log_dyndns_update": "Aggiorna l'ip associato con il tuo sottodominio YunoHost '{}'", + "log_dyndns_update": "Aggiorna l'IP associato con il tuo sottodominio YunoHost '{}'", "log_letsencrypt_cert_install": "Installa un certificato Let's encrypt sul dominio '{}'", "log_selfsigned_cert_install": "Installa un certificato autofirmato sul dominio '{}'", - "log_letsencrypt_cert_renew": "Rinnova il certificato Let's encrypt sul dominio '{}'", + "log_letsencrypt_cert_renew": "Rinnova il certificato Let's Encrypt sul dominio '{}'", "log_regen_conf": "Rigenera configurazioni di sistema '{}'", "log_user_create": "Aggiungi l'utente '{}'", "log_user_delete": "Elimina l'utente '{}'", - "log_user_update": "Aggiornate le informazioni dell'utente '{}'", + "log_user_update": "Aggiorna le informazioni dell'utente '{}'", "log_domain_main_domain": "Rendi '{}' dominio principale", - "log_tools_migrations_migrate_forward": "Migra avanti", + "log_tools_migrations_migrate_forward": "Esegui le migrazioni", "log_tools_postinstall": "Postinstallazione del tuo server YunoHost", "log_tools_upgrade": "Aggiornamento dei pacchetti di sistema", "log_tools_shutdown": "Spegni il tuo server", @@ -334,12 +334,12 @@ "migration_0003_yunohost_upgrade": "Iniziando l'aggiornamento dei pacchetti yunohost… La migrazione terminerà, ma l'aggiornamento attuale avverrà subito dopo. Dopo che l'operazione sarà completata, probabilmente dovrai riaccedere all'interfaccia di amministrazione.", "migration_0003_not_jessie": "La distribuzione attuale non è Jessie!", "migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.", - "this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/apt (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo dpkg --configure -a`.", + "this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/APT (i gestori di pacchetti del sistema)... Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo apt install --fix-broken` e/o `sudo dpkg --configure -a`.", "app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}", "app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione...", "app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione", "app_install_failed": "Impossibile installare {app}:{error}", - "app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono state installate sul dominio '{domain}'. Dovresti invece usare un sotto-dominio dedicato per questa app.", + "app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono già installate sul dominio '{domain}'. Potresti usare invece un sotto-dominio dedicato per questa app.", "app_upgrade_script_failed": "È stato trovato un errore nello script di aggiornamento dell'applicazione", "apps_already_up_to_date": "Tutte le applicazioni sono aggiornate", "apps_catalog_init_success": "Catalogo delle applicazioni inizializzato!", @@ -357,7 +357,7 @@ "app_manifest_install_ask_admin": "Scegli un utente amministratore per quest'applicazione", "app_manifest_install_ask_password": "Scegli una password di amministrazione per quest'applicazione", "app_manifest_install_ask_path": "Scegli il percorso dove installare quest'applicazione", - "app_manifest_install_ask_domain": "Scegli il dominio sotto il quale installare quest'applicazione", + "app_manifest_install_ask_domain": "Scegli il dominio dove installare quest'app", "app_argument_password_no_default": "Errore durante il parsing dell'argomento '{name}': l'argomento password non può avere un valore di default per ragioni di sicurezza", "additional_urls_already_added": "L'URL aggiuntivo '{url:s}' è già utilizzato come URL aggiuntivo per il permesso '{permission:s}'", "diagnosis_basesystem_ynh_inconsistent_versions": "Stai eseguendo versioni incompatibili dei pacchetti YunoHost... probabilmente a causa di aggiornamenti falliti o parziali.", @@ -430,5 +430,12 @@ "diagnosis_ram_verylow": "Il sistema ha solo {available} ({available_percent}%) di RAM disponibile (su {total})", "diagnosis_diskusage_ok": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total})!", "diagnosis_diskusage_low": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Fa attenzione.", - "diagnosis_diskusage_verylow": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Dovresti seriamente considerare di fare un po' di pulizia!" + "diagnosis_diskusage_verylow": "Lo storage {mountpoint} (nel device {device} ha solo {free} ({free_percent}%) di spazio libero rimanente (su {total}). Dovresti seriamente considerare di fare un po' di pulizia!", + "diagnosis_mail_fcrdns_nok_details": "Dovresti prima configurare il DNS inverso con {ehlo_domain} nell'interfaccia del tuo router internet o del tuo hosting provider. (Alcuni hosting provider potrebbero richiedere l'invio di un ticket di supporto per la richiesta).", + "diagnosis_mail_fcrdns_dns_missing": "Nessun DNS inverso è configurato per IPv{ipversion}. Alcune email potrebbero non essere inviate o segnalate come spam.", + "diagnosis_mail_fcrdns_ok": "Il tuo DNS inverso è configurato correttamente!", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Errore: {error}", + "diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.", + "diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.", + "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server." } From 6360f46972df369e57923e25993206d4ad524bb5 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 14 Dec 2020 19:27:41 +0000 Subject: [PATCH 046/363] Translated using Weblate (German) Currently translated at 57.4% (362 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index d85879bf4..9c8fa33c8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -298,7 +298,7 @@ "log_app_makedefault": "Mache '{}' zur Standard-Anwendung", "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}", "app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.", - "app_install_failed": "Installation von {app} fehlgeschlagen: {error}", + "app_install_failed": "{app} kann nicht installiert werden: {error}", "app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten", "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation...", "app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten", @@ -469,5 +469,7 @@ "additional_urls_already_added": "Zusätzliche URL '{url:s}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission:s}'", "additional_urls_already_removed": "Zusätzliche URL '{url:s}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission:s}'", "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutzen Sie den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", - "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen" + "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen", + "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.", + "diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren." } From 1a92a905aa52884a7cb04d028857df6ccaf55ee3 Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Mon, 14 Dec 2020 16:13:36 +0000 Subject: [PATCH 047/363] Translated using Weblate (Italian) Currently translated at 58.4% (368 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locales/it.json b/locales/it.json index 085bd506a..b3f2dc718 100644 --- a/locales/it.json +++ b/locales/it.json @@ -11,13 +11,13 @@ "backup_output_directory_not_empty": "Dovresti scegliere una cartella di output vuota", "domain_created": "Dominio creato", "domain_exists": "Il dominio esiste già", - "ldap_initialized": "LDAP è stato inizializzato", - "pattern_email": "L'indirizzo email deve essere valido (es. someone@domain.org)", + "ldap_initialized": "LDAP inizializzato", + "pattern_email": "L'indirizzo email deve essere valido, senza simboli '+' (es. tizio@dominio.com)", "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", "service_add_failed": "Impossibile aggiungere il servizio '{service:s}'", "service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'", - "service_disabled": "Il servizio '{service:s}' è stato disattivato", + "service_disabled": "Il servizio '{service:s}' non partirà più al boot di sistema.", "service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'", "service_removed": "Il servizio '{service:s}' è stato rimosso", "service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", @@ -68,7 +68,7 @@ "domain_deletion_failed": "Impossibile cancellare il dominio {domain}: {error}", "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_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).", "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio: {apps}. Disinstallale prima di procedere alla cancellazione di un dominio", "domain_unknown": "Dominio sconosciuto", "done": "Terminato", @@ -92,18 +92,18 @@ "firewall_reload_failed": "Impossibile ricaricare il firewall", "firewall_reloaded": "Firewall ricaricato", "firewall_rules_cmd_failed": "Alcune regole del firewall sono fallite. Per ulteriori informazioni, vedi il registro.", - "hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}", - "hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}", + "hook_exec_failed": "Impossibile eseguire lo script: {path:s}", + "hook_exec_not_terminated": "Los script non è stato eseguito correttamente: {path:s}", "hook_name_unknown": "Nome di hook '{name:s}' sconosciuto", "installation_complete": "Installazione completata", - "installation_failed": "Installazione fallita", + "installation_failed": "Qualcosa è andato storto durante l'installazione", "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", "mail_alias_remove_failed": "Impossibile rimuovere l'alias mail '{mail:s}'", - "mail_domain_unknown": "Dominio d'indirizzo mail '{domain:s}' sconosciuto", + "mail_domain_unknown": "Indirizzo mail non valido per il dominio '{domain:s}'. Usa un dominio gestito da questo server.", "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", + "mailbox_used_space_dovecot_down": "La casella di posta elettronica Dovecot deve essere attivato se vuoi recuperare lo spazio usato dalla posta elettronica", "main_domain_change_failed": "Impossibile cambiare il dominio principale", "main_domain_changed": "Il dominio principale è stato cambiato", "no_internet_connection": "Il server non è collegato a Internet", @@ -119,7 +119,7 @@ "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}", - "restore_already_installed_app": "Un'applicazione è già installata con l'identificativo '{app:s}'", + "restore_already_installed_app": "Un'applicazione con l'ID '{app:s}' è già installata", "restore_app_failed": "Impossibile ripristinare l'applicazione '{app:s}'", "restore_cleaning_failed": "Impossibile pulire la directory temporanea di ripristino", "restore_complete": "Ripristino completo", @@ -127,15 +127,15 @@ "restore_failed": "Impossibile ripristinare il sistema", "user_update_failed": "Impossibile aggiornare l'utente", "restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", - "restore_nothings_done": "Non è stato ripristinato nulla", + "restore_nothings_done": "Nulla è stato ripristinato", "restore_running_app_script": "Ripristino dell'app '{app:s}'…", "restore_running_hooks": "Esecuzione degli hook di ripristino…", "service_added": "Il servizio '{service:s}' è stato aggiunto", - "service_already_started": "Il servizio '{service:s}' è già stato avviato", + "service_already_started": "Il servizio '{service:s}' è già avviato", "service_already_stopped": "Il servizio '{service:s}' è già stato fermato", - "service_disable_failed": "Impossibile disabilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", - "service_enable_failed": "Impossibile abilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", - "service_enabled": "Il servizio '{service:s}' è stato attivato", + "service_disable_failed": "Impossibile disabilitare l'avvio al boot del servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", + "service_enable_failed": "Impossibile eseguire il servizio '{service:s}' al boot di sistema.\n\nRegistri di servizio recenti:{logs:s}", + "service_enabled": "Il servizio '{service:s}' si avvierà automaticamente al boot di sistema.", "service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", "service_started": "Il servizio '{service:s}' è stato avviato", "service_stopped": "Il servizio '{service:s}' è stato fermato", @@ -304,7 +304,7 @@ "log_user_create": "Aggiungi l'utente '{}'", "log_user_delete": "Elimina l'utente '{}'", "log_user_update": "Aggiorna le informazioni dell'utente '{}'", - "log_domain_main_domain": "Rendi '{}' dominio principale", + "log_domain_main_domain": "Rendi '{}' il dominio principale", "log_tools_migrations_migrate_forward": "Esegui le migrazioni", "log_tools_postinstall": "Postinstallazione del tuo server YunoHost", "log_tools_upgrade": "Aggiornamento dei pacchetti di sistema", From 602ca3752a2942e4fa386637df2cf6c0a984beaf Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Tue, 15 Dec 2020 10:49:08 +0000 Subject: [PATCH 048/363] Translated using Weblate (Italian) Currently translated at 63.0% (397 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/locales/it.json b/locales/it.json index b3f2dc718..fc22b76e7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -19,12 +19,12 @@ "service_cmd_exec_failed": "Impossibile eseguire il comando '{command:s}'", "service_disabled": "Il servizio '{service:s}' non partirà più al boot di sistema.", "service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'", - "service_removed": "Il servizio '{service:s}' è stato rimosso", + "service_removed": "Servizio '{service:s}' rimosso", "service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", - "system_username_exists": "il nome utente esiste già negli utenti del sistema", - "unrestore_app": "L'applicazione '{app:s}' non verrà ripristinata", + "system_username_exists": "Il nome utente esiste già negli utenti del sistema", + "unrestore_app": "{app:s} non verrà ripristinata", "upgrading_packages": "Aggiornamento dei pacchetti...", - "user_deleted": "L'utente è stato cancellato", + "user_deleted": "Utente cancellato", "admin_password": "Password dell'amministrazione", "admin_password_change_failed": "Impossibile cambiare la password", "admin_password_changed": "La password d'amministrazione è stata cambiata", @@ -110,7 +110,7 @@ "not_enough_disk_space": "Non c'è abbastanza spazio libero in '{path:s}'", "package_unknown": "Pacchetto '{pkgname}' sconosciuto", "packages_upgrade_failed": "Impossibile aggiornare tutti i pacchetti", - "pattern_backup_archive_name": "Deve essere un nome di file valido con caratteri alfanumerici e -_. soli", + "pattern_backup_archive_name": "Deve essere un nome di file valido di massimo 30 caratteri di lunghezza, con caratteri alfanumerici e \"-_.\" come unica punteggiatura", "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", @@ -125,7 +125,7 @@ "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", + "user_update_failed": "Impossibile aggiornare l'utente {user}: {error}", "restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio", "restore_nothings_done": "Nulla è stato ripristinato", "restore_running_app_script": "Ripristino dell'app '{app:s}'…", @@ -137,32 +137,32 @@ "service_enable_failed": "Impossibile eseguire il servizio '{service:s}' al boot di sistema.\n\nRegistri di servizio recenti:{logs:s}", "service_enabled": "Il servizio '{service:s}' si avvierà automaticamente al boot di sistema.", "service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", - "service_started": "Il servizio '{service:s}' è stato avviato", - "service_stopped": "Il servizio '{service:s}' è stato fermato", + "service_started": "Servizio '{service:s}' avviato", + "service_stopped": "Servizio '{service:s}' fermato", "service_unknown": "Servizio '{service:s}' sconosciuto", - "ssowat_conf_generated": "La configurazione SSOwat è stata generata", - "ssowat_conf_updated": "La configurazione SSOwat è stata aggiornata", - "system_upgraded": "Il sistema è stato aggiornato", - "unbackup_app": "L'applicazione '{app:s}' non verrà salvata", - "unexpected_error": "Un'errore inaspettata si è verificata", + "ssowat_conf_generated": "La configurazione SSOwat rigenerata", + "ssowat_conf_updated": "Configurazione SSOwat aggiornata", + "system_upgraded": "Sistema aggiornato", + "unbackup_app": "{app:s} non verrà salvata", + "unexpected_error": "È successo qualcosa di inatteso: {error}", "unlimit": "Nessuna quota", "updating_apt_cache": "Recupero degli aggiornamenti disponibili per i pacchetti di sistema...", "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", + "upnp_port_open_failed": "Impossibile aprire le porte attraverso UPnP", + "user_created": "Utente creato", + "user_creation_failed": "Impossibile creare l'utente {user}: {error}", + "user_deletion_failed": "Impossibile cancellare l'utente {user}: {error}", + "user_home_creation_failed": "Impossibile creare la 'home' directory del utente", "user_unknown": "Utente sconosciuto: {user:s}", - "user_updated": "L'utente è stato aggiornato", + "user_updated": "Info dell'utente cambiate", "yunohost_already_installed": "YunoHost è già installato", "yunohost_ca_creation_failed": "Impossibile creare una certificate authority", - "yunohost_configured": "YunoHost è stato configurato", + "yunohost_configured": "YunoHost ora è configurato", "yunohost_installing": "Installazione di YunoHost...", - "yunohost_not_installed": "YunoHost non è o non corretamente installato. Esegui 'yunohost tools postinstall'", + "yunohost_not_installed": "YunoHost non è correttamente 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", @@ -437,5 +437,7 @@ "diagnosis_mail_ehlo_could_not_diagnose_details": "Errore: {error}", "diagnosis_mail_ehlo_could_not_diagnose": "Non è possibile verificare se il server mail postfix è raggiungibile dall'esterno su IPv{ipversion}.", "diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.", - "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server." + "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", + "diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo." } From a26d341d59f2416f4e253f35ab439ba9bbc0c167 Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Wed, 16 Dec 2020 12:37:38 +0000 Subject: [PATCH 049/363] Translated using Weblate (Italian) Currently translated at 63.6% (401 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index fc22b76e7..452876803 100644 --- a/locales/it.json +++ b/locales/it.json @@ -439,5 +439,9 @@ "diagnosis_mail_ehlo_wrong": "Un server mail SMTP diverso sta rispondendo su IPv{ipversion}. Probabilmente il tuo server non può ricevere email.", "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", - "diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo." + "diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist.", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti." } From 1441ddea5784867d9cb245203d85ed8535388f79 Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Wed, 16 Dec 2020 16:21:22 +0000 Subject: [PATCH 050/363] Translated using Weblate (Italian) Currently translated at 100.0% (630 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 233 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 2 deletions(-) diff --git a/locales/it.json b/locales/it.json index 452876803..0a30b5790 100644 --- a/locales/it.json +++ b/locales/it.json @@ -440,8 +440,237 @@ "diagnosis_mail_ehlo_bad_answer_details": "Potrebbe essere un'altra macchina a rispondere al posto del tuo server.", "diagnosis_mail_fcrdns_nok_alternatives_4": "Alcuni provider non ti permettono di configurare un DNS inverso (o la loro configurazione non funziona...). Se stai avendo problemi a causa di ciò, considera le seguenti soluzioni:
- Alcuni ISP mettono a disposizione un alternativa attraverso un mail server relay anche se implica che il relay ha la capacità di leggere il vostro traffico email.
- Un alternativa privacy-friendly è quella di usare una VPN *con un indirizzo IP pubblico dedicato* per bypassare questo tipo di limite. Vedi https://yunohost.org/#/vpn_advantage
- Puoi anche prendere in considerazione di cambiare internet provider", "diagnosis_mail_ehlo_wrong_details": "L'EHLO ricevuto dalla diagnostica remota su IPv{ipversion} è differente dal dominio del tuo server.
EHLO ricevuto: {wrong_ehlo}
EHLO atteso: {right_ehlo}
La causa più comune di questo problema è la porta 25 non correttamente inoltrata al tuo server. Oppure assicurati che nessun firewall o reverse-proxy stia interferendo.", - "diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist.", + "diagnosis_mail_blacklist_ok": "Gli IP e i domini utilizzati da questo server non sembrano essere nelle blacklist", "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "DNS invero corrente: {rdns_domain}
Valore atteso: {ehlo_domain}", "diagnosis_mail_fcrdns_different_from_ehlo_domain": "Il DNS inverso non è correttamente configurato su IPv{ipversion}. Alcune email potrebbero non essere spedite o segnalate come SPAM.", - "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti." + "diagnosis_mail_fcrdns_nok_alternatives_6": "Alcuni provider non permettono di configurare un DNS inverso (o non è configurato bene...). Se il tuo DNS inverso è correttamente configurato per IPv4, puoi provare a disabilitare l'utilizzo di IPv6 durante l'invio mail eseguendo yunohost settings set smtp.allow_ipv6 -v off. NB: se esegui il comando non sarà più possibile inviare o ricevere email da i pochi IPv6-only server mail esistenti.", + "yunohost_postinstall_end_tip": "La post-installazione è completata! Per rifinire il tuo setup, considera di:\n\t- aggiungere il primo utente nella sezione 'Utenti' del webadmin (o eseguendo da terminale 'yunohost user create ');\n\t- eseguire una diagnosi alla ricerca di problemi nella sezione 'Diagnosi' del webadmin (o eseguendo da terminale 'yunohost diagnosis run');\n\t- leggere 'Finalizing your setup' e 'Getting to know Yunohost' nella documentazione admin: https://yunohost.org/admindoc.", + "user_already_exists": "L'utente '{user}' esiste già", + "update_apt_cache_warning": "Qualcosa è andato storto mentre eseguivo l'aggiornamento della cache APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", + "update_apt_cache_failed": "Impossibile aggiornare la cache di APT (package manager di Debian). Ecco il dump di sources.list, che potrebbe aiutare ad identificare le linee problematiche:\n{sourceslist}", + "unknown_main_domain_path": "Percorso o dominio sconosciuto per '{app}'. Devi specificare un dominio e un percorso per poter specificare un URL per il permesso.", + "tools_upgrade_special_packages_completed": "Aggiornamento pacchetti YunoHost completato.\nPremi [Invio] per tornare al terminale", + "tools_upgrade_special_packages_explanation": "L'aggiornamento speciale continuerà in background. Per favore non iniziare nessun'altra azione sul tuo server per i prossimi ~10 minuti (dipende dalla velocità hardware). Dopo questo, dovrai ri-loggarti nel webadmin. Il registro di aggiornamento sarà disponibile in Strumenti → Log/Registri (nel webadmin) o dalla linea di comando eseguendo 'yunohost log list'.", + "tools_upgrade_special_packages": "Adesso aggiorno i pacchetti 'speciali' (correlati a yunohost)…", + "tools_upgrade_regular_packages_failed": "Impossibile aggiornare i pacchetti: {packages_list}", + "tools_upgrade_regular_packages": "Adesso aggiorno i pacchetti 'normali' (non correlati a yunohost)…", + "tools_upgrade_cant_unhold_critical_packages": "Impossibile annullare il blocco dei pacchetti critici/importanti…", + "tools_upgrade_cant_hold_critical_packages": "Impossibile bloccare i pacchetti critici/importanti…", + "tools_upgrade_cant_both": "Impossibile aggiornare sia il sistema e le app nello stesso momento", + "tools_upgrade_at_least_one": "Specifica '--apps', o '--system'", + "show_tile_cant_be_enabled_for_regex": "Non puoi abilitare 'show_tile' in questo momento, perché l'URL del permesso '{permission}' è una regex", + "show_tile_cant_be_enabled_for_url_not_defined": "Non puoi abilitare 'show_tile' in questo momento, devi prima definire un URL per il permesso '{permission}'", + "service_reloaded_or_restarted": "Il servizio '{service:s}' è stato ricaricato o riavviato", + "service_reload_or_restart_failed": "Impossibile ricaricare o riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}", + "service_restarted": "Servizio '{service:s}' riavviato", + "service_restart_failed": "Impossibile riavviare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}", + "service_reloaded": "Servizio '{service:s}' ricaricato", + "service_reload_failed": "Impossibile ricaricare il servizio '{service:s}'\n\nUltimi registri del servizio: {logs:s}", + "service_regen_conf_is_deprecated": "'yunohost service regen-conf' è obsoleto! Per favore usa 'yunohost tools regen-conf' al suo posto.", + "service_description_yunohost-firewall": "Gestisce l'apertura e la chiusura delle porte ai servizi", + "service_description_yunohost-api": "Gestisce l'interazione tra l'interfaccia web YunoHost ed il sistema", + "service_description_ssh": "Ti consente di accedere da remoto al tuo server attraverso il terminale (protocollo SSH)", + "service_description_slapd": "Memorizza utenti, domini e info correlate", + "service_description_rspamd": "Filtra SPAM, e altre funzionalità legate alle mail", + "service_description_redis-server": "Un database specializzato usato per un veloce accesso ai dati, task queue, e comunicazioni tra programmi", + "service_description_postfix": "Usato per inviare e ricevere email", + "service_description_php7.3-fpm": "Esegue app scritte in PHP con NGINX", + "service_description_nginx": "Serve o permette l'accesso a tutti i siti pubblicati sul tuo server", + "service_description_mysql": "Memorizza i dati delle app (database SQL)", + "service_description_metronome": "Gestisce gli account di messaggistica instantanea XMPP", + "service_description_fail2ban": "Ti protegge dal brute-force e altri tipi di attacchi da Internet", + "service_description_dovecot": "Consente ai client mail di accedere/recuperare le email (via IMAP e POP3)", + "service_description_dnsmasq": "Gestisce la risoluzione dei domini (DNS)", + "service_description_avahi-daemon": "Consente di raggiungere il tuo server eseguendo 'yunohost.local' sulla tua LAN", + "server_reboot_confirm": "Il server si riavvierà immediatamente, sei sicuro? [{answers:s}]", + "server_reboot": "Il server si riavvierà", + "server_shutdown_confirm": "Il server si spegnerà immediatamente, sei sicuro? [{answers:s}]", + "server_shutdown": "Il server si spegnerà", + "root_password_replaced_by_admin_password": "La tua password di root è stata sostituita dalla tua password d'amministratore.", + "root_password_desynchronized": "La password d'amministratore è stata cambiata, ma YunoHost non ha potuto propagarla alla password di root!", + "restore_system_part_failed": "Impossibile ripristinare la sezione di sistema '{part:s}'", + "restore_removing_tmp_dir_failed": "Impossibile rimuovere una vecchia directory temporanea", + "restore_not_enough_disk_space": "Spazio libero insufficiente (spazio: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)", + "restore_may_be_not_enough_disk_space": "Il tuo sistema non sembra avere abbastanza spazio (libero: {free_space:d}B, necessario: {needed_space:d}B, margine di sicurezza: {margin:d}B)", + "restore_extracting": "Sto estraendo i file necessari dall'archivio…", + "restore_already_installed_apps": "Le seguenti app non possono essere ripristinate perché sono già installate: {apps}", + "regex_with_only_domain": "Non puoi usare una regex per il dominio, solo per i percorsi", + "regex_incompatible_with_tile": "/!\\ Packagers! Il permesso '{permission}' ha show_tile impostato su 'true' e perciò non è possibile definire un URL regex per l'URL principale", + "regenconf_need_to_explicitly_specify_ssh": "La configurazione ssh è stata modificata manualmente, ma devi specificare la categoria 'ssh' con --force per applicare le modifiche.", + "regenconf_pending_applying": "Applico le configurazioni in attesa per la categoria '{category}'...", + "regenconf_failed": "Impossibile rigenerare la configurazione per le categorie: {categories}", + "regenconf_dry_pending_applying": "Controllo configurazioni in attesa che potrebbero essere applicate alla categoria '{category}'…", + "regenconf_would_be_updated": "La configurazione sarebbe stata aggiornata per la categoria '{category}'", + "regenconf_updated": "Configurazione aggiornata per '{category}'", + "regenconf_up_to_date": "Il file di configurazione è già aggiornato per la categoria '{category}'", + "regenconf_now_managed_by_yunohost": "Il file di configurazione '{conf}' da adesso è gestito da YunoHost (categoria {category}).", + "regenconf_file_updated": "File di configurazione '{conf}' aggiornato", + "regenconf_file_removed": "File di configurazione '{conf}' rimosso", + "regenconf_file_remove_failed": "Impossibile rimuovere il file di configurazione '{conf}'", + "regenconf_file_manually_removed": "Il file di configurazione '{conf}' è stato rimosso manualmente, e non sarà generato", + "regenconf_file_manually_modified": "Il file di configurazione '{conf}' è stato modificato manualmente e non sarà aggiornato", + "regenconf_file_kept_back": "Il file di configurazione '{conf}' dovrebbe esser stato cancellato da regen-conf (categoria {category}), ma non è così.", + "regenconf_file_copy_failed": "Impossibile copiare il nuovo file di configurazione da '{new}' a '{conf}'", + "regenconf_file_backed_up": "File di configurazione '{conf}' salvato in '{backup}'", + "permission_require_account": "Il permesso {permission} ha senso solo per gli utenti con un account, quindi non può essere attivato per i visitatori.", + "permission_protected": "Il permesso {permission} è protetto. Non puoi aggiungere o rimuovere il gruppo visitatori dal permesso.", + "permission_updated": "Permesso '{permission:s}' aggiornato", + "permission_update_failed": "Impossibile aggiornare il permesso '{permission}': {error}", + "permission_not_found": "Permesso '{permission:s}' non trovato", + "permission_deletion_failed": "Impossibile cancellare il permesso '{permission}': {error}", + "permission_deleted": "Permesso '{permission:s}' cancellato", + "permission_currently_allowed_for_all_users": "Il permesso è attualmente garantito a tutti gli utenti oltre gli altri gruppi. Probabilmente vuoi o rimuovere il permesso 'all_user' o rimuovere gli altri gruppi per cui è garantito attualmente.", + "permission_creation_failed": "Impossibile creare i permesso '{permission}': {error}", + "permission_created": "Permesso '{permission:s}' creato", + "permission_cannot_remove_main": "Non è possibile rimuovere un permesso principale", + "permission_already_up_to_date": "Il permesso non è stato aggiornato perché la richiesta di aggiunta/rimozione è già coerente con lo stato attuale.", + "permission_already_exist": "Permesso '{permission}' esiste già", + "permission_already_disallowed": "Il gruppo '{group}' ha già il permesso '{permission}' disabilitato", + "permission_already_allowed": "Il gruppo '{group}' ha già il permesso '{permission}' abilitato", + "pattern_password_app": "Mi spiace, le password non possono contenere i seguenti caratteri: {forbidden_chars}", + "pattern_email_forward": "Dev'essere un indirizzo mail valido, simbolo '+' accettato (es: tizio+tag@example.com)", + "operation_interrupted": "L'operazione è stata interrotta manualmente?", + "invalid_number": "Dev'essere un numero", + "migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations migrate`.", + "migrations_success_forward": "Migrazione {id} completata", + "migrations_skip_migration": "Salto migrazione {id}...", + "migrations_running_forward": "Eseguo migrazione {id}...", + "migrations_pending_cant_rerun": "Queste migrazioni sono ancora in attesa, quindi non possono essere eseguite nuovamente: {ids}", + "migrations_not_pending_cant_skip": "Queste migrazioni non sono in attesa, quindi non possono essere saltate: {ids}", + "migrations_no_such_migration": "Non esiste una migrazione chiamata '{id}'", + "migrations_no_migrations_to_run": "Nessuna migrazione da eseguire", + "migrations_need_to_accept_disclaimer": "Per eseguire la migrazione {id}, devi accettare il disclaimer seguente:\n---\n{disclaimer}\n---\nSe accetti di eseguire la migrazione, per favore reinserisci il comando con l'opzione '--accept-disclaimer'.", + "migrations_must_provide_explicit_targets": "Devi specificare i target quando utilizzi '--skip' o '--force-rerun'", + "migrations_migration_has_failed": "Migrazione {id} non completata, annullamento. Errore: {exception}", + "migrations_loading_migration": "Caricamento migrazione {id}...", + "migrations_list_conflict_pending_done": "Non puoi usare sia '--previous' e '--done' allo stesso tempo.", + "migrations_exclusive_options": "'--auto', '--skip', e '--force-rerun' sono opzioni che si escludono a vicenda.", + "migrations_failed_to_load_migration": "Impossibile caricare la migrazione {id}: {error}", + "migrations_dependencies_not_satisfied": "Esegui queste migrazioni: '{dependencies_id}', prima di {id}.", + "migrations_cant_reach_migration_file": "Impossibile accedere ai file di migrazione nel path '%s'", + "migrations_already_ran": "Migrazioni già effettuate: {ids}", + "migration_0019_slapd_config_will_be_overwritten": "Sembra che tu abbia modificato manualmente la configurazione slapd. Per questa importante migrazione, YunoHost deve forzare l'aggiornamento della configurazione slapd. I file originali verranno back-uppati in {conf_backup_folder}.", + "migration_0019_rollback_success": "Sistema ripristinato.", + "migration_0019_migration_failed_trying_to_rollback": "Impossibile migrare... sto cercando di ripristinare il sistema.", + "migration_0019_can_not_backup_before_migration": "Il backup del sistema non è stato completato prima della migrazione. Errore: {error:s}", + "migration_0019_backup_before_migration": "Creando un backup del database LDAP e delle impostazioni delle app prima dell'effettiva migrazione.", + "migration_0019_add_new_attributes_in_ldap": "Aggiungi nuovi attributi ai permessi nel database LDAP", + "migration_0018_failed_to_reset_legacy_rules": "Impossibile resettare le regole iptables legacy: {error}", + "migration_0018_failed_to_migrate_iptables_rules": "Migrazione fallita delle iptables legacy a nftables: {error}", + "migration_0017_not_enough_space": "Libera abbastanza spazio in {path} per eseguire la migrazione.", + "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 è installato, ma non PostgreSQL 11 ?! Qualcosa di strano potrebbe esser successo al tuo sistema :'( ...", + "migration_0017_postgresql_96_not_installed": "PostgreSQL non è stato installato sul tuo sistema. Nulla da fare.", + "migration_0015_weak_certs": "I seguenti certificati utilizzano ancora un algoritmo di firma debole e dovrebbero essere aggiornati per essere compatibili con la prossima versione di nginx: {certs}", + "migration_0015_cleaning_up": "Sto pulendo la cache e i pacchetti non più utili...", + "migration_0015_specific_upgrade": "Inizio l'aggiornamento dei pacchetti di sistema che necessitano di essere aggiornati da soli...", + "migration_0015_modified_files": "Attenzioni, i seguenti file sembrano esser stati modificati manualmente, e potrebbero essere sovrascritti dopo l'aggiornamento: {manually_modified_files}", + "migration_0015_problematic_apps_warning": "Alcune applicazioni potenzialmente problematiche sono state rilevate nel sistema. Sembra che non siano state installate attraverso il catalogo app YunoHost, o non erano flaggate come 'working'/'funzionanti'. Di conseguenza, non è possibile garantire che funzioneranno ancora dopo l'aggiornamento: {problematic_apps}", + "migration_0015_general_warning": "Attenzione, sappi che questa migrazione è un'operazione delicata. Il team YunoHost ha fatto del suo meglio nel controllarla e testarla, ma le probabilità che il sistema e/o qualche app si danneggi non sono nulle.\n\nPerciò, ti raccomandiamo di:\n\t- Effettuare un backup di tutti i dati e app importanti. Maggiori informazioni su https://yunohost.org/backup;\n\t- Sii paziente dopo aver lanciato l'operazione: in base alla tua connessione internet e al tuo hardware, potrebbero volerci alcune ore per aggiornare tutto.", + "migration_0015_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Esegui un aggiornamento classico prima di lanciare la migrazione a Buster.", + "migration_0015_not_enough_free_space": "Poco spazio libero disponibile in /var/! Dovresti avere almeno 1GB libero per effettuare questa migrazione.", + "migration_0015_not_stretch": "La distribuzione Debian corrente non è Stretch!", + "migration_0015_yunohost_upgrade": "Inizio l'aggiornamento del core di YunoHost...", + "migration_0015_still_on_stretch_after_main_upgrade": "Qualcosa è andato storto durante l'aggiornamento principale, il sistema sembra essere ancora su Debian Stretch", + "migration_0015_main_upgrade": "Inizio l'aggiornamento principale...", + "migration_0015_patching_sources_list": "Applico le patch a sources.lists...", + "migration_0015_start": "Inizio migrazione a Buster", + "migration_0011_failed_to_remove_stale_object": "Impossibile rimuovere l'oggetto {dn}: {error}", + "migration_0011_update_LDAP_schema": "Aggiornado lo schema LDAP...", + "migration_0011_update_LDAP_database": "Aggiornando il database LDAP...", + "migration_0011_migrate_permission": "Migrando permessi dalle impostazioni delle app a LDAP...", + "migration_0011_LDAP_update_failed": "Impossibile aggiornare LDAP. Errore: {error:s}", + "migration_0011_create_group": "Sto creando un gruppo per ogni utente...", + "migration_description_0019_extend_permissions_features": "Estendi il sistema di gestione dei permessi app", + "migration_description_0018_xtable_to_nftable": "Migra le vecchie regole di traffico network sul nuovo sistema nftable", + "migration_description_0017_postgresql_9p6_to_11": "Migra i database da PostgreSQL 9.6 a 11", + "migration_description_0016_php70_to_php73_pools": "MIgra i file di configurazione 'pool' di php7.0-fpm su php7.3", + "migration_description_0015_migrate_to_buster": "Aggiorna il sistema a Debian Buster e YunoHost 4.X", + "migrating_legacy_permission_settings": "Impostando le impostazioni legacy dei permessi..", + "mailbox_disabled": "E-mail disabilitate per l'utente {user:s}", + "log_user_permission_reset": "Resetta il permesso '{}'", + "log_user_permission_update": "Aggiorna gli accessi del permesso '{}'", + "log_user_group_update": "Aggiorna il gruppo '{}'", + "log_user_group_delete": "Cancella il gruppo '{}'", + "log_user_group_create": "Crea il gruppo '[}'", + "log_permission_url": "Aggiorna l'URL collegato al permesso '{}'", + "log_permission_delete": "Cancella permesso '{}'", + "log_permission_create": "Crea permesso '{}'", + "log_app_config_apply": "Applica la configurazione all'app '{}'", + "log_app_config_show_panel": "Mostra il pannello di configurazione dell'app '{}'", + "log_app_action_run": "Esegui l'azione dell'app '{}'", + "log_operation_unit_unclosed_properly": "Operazion unit non è stata chiusa correttamente", + "invalid_regex": "Regex invalida:'{regex:s}'", + "hook_list_by_invalid": "Questa proprietà non può essere usata per listare gli hooks", + "hook_json_return_error": "Impossibile leggere la risposta del hook {path:s}. Errore: {msg:s}. Contenuto raw: {raw_content}", + "group_user_not_in_group": "L'utente {user} non è nel gruppo {group}", + "group_user_already_in_group": "L'utente {user} è già nel gruppo {group}", + "group_update_failed": "Impossibile aggiornare il gruppo '{group}': {error}", + "group_updated": "Gruppo '{group}' aggiornato", + "group_unknown": "Gruppo '{group:s}' sconosciuto", + "group_deletion_failed": "Impossibile cancellare il gruppo '{group}': {error}", + "group_deleted": "Gruppo '{group}' cancellato", + "group_cannot_be_deleted": "Il gruppo {group} non può essere eliminato manualmente.", + "group_cannot_edit_primary_group": "Il gruppo '{group}' non può essere modificato manualmente. È il gruppo principale con lo scopo di contenere solamente uno specifico utente.", + "group_cannot_edit_visitors": "Il gruppo 'visitatori' non può essere modificato manualmente. È un gruppo speciale che rappresenta i visitatori anonimi", + "group_cannot_edit_all_users": "Il gruppo 'all_users' non può essere modificato manualmente. È un gruppo speciale che contiene tutti gli utenti registrati in YunoHost", + "group_creation_failed": "Impossibile creare il gruppo '{group}': {error}", + "group_created": "Gruppo '{group}' creato", + "group_already_exist_on_system_but_removing_it": "Il gruppo {group} esiste già tra i gruppi di sistema, ma YunoHost lo cancellerà...", + "group_already_exist_on_system": "Il gruppo {group} esiste già tra i gruppi di sistema", + "group_already_exist": "Il gruppo {group} esiste già", + "global_settings_setting_backup_compress_tar_archives": "Quando creo nuovi backup, usa un archivio (.tar.gz) al posto di un archivio non compresso (.tar). NB: abilitare quest'opzione significa create backup più leggeri, ma la procedura durerà di più e il carico CPU sarà maggiore.", + "global_settings_setting_smtp_relay_password": "Password del relay SMTP", + "global_settings_setting_smtp_relay_user": "User account del relay SMTP", + "global_settings_setting_smtp_relay_port": "Porta del relay SMTP", + "global_settings_setting_smtp_relay_host": "Utilizza SMTP relay per inviare mail al posto di questa instanza yunohost. Utile se sei in una di queste situazioni: la tua porta 25 è bloccata dal tuo provider ISP o VPS; hai un IP residenziale listato su DUHL; non sei puoi configurare il DNS inverso; oppure questo server non è direttamente esposto a Internet e vuoi usarne un'altro per spedire email.", + "global_settings_setting_smtp_allow_ipv6": "Permetti l'utilizzo di IPv6 per ricevere e inviare mail", + "global_settings_setting_pop3_enabled": "Abilita il protocollo POP3 per il server mail", + "dyndns_provider_unreachable": "Incapace di raggiungere il provider DynDNS {provider}: o il tuo YunoHost non è connesso ad internet o il server dynette è down.", + "dpkg_lock_not_available": "Impossibile eseguire il comando in questo momento perché un altro programma sta bloccando dpkg (il package manager di sistema)", + "domain_name_unknown": "Dominio '{domain}' sconosciuto", + "domain_cannot_remove_main_add_new_one": "Non puoi rimuovere '{domain:s}' visto che è il dominio principale nonché il tuo unico dominio, devi prima aggiungere un altro dominio eseguendo 'yunohost domain add ', impostarlo come dominio principale con 'yunohost domain main-domain n ', e solo allora potrai rimuovere il dominio '{domain:s}' eseguendo 'yunohost domain remove {domain:s}'.'", + "domain_cannot_add_xmpp_upload": "Non puoi aggiungere domini che iniziano per 'xmpp-upload.'. Questo tipo di nome è riservato per la funzionalità di upload XMPP integrata in YunoHost.", + "diagnosis_processes_killed_by_oom_reaper": "Alcuni processi sono stati terminati dal sistema che era a corto di memoria. Questo è un sintomo di insufficienza di memoria nel sistema o di un processo che richiede troppa memoria. Lista dei processi terminati:\n{kills_summary}", + "diagnosis_never_ran_yet": "Sembra che questo server sia stato impostato recentemente e non è presente nessun report di diagnostica. Dovresti partire eseguendo una diagnostica completa, da webadmin o da terminale con il comando 'yunohost diagnosis run'.", + "diagnosis_unknown_categories": "Le seguenti categorie sono sconosciute: {categories}", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Per sistemare, ispeziona le differenze nel terminale eseguendo yunohost tools regen-conf nginx --dry-run --with-diff e se ti va bene, applica le modifiche con yunohost tools regen-conf ngix --force.", + "diagnosis_http_nginx_conf_not_up_to_date": "La configurazione nginx di questo dominio sembra esser stato modificato manualmente, e impedisce a YunoHost di controlalre se è raggiungibile su HTTP.", + "diagnosis_http_partially_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN su IPv{failed}, anche se funziona su IPv{passed}.", + "diagnosis_http_unreachable": "Il dominio {domain} sembra irraggiungibile attraverso HTTP dall'esterno della tua LAN.", + "diagnosis_http_bad_status_code": "Sembra che un altro dispositivo (forse il tuo router internet) abbia risposto al posto del tuo server
1. La causa più comune è la porta 80 (e 443) non correttamente inoltrata al tuo server.
2. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_http_connection_error": "Errore connessione: impossibile connettersi al dominio richiesto, probabilmente è irraggiungibile.", + "diagnosis_http_timeout": "Andato in time-out cercando di contattare il server dall'esterno. Sembra essere irraggiungibile.
1. La causa più comune è la porta 80 (e 443) non correttamente inoltrata al tuo server.
2. Dovresti accertarti che il servizio nginx sia attivo.
3. Su setup più complessi: assicurati che nessun firewall o reverse-proxy stia interferendo.", + "diagnosis_http_ok": "Il dominio {domain} è raggiungibile attraverso HTTP al di fuori della tua LAN.", + "diagnosis_http_could_not_diagnose_details": "Errore: {error}", + "diagnosis_http_could_not_diagnose": "Non posso controllare se i domini sono raggiungibili dall'esterno su IPv{ipversion}.", + "diagnosis_http_hairpinning_issue_details": "Questo probabilmente è causato dal tuo ISP router. Come conseguenza, persone al di fuori della tua LAN saranno in grado di accedere al tuo server come atteso, ma non le persone all'interno della LAN (tipo te, immagino) utilizzando il dominio internet o l'IP globale. Dovresti essere in grado di migliorare la situazione visitando https://yunohost.org/dns_local_network", + "diagnosis_http_hairpinning_issue": "La tua rete locale sembra non avere \"hairpinning\" abilitato.", + "diagnosis_ports_forwarding_tip": "Per sistemare questo problema, probabilmente dovresti configurare l'inoltro della porta sul tuo router internet come descritto qui https://yunohost.org/isp_box_config", + "diagnosis_ports_needed_by": "Esporre questa porta è necessario per le feature di {category} (servizio {service})", + "diagnosis_ports_ok": "La porta {port} è raggiungibile dall'esterno.", + "diagnosis_ports_partially_unreachable": "La porta {port} non è raggiungibile dall'esterno su IPv{failed}.", + "diagnosis_ports_unreachable": "La porta {port} non è raggiungibile dall'esterno.", + "diagnosis_ports_could_not_diagnose_details": "Errore: {error}", + "diagnosis_ports_could_not_diagnose": "Impossibile diagnosticare se le porte sono raggiungibili dall'esterno su IPv{ipversion}.", + "diagnosis_description_regenconf": "Configurazioni sistema", + "diagnosis_description_mail": "Email", + "diagnosis_description_web": "Web", + "diagnosis_description_ports": "Esposizione porte", + "diagnosis_description_systemresources": "Risorse di sistema", + "diagnosis_description_services": "Check stato servizi", + "diagnosis_description_dnsrecords": "Record DNS", + "diagnosis_description_ip": "Connettività internet", + "diagnosis_description_basesystem": "Sistema base", + "diagnosis_security_vulnerable_to_meltdown_details": "Per sistemare, dovresti aggiornare il tuo sistema e fare il reboot per caricare il nuovo kernel linux (o contatta il tuo server provider se non funziona). Visita https://meltdownattack.com/ per maggiori info.", + "diagnosis_security_vulnerable_to_meltdown": "Sembra che tu sia vulnerabile alla vulnerabilità di sicurezza critica \"Meltdown\"", + "diagnosis_regenconf_manually_modified_details": "Questo è probabilmente OK se sai cosa stai facendo! YunoHost smetterà di aggiornare automaticamente questo file... Ma sappi che gli aggiornamenti di YunoHost potrebbero contenere importanti cambiamenti. Se vuoi, puoi controllare le differente con yunohost tools regen-conf {category} --dry-run --with-diff e forzare il reset della configurazione raccomandata con yunohost tools regen-conf {category} --force", + "diagnosis_regenconf_manually_modified": "Il file di configurazione {file} sembra esser stato modificato manualmente.", + "diagnosis_regenconf_allgood": "Tutti i file di configurazione sono allineati con le configurazioni raccomandate!", + "diagnosis_mail_queue_too_big": "Troppe email in attesa nella coda ({nb_pending} emails)", + "diagnosis_mail_queue_unavailable_details": "Errore: {error}", + "diagnosis_mail_queue_unavailable": "Impossibile consultare il numero di email in attesa", + "diagnosis_mail_queue_ok": "{nb_pending} emails in attesa nelle code", + "diagnosis_mail_blacklist_website": "Dopo aver identificato il motivo e averlo risolto, sentiti libero di chiedere di rimuovere il tuo IP o dominio da {blacklist_website}", + "diagnosis_mail_blacklist_reason": "Il motivo della blacklist è: {reason}", + "diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio {item} è nella blacklist {blacklist_name}" } From 94c1fc8d417d11fc0e7ca872e7843a257cefd6ff Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 19 Dec 2020 01:31:31 +0100 Subject: [PATCH 051/363] Fix duplicated stuff in fr string --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index ea7228e41..90190c223 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -404,7 +404,7 @@ "dpkg_lock_not_available": "Cette commande ne peut pas être exécutée pour le moment car un autre programme semble utiliser le verrou de dpkg (le gestionnaire de package système)", "tools_upgrade_cant_unhold_critical_packages": "Impossible d'enlever le drapeau 'hold' pour les paquets critiques…", "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à l'administrateur Web. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant la « liste des journaux yunohost » (à partir de la ligne de commande).", - "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}\n{sourceslist}", + "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "backup_permission": "Permission de sauvegarde pour {app:s}", "group_created": "Le groupe '{group}' a été créé", From 6c75aa00742bd459870f5e5dacf5f9046afbd2fe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 19 Dec 2020 01:47:52 +0100 Subject: [PATCH 052/363] Update changelog for 4.1.1 --- debian/changelog | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/debian/changelog b/debian/changelog index 1fa414d9e..0996629f9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,29 @@ +yunohost (4.1.1) testing; urgency=low + + - [fix] Backup/restore DKIM keys ([#1098](https://github.com/yunohost/yunohost/pull/1098), [#1100](https://github.com/yunohost/yunohost/pull/1100)) + - [fix] Backup/restore Dyndns keys ([#1101](https://github.com/yunohost/yunohost/pull/1101)) + - [fix] mail: Add a max limit to number of recipients ([#1094](https://github.com/yunohost/yunohost/pull/1094)) + - [fix] mail: Do not enforce encryption for relays .. some don't support it ... (11fe9d7e) + - [i18n] Translations updated for French, German, Italian, Occitan + + Misc small fixes: + + - [fix] misc: Prevent running `yunohost domain dns-conf` on arbirary domains ([#1099](https://github.com/yunohost/yunohost/pull/1099)) + - [enh] misc: We don't care that 'apt-key output should not be parsed' (5422a49d) + - [fix] dnsmasq: Avoid to define wildcard records locally ([#1102](https://github.com/yunohost/yunohost/pull/1102)) + - [fix] ssowat: Fix indent ([#1103](https://github.com/yunohost/yunohost/pull/1103)) + - [fix] nginx: Force-disable gzip for acme-challenge (c5d06af2) + - [enh] app helpers: Handle change php version ([#1107](https://github.com/yunohost/yunohost/pull/1107)) + - [fix] permissions: Misc fixes ([#1104](https://github.com/yunohost/yunohost/pull/1104), [#1105](https://github.com/yunohost/yunohost/pull/1105)) + - [fix] certificates: Use organization name to check if from Lets Encrypt ([#1093](https://github.com/yunohost/yunohost/pull/1093)) + - [enh] ldap: Increase ldap search size limit? ([#1074](https://github.com/yunohost/yunohost/pull/1074)) + - [fix] app helpers: Avoid unecessarily reloading php7.3 too fast ([#1108](https://github.com/yunohost/yunohost/pull/1108)) + - [fix] log: Fix a small issue where metadata could be None (because of empty yaml maybe?) (f9143d53) + + Thanks to all contributors <3 ! (Christian Wehrli, Eric COURTEAU, Flavio Cristoforetti, Kay0u, Kayou, ljf, ljf (zamentur), Quentí) + + -- Alexandre Aubin Sat, 19 Dec 2020 01:33:36 +0100 + yunohost (4.1.0) testing; urgency=low - [enh] Extends permissions features, improve legacy settings handling (YunoHost#861) From 42f3ff6b009ec0bb98074381c484b49681ec1ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Le=20Bouter?= Date: Mon, 21 Dec 2020 00:21:50 +0100 Subject: [PATCH 053/363] firewall: force source port for UPnP. miniupnpc uses a random source port by default, the issue is that the firewall rule to allow destination port 1900 incoming is unused because the UPnP server will use the random source port as destination port in the reply which iptables will block. Forcing the source port to be 1900 will ensure the UPnP server also uses that as destination port in the reply and pass the firewall. python-miniupnpc 2.0 or later is required for this change to have any effect, it is otherwise silently ignored. A debian package upgrade is in the works for official Yunohost repos. --- src/yunohost/firewall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index c17e958e7..dbf87a7b5 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -336,7 +336,7 @@ def firewall_upnp(action='status', no_refresh=False): # Refresh port mapping using UPnP if not no_refresh: - upnpc = miniupnpc.UPnP() + upnpc = miniupnpc.UPnP(localport=1) upnpc.discoverdelay = 3000 # Discover UPnP device(s) From 6419c2ac64ee1c2854001d3c9f1309a6552e9435 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 24 Dec 2020 15:38:39 +0100 Subject: [PATCH 054/363] Don't mess with Sury's pinning --- data/helpers.d/apt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 59f233c60..7c6de912d 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -460,7 +460,8 @@ ynh_remove_extra_repo () { name="${name:-$app}" ynh_secure_remove "/etc/apt/sources.list.d/$name.list" - ynh_secure_remove "/etc/apt/preferences.d/$name" + # Sury pinning is managed by the regenconf in the core... + [[ "$name" == "extra_php_version" ]] || ynh_secure_remove "/etc/apt/preferences.d/$name" ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.asc" > /dev/null @@ -548,6 +549,9 @@ ynh_pin_repo () { append="tee" fi + # Sury pinning is managed by the regenconf in the core... + [[ "$name" != "extra_php_version" ]] || return + mkdir --parents "/etc/apt/preferences.d" echo "Package: $package Pin: $pin From 0c977d8c70c454d196cb44576b62df2d57d1f3a0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Dec 2020 01:45:00 +0100 Subject: [PATCH 055/363] [fix] double return prevent new code from working --- src/yunohost/domain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index b771d60ab..5d47aefe0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -75,8 +75,6 @@ def domain_list(exclude_subdomains=False): result_list = sorted(result_list, cmp_domain) - return {'domains': result_list} - return { 'domains': result_list, 'main': _get_maindomain() From bdff5937f09a8bdee95bcd45cc2762c7ac35ecdf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Dec 2020 16:58:50 +0100 Subject: [PATCH 056/363] Handle permission regexes that may start with ^ or \ --- src/yunohost/permission.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 8a5ef7260..547510323 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -668,6 +668,9 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): For example: re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ + + We can also have less-trivial regexes like: + re:^\/api\/.*|\/scripts\/api.js$ """ from yunohost.domain import domain_list @@ -692,9 +695,9 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): if url.startswith('re:'): # regex without domain - - if url.startswith('re:/'): - validate_regex(url[4:]) + # we check for the first char after 're:' + if url[3] in ['/', '^', '\\']: + validate_regex(url[3:]) return url # regex with domain From d77d5afb2cb6bb8f70a09845e1930177a340cf99 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Dec 2020 20:05:51 +0100 Subject: [PATCH 057/363] When encountering unknown setting, also save the regular setting so we don't re-encounter the unknown settings everytime --- src/yunohost/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index db5f56b45..3c79d7945 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -263,6 +263,7 @@ def _get_settings(): if unknown_settings: try: _save_settings(unknown_settings, location=unknown_settings_path) + _save_settings(settings) except Exception as e: logger.warning("Failed to save unknown settings (because %s), aborting." % e) From b685a274812099523df4f78515cd5a62924e16bf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 28 Dec 2020 22:57:52 +0100 Subject: [PATCH 058/363] [yolo] Detect moar hardware name --- data/hooks/diagnosis/00-basesystem.py | 10 ++++++++-- locales/en.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/data/hooks/diagnosis/00-basesystem.py b/data/hooks/diagnosis/00-basesystem.py index d56faec98..edfb68beb 100644 --- a/data/hooks/diagnosis/00-basesystem.py +++ b/data/hooks/diagnosis/00-basesystem.py @@ -32,11 +32,17 @@ class BaseSystemDiagnoser(Diagnoser): data={"virt": virt, "arch": arch}, summary="diagnosis_basesystem_hardware") - # Also possibly the board name + # Also possibly the board / hardware name if os.path.exists("/proc/device-tree/model"): model = read_file('/proc/device-tree/model').strip().replace('\x00', '') hardware["data"]["model"] = model - hardware["details"] = ["diagnosis_basesystem_hardware_board"] + hardware["details"] = ["diagnosis_basesystem_hardware_model"] + elif os.path.exists("/sys/devices/virtual/dmi/id/sys_vendor"): + model = read_file("/sys/devices/virtual/dmi/id/sys_vendor").strip() + if os.path.exists("/sys/devices/virtual/dmi/id/product_name"): + model = "%s %s" % (model, read_file("/sys/devices/virtual/dmi/id/product_name").strip()) + hardware["data"]["model"] = model + hardware["details"] = ["diagnosis_basesystem_hardware_model"] yield hardware diff --git a/locales/en.json b/locales/en.json index 8510cdf58..c24fc831c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -141,7 +141,7 @@ "confirm_app_install_thirdparty": "DANGER! This app is not part of Yunohost's app catalog. Installing third-party apps may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. NO SUPPORT will be provided if this app doesn't work or breaks your system… If you are willing to take that risk anyway, type '{answers:s}'", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "diagnosis_basesystem_hardware": "Server hardware architecture is {virt} {arch}", - "diagnosis_basesystem_hardware_board": "Server board model is {model}", + "diagnosis_basesystem_hardware_model": "Server model is {model}", "diagnosis_basesystem_host": "Server is running Debian {debian_version}", "diagnosis_basesystem_kernel": "Server is running Linux kernel {kernel_version}", "diagnosis_basesystem_ynh_single_version": "{package} version: {version} ({repo})", From d6d75c528eaaa85c895fcaf7a3d3d025eaea77ab Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 29 Dec 2020 22:37:59 +0100 Subject: [PATCH 059/363] fix legacy permission migration --- src/yunohost/utils/legacy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 1cc0246f3..4aaf62179 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -104,7 +104,7 @@ class SetupGroupPermissions(): allowed = [user for user in permission.split(',') if user in known_users] else: allowed = ["all_users"] - permission_create(app + ".main", url=url, allowed=allowed, protected=False, sync_perm=False) + permission_create(app + ".main", url=url, allowed=allowed, show_tile=True, protected=False, sync_perm=False) app_setting(app, 'allowed_users', delete=True) @@ -185,12 +185,12 @@ def migrate_legacy_permission_settings(app=None): if unprotected_urls != []: permission_create(app + ".legacy_unprotected_uris", additional_urls=unprotected_urls, auth_header=True, label=legacy_permission_label(app, "unprotected"), - show_tile=False, allowed='visitors', protected=True, sync_perm=False) + show_tile=True, allowed='visitors', protected=False, sync_perm=False) if protected_urls != []: permission_create(app + ".legacy_protected_uris", additional_urls=protected_urls, auth_header=True, label=legacy_permission_label(app, "protected"), - show_tile=False, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], - protected=True, sync_perm=False) + show_tile=True, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], + protected=False, sync_perm=False) legacy_permission_settings = [ "skipped_uris", From e70f27b7ff2edcd39bdea40a9b620e6978a0b2e7 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 29 Dec 2020 22:48:10 +0100 Subject: [PATCH 060/363] Update legacy.py --- src/yunohost/utils/legacy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 4aaf62179..434746a28 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -185,12 +185,12 @@ def migrate_legacy_permission_settings(app=None): if unprotected_urls != []: permission_create(app + ".legacy_unprotected_uris", additional_urls=unprotected_urls, auth_header=True, label=legacy_permission_label(app, "unprotected"), - show_tile=True, allowed='visitors', protected=False, sync_perm=False) + show_tile=False, allowed='visitors', protected=True, sync_perm=False) if protected_urls != []: permission_create(app + ".legacy_protected_uris", additional_urls=protected_urls, auth_header=True, label=legacy_permission_label(app, "protected"), - show_tile=True, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], - protected=False, sync_perm=False) + show_tile=False, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], + protected=True, sync_perm=False) legacy_permission_settings = [ "skipped_uris", From 2b80bac7696eb3909640f12ca4cb03b1215a5d74 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 29 Dec 2020 23:12:14 +0100 Subject: [PATCH 061/363] Fix restore permission --- 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 c0f11eae8..7179430e0 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1343,7 +1343,7 @@ class RestoreManager(): additional_urls=permission_infos.get("additional_urls"), auth_header=permission_infos.get("auth_header"), label=permission_infos.get('label') if perm_name == "main" else permission_infos.get("sublabel"), - show_tile=permission_infos.get("show_tile", None), + show_tile=permission_infos.get("show_tile", True), protected=permission_infos.get("protected", True), sync_perm=False) From 9e2e5ce55e5a378deddead113a883083632555e6 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 29 Dec 2020 23:59:13 +0100 Subject: [PATCH 062/363] not protected by default --- 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 7179430e0..242cd0bfd 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1344,7 +1344,7 @@ class RestoreManager(): auth_header=permission_infos.get("auth_header"), label=permission_infos.get('label') if perm_name == "main" else permission_infos.get("sublabel"), show_tile=permission_infos.get("show_tile", True), - protected=permission_infos.get("protected", True), + protected=permission_infos.get("protected", False), sync_perm=False) permission_sync_to_user() From 7e096a8aebe96cafd1753a74946a800a757a2874 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 30 Dec 2020 11:25:54 +0100 Subject: [PATCH 063/363] [mod](user_create) only ask for one letter for first/last name --- 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 fb569dcd0..31b86c7ae 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -87,7 +87,7 @@ user: ask: ask_firstname required: True pattern: &pattern_firstname - - !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$ + - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_firstname" -l: full: --lastname @@ -95,7 +95,7 @@ user: ask: ask_lastname required: True pattern: &pattern_lastname - - !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$ + - !!str ^([^\W\d_]{1,30}[ ,.'-]{0,3})+$ - "pattern_lastname" -m: full: --mail From 8e30768598bf866424529ca61b1c77ca2b756d17 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Dec 2020 21:53:43 +0100 Subject: [PATCH 064/363] 2to3-2.7 ./src/yunohost/{,data_migrations/,utils/,tests/}*.py data/hooks/diagnosis/*.py -w -x dict -x print --nobackups --no-diffs --- src/yunohost/app.py | 29 +++++++++---------- src/yunohost/backup.py | 2 +- src/yunohost/certificate.py | 2 +- src/yunohost/dyndns.py | 2 +- src/yunohost/log.py | 7 ++--- src/yunohost/settings.py | 4 +-- src/yunohost/tests/test_apps.py | 2 +- .../tests/test_apps_arguments_parsing.py | 2 +- src/yunohost/tests/test_appurl.py | 2 +- src/yunohost/tests/test_backuprestore.py | 2 +- src/yunohost/tests/test_changeurl.py | 2 +- src/yunohost/tests/test_permission.py | 2 +- src/yunohost/tests/test_regenconf.py | 2 +- src/yunohost/tests/test_service.py | 2 +- src/yunohost/tests/test_user-group.py | 2 +- src/yunohost/tools.py | 6 ++-- 16 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d32fb59a2..05580fbe1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -30,10 +30,10 @@ import shutil import yaml import time import re -import urlparse +import urllib.parse import subprocess import glob -import urllib +import urllib.request, urllib.parse, urllib.error from collections import OrderedDict from moulinette import msignals, m18n, msettings @@ -736,7 +736,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Retrieve arguments list for install script args_dict = {} if not args else \ - dict(urlparse.parse_qsl(args, keep_blank_values=True)) + dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) args_odict = _parse_args_from_manifest(manifest, 'install', args=args_dict) # Validate domain / path availability for webapps @@ -759,7 +759,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Also redact the % escaped version of the password that might appear in # the 'args' section of metadata (relevant for password with non-alphanumeric char) data_to_redact = [value[0] for value in args_odict.values() if value[1] == "password"] - data_to_redact += [urllib.quote(data) for data in data_to_redact if urllib.quote(data) != data] + data_to_redact += [urllib.parse.quote(data) for data in data_to_redact if urllib.parse.quote(data) != data] operation_logger.data_to_redact.extend(data_to_redact) operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"] @@ -1406,7 +1406,7 @@ def app_ssowatconf(): write_to_json('/etc/ssowat/conf.json', conf_dict, sort_keys=True, indent=4) - from utils.legacy import translate_legacy_rules_in_ssowant_conf_json_persistent + from .utils.legacy import translate_legacy_rules_in_ssowant_conf_json_persistent translate_legacy_rules_in_ssowant_conf_json_persistent() logger.debug(m18n.n('ssowat_conf_generated')) @@ -1456,7 +1456,7 @@ def app_action_run(operation_logger, app, action, args=None): action_declaration = actions[action] # Retrieve arguments list for install script - args_dict = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} + args_dict = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} args_odict = _parse_args_for_action(actions[action], args=args_dict) args_list = [value[0] for value in args_odict.values()] @@ -1598,7 +1598,7 @@ def app_config_apply(operation_logger, app, args): "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), } - args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} + args = dict(urllib.parse.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 @@ -1817,8 +1817,7 @@ def _get_app_config_panel(app_id): "panel": [], } - panels = filter(lambda key_value: key_value[0] not in ("name", "version") and isinstance(key_value[1], OrderedDict), - toml_config_panel.items()) + panels = [key_value for key_value in toml_config_panel.items() if key_value[0] not in ("name", "version") and isinstance(key_value[1], OrderedDict)] for key, value in panels: panel = { @@ -1827,8 +1826,7 @@ def _get_app_config_panel(app_id): "sections": [], } - sections = filter(lambda k_v1: k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict), - value.items()) + sections = [k_v1 for k_v1 in value.items() if k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict)] for section_key, section_value in sections: section = { @@ -1837,8 +1835,7 @@ def _get_app_config_panel(app_id): "options": [], } - options = filter(lambda k_v: k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict), - section_value.items()) + options = [k_v for k_v in section_value.items() if k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict)] for option_key, option_value in options: option = dict(option_value) @@ -2315,7 +2312,7 @@ def _encode_string(value): """ Return the string encoded in utf-8 if needed """ - if isinstance(value, unicode): + if isinstance(value, str): return value.encode('utf8') return value @@ -2926,7 +2923,7 @@ def _load_apps_catalog(): try: apps_catalog_content = read_json(cache_file) if os.path.exists(cache_file) else None except Exception as e: - raise ("Unable to read cache for apps_catalog %s : %s" % (apps_catalog_id, str(e))) + raise "Unable to read cache for apps_catalog %s : %s" # Check that the version of the data matches version .... # ... otherwise it means we updated yunohost in the meantime @@ -2976,7 +2973,7 @@ def is_true(arg): """ if isinstance(arg, bool): return arg - elif isinstance(arg, basestring): + elif isinstance(arg, str): return arg.lower() in ['yes', 'true', 'on'] else: logger.debug('arg should be a boolean or a string, got %r', arg) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index c0f11eae8..7c75837b0 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -882,7 +882,7 @@ class RestoreManager(): End a restore operations by cleaning the working directory and regenerate ssowat conf (if some apps were restored) """ - from permission import permission_sync_to_user + from .permission import permission_sync_to_user permission_sync_to_user() diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c9451c2be..fc81ae0a6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -385,7 +385,7 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) except Exception as e: import traceback - from StringIO import StringIO + from io import StringIO stack = StringIO() traceback.print_exc(file=stack) msg = "Certificate renewing for %s failed !" % (domain) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index fe2a1bc9b..e43c180ad 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -105,7 +105,7 @@ def _dyndns_available(provider, domain): raise YunohostError('dyndns_could_not_check_available', domain=domain, provider=provider) - return r == u"Domain %s is available" % domain + return r == "Domain %s is available" % domain @is_unit_operation() diff --git a/src/yunohost/log.py b/src/yunohost/log.py index cf108b989..a21ddd606 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -65,8 +65,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False): operations = {} - logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), - os.listdir(OPERATIONS_PATH)) + logs = [x for x in os.listdir(OPERATIONS_PATH) if x.endswith(METADATA_FILE_EXT)] logs = list(reversed(sorted(logs))) if limit is not None: @@ -337,7 +336,7 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], entity_type = entity if entity in kwargs and kwargs[entity] is not None: - if isinstance(kwargs[entity], basestring): + if isinstance(kwargs[entity], str): related_to.append((entity_type, kwargs[entity])) else: for x in kwargs[entity]: @@ -596,7 +595,7 @@ class OperationLogger(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): + if error is not None and not isinstance(error, str): error = str(error) self.ended_at = datetime.utcnow() self._error = error diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 3c79d7945..060aca6e4 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -29,7 +29,7 @@ def is_boolean(value): """ if isinstance(value, bool): return True, value - elif isinstance(value, basestring): + elif isinstance(value, str): if str(value).lower() in ['true', 'on', 'yes', 'false', 'off', 'no']: return True, str(value).lower() in ['true', 'on', 'yes'] else: @@ -141,7 +141,7 @@ def settings_set(key, value): raise YunohostError('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): + if not isinstance(value, str): raise YunohostError('global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type) elif key_type == "enum": diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index 0f4c3749a..baa3d5181 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -4,7 +4,7 @@ import pytest import shutil import requests -from conftest import message, raiseYunohostError, get_test_apps_dir +from .conftest import message, raiseYunohostError, get_test_apps_dir from moulinette.utils.filesystem import mkdir diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 88c235252..dfe17d229 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -2,7 +2,7 @@ import sys import pytest from mock import patch -from StringIO import StringIO +from io import StringIO from collections import OrderedDict from moulinette import msignals diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 11ee7b4f5..3a2313b0e 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -1,7 +1,7 @@ import pytest import os -from conftest import get_test_apps_dir +from .conftest import get_test_apps_dir from yunohost.utils.error import YunohostError from yunohost.app import app_install, app_remove, _normalize_domain_path diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 9c9448879..b88fbd5b3 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -3,7 +3,7 @@ import os import shutil import subprocess -from conftest import message, raiseYunohostError, get_test_apps_dir +from .conftest import message, raiseYunohostError, get_test_apps_dir from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index ecd926a2e..ef076e67b 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -3,7 +3,7 @@ import time import requests import os -from conftest import get_test_apps_dir +from .conftest import get_test_apps_dir from yunohost.app import app_install, app_change_url, app_remove, app_map from yunohost.domain import _get_maindomain diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 1d3961585..a1f411b80 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -6,7 +6,7 @@ import os import json import shutil -from conftest import message, raiseYunohostError, get_test_apps_dir +from .conftest import message, raiseYunohostError, get_test_apps_dir from yunohost.app import app_install, app_upgrade, app_remove, app_change_url, app_map, _installed_apps, APPS_SETTING_PATH, _set_app_settings, _get_app_settings from yunohost.user import user_list, user_create, user_delete, \ diff --git a/src/yunohost/tests/test_regenconf.py b/src/yunohost/tests/test_regenconf.py index 4e1ae679b..c9a3e68f5 100644 --- a/src/yunohost/tests/test_regenconf.py +++ b/src/yunohost/tests/test_regenconf.py @@ -1,6 +1,6 @@ import os -from conftest import message +from .conftest import message from yunohost.domain import domain_add, domain_remove, domain_list from yunohost.regenconf import regen_conf, manually_modified_files, _get_conf_hashes, _force_clear_hashes diff --git a/src/yunohost/tests/test_service.py b/src/yunohost/tests/test_service.py index d0a3d4fc2..0c65a864a 100644 --- a/src/yunohost/tests/test_service.py +++ b/src/yunohost/tests/test_service.py @@ -1,6 +1,6 @@ import os -from conftest import raiseYunohostError +from .conftest import raiseYunohostError from yunohost.service import _get_services, _save_services, service_status, service_add, service_remove, service_log diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py index c0f51e35a..31131e939 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/yunohost/tests/test_user-group.py @@ -1,6 +1,6 @@ import pytest -from conftest import message, raiseYunohostError +from .conftest import message, raiseYunohostError from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ user_group_list, user_group_create, user_group_delete, user_group_update diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 96bd01ed6..f01f6adb8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -303,7 +303,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, '/home/yunohost.app' ] - for folder in filter(lambda x: not os.path.exists(x), folders_to_create): + for folder in [x for x in folders_to_create if not os.path.exists(x)]: os.makedirs(folder) # Change folders permissions @@ -953,7 +953,7 @@ def _get_migrations_list(): # (in particular, pending migrations / not already ran are not listed states = tools_migrations_state()["migrations"] - for migration_file in filter(lambda x: re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)): + for migration_file in [x for x in os.listdir(migrations_path) if re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x)]: m = _load_migration(migration_file) m.state = states.get(m.id, "pending") migrations.append(m) @@ -972,7 +972,7 @@ def _get_migration_by_name(migration_name): raise AssertionError("Unable to find migration with name %s" % migration_name) migrations_path = data_migrations.__path__[0] - migrations_found = filter(lambda x: re.match(r"^\d+_%s\.py$" % migration_name, x), os.listdir(migrations_path)) + migrations_found = [x for x in os.listdir(migrations_path) if re.match(r"^\d+_%s\.py$" % migration_name, x)] assert len(migrations_found) == 1, "Unable to find migration with name %s" % migration_name From 177957138c9f4114d118046ccf32bdcd6a84e360 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Dec 2020 21:59:30 +0100 Subject: [PATCH 065/363] Naively updating debian/control to python3 --- debian/control | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/debian/control b/debian/control index bfea80ccd..3d7dc41b9 100644 --- a/debian/control +++ b/debian/control @@ -2,19 +2,19 @@ Source: yunohost Section: utils Priority: extra Maintainer: YunoHost Contributors -Build-Depends: debhelper (>=9), dh-systemd, dh-python, python-all (>= 2.7), python-yaml, python-jinja2 +Build-Depends: debhelper (>=9), dh-systemd, dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2 Standards-Version: 3.9.6 -X-Python-Version: >= 2.7 +X-Python-Version: >= 3.7 Homepage: https://yunohost.org/ Package: yunohost Essential: yes Architecture: all -Depends: ${python:Depends}, ${misc:Depends} +Depends: ${python3:Depends}, ${misc:Depends} , moulinette (>= 4.1.0.1), ssowat (>= 4.0) - , python-psutil, python-requests, python-dnspython, python-openssl - , python-miniupnpc, python-dbus, python-jinja2 - , python-toml, python-packaging, python-publicsuffix + , python3-psutil, python3-requests, python3-dnspython, python3-openssl + , python3-miniupnpc, python3-dbus, python3-jinja2 + , python3-toml, python3-packaging, python3-publicsuffix , apt, apt-transport-https, apt-utils, dirmngr , php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl , mariadb-server, php7.3-mysql @@ -33,7 +33,7 @@ Recommends: yunohost-admin , ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog , php7.3-gd, php7.3-curl, php-gettext - , python-pip + , python3-pip , unattended-upgrades , libdbd-ldap-perl, libnet-dns-perl Suggests: htop, vim, rsync, acpi-support-base, udisks2 From 6b1fed53d6d327405aba88a8925840ea43670bbd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Dec 2020 22:00:06 +0100 Subject: [PATCH 066/363] Update tox.ini to python3 only --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 6c1139d52..36134e85a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = py{27,37}-{lint,invalidcode},py37-black +envlist = py37-{lint,invalidcode},py37-black [testenv] skip_install=True deps = - py{27,37}-{lint,invalidcode}: flake8 + py37-{lint,invalidcode}: flake8 py37-black: black commands = - py{27,37}-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor - py{27,37}-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F + py37-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor + py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F py37-black: black --check --diff src doc data tests From 299e04cfc71cb8141a4117456de4b505f8097dfb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 30 Dec 2020 22:29:16 +0100 Subject: [PATCH 067/363] Set yunohost(-api) bins to python3 --- bin/yunohost | 2 +- bin/yunohost-api | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/yunohost b/bin/yunohost index 546d2d913..0220c5f09 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! /usr/bin/python3 # -*- coding: utf-8 -*- import os diff --git a/bin/yunohost-api b/bin/yunohost-api index cc849590a..b3ed3a817 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -1,4 +1,4 @@ -#! /usr/bin/python +#! /usr/bin/python3 # -*- coding: utf-8 -*- import sys From 2e8aa6442e9cd4236859a2c2ac3278a2cad91e13 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 21 Dec 2020 15:49:52 +0000 Subject: [PATCH 068/363] Translated using Weblate (German) Currently translated at 57.6% (363 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 9c8fa33c8..05b6b2a5e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -464,12 +464,13 @@ "domain_cannot_add_xmpp_upload": "Eine hinzugefügte Domain darf nicht mit 'xmpp-upload.' beginnen. Dieser Name ist für das XMPP-Upload-Feature von YunoHost reserviert.", "group_cannot_be_deleted": "Die Gruppe {group} kann nicht manuell entfernt werden.", "group_cannot_edit_primary_group": "Die Gruppe '{group}' kann nicht manuell bearbeitet werden. Es ist die primäre Gruppe, welche dazu gedacht ist, nur einen spezifischen Benutzer zu enthalten.", - "diagnosis_processes_killed_by_oom_reaper": "Einige Prozesse wurden vom System beendet, weil nicht genügend Arbeitsspeicher vorhanden ist. Das passiert normalerweise, wenn das System nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: {kills_summary}", + "diagnosis_processes_killed_by_oom_reaper": "Das System hat einige Prozesse beendet, weil ihm der Arbeitsspeicher ausgegangen ist. Das passiert normalerweise, wenn das System ingesamt nicht genügend Arbeitsspeicher zur Verfügung hat oder wenn ein einzelner Prozess zu viel Speicher verbraucht. Zusammenfassung der beendeten Prozesse: \n{kills_summary}", "diagnosis_description_ports": "Offene Ports", "additional_urls_already_added": "Zusätzliche URL '{url:s}' bereits hinzugefügt in der zusätzlichen URL für Berechtigung '{permission:s}'", "additional_urls_already_removed": "Zusätzliche URL '{url:s}' bereits entfernt in der zusätzlichen URL für Berechtigung '{permission:s}'", "app_label_deprecated": "Dieser Befehl ist veraltet! Bitte nutzen Sie den neuen Befehl 'yunohost user permission update' um das Applabel zu verwalten.", "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen", "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.", - "diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren." + "diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren.", + "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'." } From 28b9b672b3f0009c31ca09f04f66c181a98b1b20 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 31 Dec 2020 16:32:13 +0100 Subject: [PATCH 069/363] Update changelog for 4.1.2 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index 0996629f9..9b824d9de 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (4.1.2) testing; urgency=low + + - [enh] diagnosis: Detect moar hardware name (b685a274) + - [fix] permissions: Handle regexes that may start with ^ or \ (bdff5937) + - [fix] permissions: Tile/protect status for legacy migration ([#1113](https://github.com/yunohost/yunohost/pull/1113)) + - [fix] domain: double return prevent new code from working (0c977d8c) + - [fix] settings: When encountering unknown setting, also save the regular setting so we don't re-encounter the unknown settings everytime (d77d5afb) + - [fix] users: only ask for one letter for first/last name ([#1114](https://github.com/yunohost/yunohost/pull/1114)) + - [fix] apt/sury: Tweak app helpers to not mess with Sury's pinning ([#1110](https://github.com/yunohost/yunohost/pull/1110)) + - [i18n] Translations updated for German + + Thanks to all contributors <3 ! (Bram, C. Wehrli, Kayou) + + -- Alexandre Aubin Thu, 31 Dec 2020 16:26:51 +0100 + yunohost (4.1.1) testing; urgency=low - [fix] Backup/restore DKIM keys ([#1098](https://github.com/yunohost/yunohost/pull/1098), [#1100](https://github.com/yunohost/yunohost/pull/1100)) From 78751ac655fd982fef0912f58cfde62693807d6c Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 31 Dec 2020 18:33:09 +0100 Subject: [PATCH 070/363] Move permissions helpers to a new file --- data/helpers.d/permission | 370 ++++++++++++++++++++++++++++++++++++++ data/helpers.d/setting | 369 ------------------------------------- 2 files changed, 370 insertions(+), 369 deletions(-) create mode 100644 data/helpers.d/permission diff --git a/data/helpers.d/permission b/data/helpers.d/permission new file mode 100644 index 000000000..46d703d7b --- /dev/null +++ b/data/helpers.d/permission @@ -0,0 +1,370 @@ +#!/bin/bash + +# Create a new permission for the app +# +# example 1: ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \ +# --label="My app admin" --show_tile=true +# +# This example will create a new permission permission with this following effect: +# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'. +# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin +# +# +# example 2: ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \ +# --label="MyApp API" --protected=true +# +# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission. +# In case of an API with need to be always public it avoid that the admin break anything. +# With this permission all client will be allowed to access to the url 'domain.tld/api'. +# Note that in this case no tile will be show on the SSO. +# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application. +# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case. +# So in this case it's better to disable this option for all API. +# +# +# usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false] +# [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false] +# [--protected=true|false] +# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. +# | Not that if 'show_tile' is enabled, this URL will be the URL of the tile. +# | arg: -A, additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden +# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true +# | arg: -a, allowed= - (optional) A list of group/user to allow for the permission +# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. +# | Default is "APP_LABEL (permission name)". +# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. +# | Default is false (for the permission different than 'main'). +# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator +# | won't be able to add or remove the visitors group of this permission. +# | By default it's 'false' +# +# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they +# start with '/'. For example: +# / -> domain.tld/app +# /admin -> domain.tld/app/admin +# domain.tld/app/api -> domain.tld/app/api +# +# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:". +# For example: +# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# +# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is: +# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls' +# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter) +# +# +# About the authentication header (auth_header parameter). +# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application: +# - "Auth-User": username +# - "Remote-User": username +# - "Email": user email +# +# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly. +# See https://github.com/YunoHost/issues/issues/1420 for more informations +# +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_create() { + # Declare an array to define the options of this helper. + local legacy_args=puAhaltP + local -A args_array=( [p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected= ) + local permission + local url + local additional_urls + local auth_header + local allowed + local label + local show_tile + local protected + ynh_handle_getopts_args "$@" + url=${url:-} + additional_urls=${additional_urls:-} + auth_header=${auth_header:-} + allowed=${allowed:-} + label=${label:-} + show_tile=${show_tile:-} + protected=${protected:-} + + if [[ -n $url ]] + then + url=",url='$url'" + fi + + if [[ -n $additional_urls ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # By example: + # --additional_urls /urlA /urlB + # will be: + # additional_urls=['/urlA', '/urlB'] + additional_urls=",additional_urls=['${additional_urls//;/\',\'}']" + fi + + if [[ -n $auth_header ]] + then + if [ $auth_header == "true" ] + then + auth_header=",auth_header=True" + else + auth_header=",auth_header=False" + fi + fi + + if [[ -n $allowed ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # By example: + # --allowed alice bob + # will be: + # allowed=['alice', 'bob'] + allowed=",allowed=['${allowed//;/\',\'}']" + fi + + if [[ -n ${label:-} ]]; then + label=",label='$label'" + else + label=",label='$permission'" + fi + + if [[ -n ${show_tile:-} ]] + then + if [ $show_tile == "true" ] + then + show_tile=",show_tile=True" + else + show_tile=",show_tile=False" + fi + fi + + if [[ -n ${protected:-} ]] + then + if [ $protected == "true" ] + then + protected=",protected=True" + else + protected=",protected=False" + fi + fi + + yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)" +} + +# Remove a permission for the app (note that when the app is removed all permission is automatically removed) +# +# example: ynh_permission_delete --permission=editors +# +# usage: ynh_permission_delete --permission="permission" +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_delete() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=( [p]=permission= ) + local permission + ynh_handle_getopts_args "$@" + + yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')" +} + +# Check if a permission exists +# +# usage: ynh_permission_exists --permission=permission +# | arg: -p, --permission= - the permission to check +# | exit: Return 1 if the permission doesn't exist, 0 otherwise +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_exists() { + # Declare an array to define the options of this helper. + local legacy_args=p + local -A args_array=( [p]=permission= ) + local permission + ynh_handle_getopts_args "$@" + + yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission" +} + +# Redefine the url associated to a permission +# +# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] +# [--auth_header=true|false] [--clear_urls] +# | arg: -p, permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. +# | Note that if you want to remove url you can pass an empty sting as arguments (""). +# | arg: -a, add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. +# | arg: -r, remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden +# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application +# | arg: -c, clear_urls - (optional) Clean all urls (url and additional_urls) +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_url() { + # Declare an array to define the options of this helper. + local legacy_args=puarhc + local -A args_array=( [p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls ) + local permission + local url + local add_url + local remove_url + local auth_header + local clear_urls + ynh_handle_getopts_args "$@" + url=${url:-} + add_url=${add_url:-} + remove_url=${remove_url:-} + auth_header=${auth_header:-} + clear_urls=${clear_urls:-} + + if [[ -n $url ]] + then + url=",url='$url'" + fi + + if [[ -n $add_url ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --add_url /urlA /urlB + # will be: + # add_url=['/urlA', '/urlB'] + add_url=",add_url=['${add_url//;/\',\'}']" + fi + + if [[ -n $remove_url ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --remove_url /urlA /urlB + # will be: + # remove_url=['/urlA', '/urlB'] + remove_url=",remove_url=['${remove_url//;/\',\'}']" + fi + + if [[ -n $auth_header ]] + then + if [ $auth_header == "true" ] + then + auth_header=",auth_header=True" + else + auth_header=",auth_header=False" + fi + fi + + if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ] + then + clear_urls=",clear_urls=True" + fi + + yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)" +} + + +# Update a permission for the app +# +# usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] +# [--label="label"] [--show_tile=true|false] [--protected=true|false] +# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -a, add= - the list of group or users to enable add to the permission +# | arg: -r, remove= - the list of group or users to remove from the permission +# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. +# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO +# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator +# | won't be able to add or remove the visitors group of this permission. +# +# Requires YunoHost version 3.7.0 or higher. +ynh_permission_update() { + # Declare an array to define the options of this helper. + local legacy_args=parltP + local -A args_array=( [p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected= ) + local permission + local add + local remove + local label + local show_tile + local protected + ynh_handle_getopts_args "$@" + add=${add:-} + remove=${remove:-} + label=${label:-} + show_tile=${show_tile:-} + protected=${protected:-} + + if [[ -n $add ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --add alice bob + # will be: + # add=['alice', 'bob'] + add=",add=['${add//';'/"','"}']" + fi + if [[ -n $remove ]] + then + # Convert a list from getopts to python list + # Note that getopts separate the args with ';' + # For example: + # --remove alice bob + # will be: + # remove=['alice', 'bob'] + remove=",remove=['${remove//';'/"','"}']" + fi + + if [[ -n $label ]] + then + label=",label='$label'" + fi + + if [[ -n $show_tile ]] + then + if [ $show_tile == "true" ] + then + show_tile=",show_tile=True" + else + show_tile=",show_tile=False" + fi + fi + + if [[ -n $protected ]]; then + if [ $protected == "true" ] + then + protected=",protected=True" + else + protected=",protected=False" + fi + fi + + yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove $label $show_tile $protected , force=True)" +} + +# Check if a permission has an user +# +# example: ynh_permission_has_user --permission=main --user=visitors +# +# usage: ynh_permission_has_user --permission=permission --user=user +# | arg: -p, --permission= - the permission to check +# | arg: -u, --user= - the user seek in the permission +# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise +# +# Requires YunoHost version 3.7.1 or higher. +ynh_permission_has_user() { + local legacy_args=pu + # Declare an array to define the options of this helper. + local -A args_array=( [p]=permission= [u]=user= ) + local permission + local user + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if ! ynh_permission_exists --permission=$permission + then + return 1 + fi + + yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user" +} diff --git a/data/helpers.d/setting b/data/helpers.d/setting index af52b8321..883bd9dfe 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -147,372 +147,3 @@ ynh_webpath_register () { yunohost app register-url $app $domain $path_url } - -# Create a new permission for the app -# -# example 1: ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \ -# --label="My app admin" --show_tile=true -# -# This example will create a new permission permission with this following effect: -# - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'. -# - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin -# -# -# example 2: ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \ -# --label="MyApp API" --protected=true -# -# This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission. -# In case of an API with need to be always public it avoid that the admin break anything. -# With this permission all client will be allowed to access to the url 'domain.tld/api'. -# Note that in this case no tile will be show on the SSO. -# Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application. -# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case. -# So in this case it's better to disable this option for all API. -# -# -# usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false] -# [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false] -# [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Not that if 'show_tile' is enabled, this URL will be the URL of the tile. -# | arg: -A, additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true -# | arg: -a, allowed= - (optional) A list of group/user to allow for the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | Default is "APP_LABEL (permission name)". -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. -# | Default is false (for the permission different than 'main'). -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. -# | By default it's 'false' -# -# If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they -# start with '/'. For example: -# / -> domain.tld/app -# /admin -> domain.tld/app/admin -# domain.tld/app/api -> domain.tld/app/api -# -# 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:". -# For example: -# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ -# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ -# -# Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is: -# - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls' -# - 'url' is used for the url of tile in the SSO (if enabled with the 'show_tile' parameter) -# -# -# About the authentication header (auth_header parameter). -# The SSO pass (by default) to the application theses following HTTP header (linked to the authenticated user) to the application: -# - "Auth-User": username -# - "Remote-User": username -# - "Email": user email -# -# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly. -# See https://github.com/YunoHost/issues/issues/1420 for more informations -# -# -# Requires YunoHost version 3.7.0 or higher. -ynh_permission_create() { - # Declare an array to define the options of this helper. - local legacy_args=puAhaltP - local -A args_array=( [p]=permission= [u]=url= [A]=additional_urls= [h]=auth_header= [a]=allowed= [l]=label= [t]=show_tile= [P]=protected= ) - local permission - local url - local additional_urls - local auth_header - local allowed - local label - local show_tile - local protected - ynh_handle_getopts_args "$@" - url=${url:-} - additional_urls=${additional_urls:-} - auth_header=${auth_header:-} - allowed=${allowed:-} - label=${label:-} - show_tile=${show_tile:-} - protected=${protected:-} - - if [[ -n $url ]] - then - url=",url='$url'" - fi - - if [[ -n $additional_urls ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # By example: - # --additional_urls /urlA /urlB - # will be: - # additional_urls=['/urlA', '/urlB'] - additional_urls=",additional_urls=['${additional_urls//;/\',\'}']" - fi - - if [[ -n $auth_header ]] - then - if [ $auth_header == "true" ] - then - auth_header=",auth_header=True" - else - auth_header=",auth_header=False" - fi - fi - - if [[ -n $allowed ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # By example: - # --allowed alice bob - # will be: - # allowed=['alice', 'bob'] - allowed=",allowed=['${allowed//;/\',\'}']" - fi - - if [[ -n ${label:-} ]]; then - label=",label='$label'" - else - label=",label='$permission'" - fi - - if [[ -n ${show_tile:-} ]] - then - if [ $show_tile == "true" ] - then - show_tile=",show_tile=True" - else - show_tile=",show_tile=False" - fi - fi - - if [[ -n ${protected:-} ]] - then - if [ $protected == "true" ] - then - protected=",protected=True" - else - protected=",protected=False" - fi - fi - - yunohost tools shell -c "from yunohost.permission import permission_create; permission_create('$app.$permission' $url $additional_urls $auth_header $allowed $label $show_tile $protected)" -} - -# Remove a permission for the app (note that when the app is removed all permission is automatically removed) -# -# example: ynh_permission_delete --permission=editors -# -# usage: ynh_permission_delete --permission="permission" -# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# -# Requires YunoHost version 3.7.0 or higher. -ynh_permission_delete() { - # Declare an array to define the options of this helper. - local legacy_args=p - local -A args_array=( [p]=permission= ) - local permission - ynh_handle_getopts_args "$@" - - yunohost tools shell -c "from yunohost.permission import permission_delete; permission_delete('$app.$permission')" -} - -# Check if a permission exists -# -# usage: ynh_permission_exists --permission=permission -# | arg: -p, --permission= - the permission to check -# | exit: Return 1 if the permission doesn't exist, 0 otherwise -# -# Requires YunoHost version 3.7.0 or higher. -ynh_permission_exists() { - # Declare an array to define the options of this helper. - local legacy_args=p - local -A args_array=( [p]=permission= ) - local permission - ynh_handle_getopts_args "$@" - - yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission" -} - -# Redefine the url associated to a permission -# -# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] -# [--auth_header=true|false] [--clear_urls] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Note that if you want to remove url you can pass an empty sting as arguments (""). -# | arg: -a, add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. -# | arg: -r, remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application -# | arg: -c, clear_urls - (optional) Clean all urls (url and additional_urls) -# -# Requires YunoHost version 3.7.0 or higher. -ynh_permission_url() { - # Declare an array to define the options of this helper. - local legacy_args=puarhc - local -A args_array=( [p]=permission= [u]=url= [a]=add_url= [r]=remove_url= [h]=auth_header= [c]=clear_urls ) - local permission - local url - local add_url - local remove_url - local auth_header - local clear_urls - ynh_handle_getopts_args "$@" - url=${url:-} - add_url=${add_url:-} - remove_url=${remove_url:-} - auth_header=${auth_header:-} - clear_urls=${clear_urls:-} - - if [[ -n $url ]] - then - url=",url='$url'" - fi - - if [[ -n $add_url ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # For example: - # --add_url /urlA /urlB - # will be: - # add_url=['/urlA', '/urlB'] - add_url=",add_url=['${add_url//;/\',\'}']" - fi - - if [[ -n $remove_url ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # For example: - # --remove_url /urlA /urlB - # will be: - # remove_url=['/urlA', '/urlB'] - remove_url=",remove_url=['${remove_url//;/\',\'}']" - fi - - if [[ -n $auth_header ]] - then - if [ $auth_header == "true" ] - then - auth_header=",auth_header=True" - else - auth_header=",auth_header=False" - fi - fi - - if [[ -n $clear_urls ]] && [ $clear_urls -eq 1 ] - then - clear_urls=",clear_urls=True" - fi - - yunohost tools shell -c "from yunohost.permission import permission_url; permission_url('$app.$permission' $url $add_url $remove_url $auth_header $clear_urls)" -} - - -# Update a permission for the app -# -# usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] -# [--label="label"] [--show_tile=true|false] [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -a, add= - the list of group or users to enable add to the permission -# | arg: -r, remove= - the list of group or users to remove from the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. -# -# Requires YunoHost version 3.7.0 or higher. -ynh_permission_update() { - # Declare an array to define the options of this helper. - local legacy_args=parltP - local -A args_array=( [p]=permission= [a]=add= [r]=remove= [l]=label= [t]=show_tile= [P]=protected= ) - local permission - local add - local remove - local label - local show_tile - local protected - ynh_handle_getopts_args "$@" - add=${add:-} - remove=${remove:-} - label=${label:-} - show_tile=${show_tile:-} - protected=${protected:-} - - if [[ -n $add ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # For example: - # --add alice bob - # will be: - # add=['alice', 'bob'] - add=",add=['${add//';'/"','"}']" - fi - if [[ -n $remove ]] - then - # Convert a list from getopts to python list - # Note that getopts separate the args with ';' - # For example: - # --remove alice bob - # will be: - # remove=['alice', 'bob'] - remove=",remove=['${remove//';'/"','"}']" - fi - - if [[ -n $label ]] - then - label=",label='$label'" - fi - - if [[ -n $show_tile ]] - then - if [ $show_tile == "true" ] - then - show_tile=",show_tile=True" - else - show_tile=",show_tile=False" - fi - fi - - if [[ -n $protected ]]; then - if [ $protected == "true" ] - then - protected=",protected=True" - else - protected=",protected=False" - fi - fi - - yunohost tools shell -c "from yunohost.permission import user_permission_update; user_permission_update('$app.$permission' $add $remove $label $show_tile $protected , force=True)" -} - -# Check if a permission has an user -# -# example: ynh_permission_has_user --permission=main --user=visitors -# -# usage: ynh_permission_has_user --permission=permission --user=user -# | arg: -p, --permission= - the permission to check -# | arg: -u, --user= - the user seek in the permission -# | exit: Return 1 if the permission doesn't have that user or doesn't exist, 0 otherwise -# -# Requires YunoHost version 3.7.1 or higher. -ynh_permission_has_user() { - local legacy_args=pu - # Declare an array to define the options of this helper. - local -A args_array=( [p]=permission= [u]=user= ) - local permission - local user - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - if ! ynh_permission_exists --permission=$permission - then - return 1 - fi - - yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user" -} From 5c17ac5eeca711931b7c46ea8b94a8da1e62cd3f Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 31 Dec 2020 18:34:37 +0100 Subject: [PATCH 071/363] [wip] add legacy permissions management --- data/helpers.d/permission | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/data/helpers.d/permission b/data/helpers.d/permission index 46d703d7b..fb0e8722b 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -368,3 +368,22 @@ ynh_permission_has_user() { yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user" } + +ynh_legacy_permissions_exists () { + for permission in "skipped" "unprotected" "protected" + do + if ynh_permission_exists --permission="legacy_${permission}_uris"; then + return 0 + fi + done + return 1 +} + +ynh_legacy_permissions_delete_all () { + for permission in "skipped" "unprotected" "protected" + do + if ynh_permission_exists --permission="legacy_${permission}_uris"; then + ynh_permission_delete --permission="legacy_${permission}_uris" + fi + done +} From 86a612fab1fa76f14a3dafed8ceb9360386ea0fc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 04:04:56 +0100 Subject: [PATCH 072/363] c.f. change in the moulinette regarding call_async_output and the handling of stdinfo ... YNH_STDINFO is now a file descriptor instead of a named pipe --- data/helpers.d/logging | 2 +- src/yunohost/hook.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 45b5b7e67..dc32ecba9 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -35,7 +35,7 @@ ynh_print_info() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - echo "$message" >> "$YNH_STDINFO" + echo "$message" >&$YNH_STDINFO } # Ignore the yunohost-cli log to prevent errors with conditional commands diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index eafcaf825..94126fd36 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -387,9 +387,6 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge env['YNH_INTERFACE'] = msettings.get('interface') - stdinfo = os.path.join(tempfile.mkdtemp(), "stdinfo") - env['YNH_STDINFO'] = stdinfo - stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn") with open(stdreturn, 'w') as f: f.write('') @@ -415,10 +412,7 @@ def _hook_exec_bash(path, args, no_trace, chdir, env, user, return_format, logge logger.debug("Executing command '%s'" % ' '.join(command)) - returncode = call_async_output( - command, loggers, shell=False, cwd=chdir, - stdinfo=stdinfo - ) + returncode = call_async_output(command, loggers, shell=False, cwd=chdir) raw_content = None try: From 5f0b1b745069b89d7474386de1f21326137dfabf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 04:06:09 +0100 Subject: [PATCH 073/363] Encoding fixes --- src/yunohost/app.py | 15 +++------------ src/yunohost/service.py | 2 +- src/yunohost/utils/password.py | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 05580fbe1..d3cf5a11c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1873,7 +1873,7 @@ def _get_app_settings(app_id): settings = yaml.load(f) # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... - settings = {k: _encode_string(v) for k, v in settings.items()} + settings = {k: v for k, v in settings.items()} if app_id == settings['id']: return settings except (IOError, TypeError, KeyError): @@ -2300,21 +2300,12 @@ def _value_for_locale(values): for lang in [m18n.locale, m18n.default_locale]: try: - return _encode_string(values[lang]) + return values[lang] except KeyError: continue # Fallback to first value - return _encode_string(values.values()[0]) - - -def _encode_string(value): - """ - Return the string encoded in utf-8 if needed - """ - if isinstance(value, str): - return value.encode('utf8') - return value + return values.values()[0] def _check_manifest_requirements(manifest, app_instance_name): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 1b1fec81b..89d51b740 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -358,7 +358,7 @@ def _get_and_format_service_status(service, infos): # 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.decode('utf-8') == translation_key: + if description == translation_key: description = str(raw_status.get("Description", "")) output = { diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e7ff6c275..ac26ae4bf 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -173,7 +173,7 @@ class PasswordValidator(object): # stdin to avoid it being shown in ps -ef --forest... command = "grep -q -f - %s" % MOST_USED_PASSWORDS p = subprocess.Popen(command.split(), stdin=subprocess.PIPE) - p.communicate(input=password) + p.communicate(input=password.encode('utf-8')) return not bool(p.returncode) From 0335bcbf4c0ddbc9f744451a646cea962109d276 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 04:07:16 +0100 Subject: [PATCH 074/363] .keys() now returns a generator, gotta wrap it in a list() for what we do here --- src/yunohost/user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 67fd43a03..63a5e7d28 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -540,7 +540,7 @@ def user_group_list(short=False, full=False, include_primary_groups=True): groups[name]["permissions"] = [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])] if short: - groups = groups.keys() + groups = list(groups.keys()) return {'groups': groups} @@ -625,7 +625,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface - existing_groups = user_group_list()['groups'].keys() + existing_groups = list(user_group_list()['groups'].keys()) if groupname not in existing_groups: raise YunohostError('group_unknown', group=groupname) @@ -633,7 +633,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): # without the force option... # # We also can't delete "all_users" because that's a special group... - existing_users = user_list()['users'].keys() + existing_users = list(user_list()['users'].keys()) undeletable_groups = existing_users + ["all_users", "visitors"] if groupname in undeletable_groups and not force: raise YunohostError('group_cannot_be_deleted', group=groupname) @@ -669,7 +669,7 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface - existing_users = user_list()['users'].keys() + existing_users = list(user_list()['users'].keys()) # Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam') # Those kind of group should only ever contain the user (e.g. sam) and only this one. From 0434bd5dc22015a06b2756b155768f9f23103c99 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 04:07:27 +0100 Subject: [PATCH 075/363] cmp() doesn't exists anymore --- src/yunohost/domain.py | 15 ++++++--------- src/yunohost/utils/packages.py | 6 +++++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 5d47aefe0..5761474a6 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -62,18 +62,15 @@ def domain_list(exclude_subdomains=False): result_list.append(domain) - def cmp_domain(domain1, domain2): + def cmp_domain(domain): # Keep the main part of the domain and the extension together # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] - domain1 = domain1.split('.') - domain2 = domain2.split('.') - domain1[-1] = domain1[-2] + domain1.pop() - domain2[-1] = domain2[-2] + domain2.pop() - domain1 = list(reversed(domain1)) - domain2 = list(reversed(domain2)) - return cmp(domain1, domain2) + domain = domain.split('.') + domain[-1] = domain[-2] + domain.pop() + domain = list(reversed(domain)) + return domain - result_list = sorted(result_list, cmp_domain) + result_list = sorted(result_list, key=cmp_domain) return { 'domains': result_list, diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index f9ad385f8..5672a7233 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -70,7 +70,11 @@ def meets_version_specifier(pkg_name, specifier): op, req_version = re.search(r'(<<|<=|=|>=|>>) *([\d\.]+)', specifier).groups() req_version = version.parse(req_version) - # cmp is a python builtin that returns (-1, 0, 1) depending on comparison + # Python2 had a builtin that returns (-1, 0, 1) depending on comparison + # c.f. https://stackoverflow.com/a/22490617 + def cmp(a, b): + return (a > b) - (a < b) + deb_operators = { "<<": lambda v1, v2: cmp(v1, v2) in [-1], "<=": lambda v1, v2: cmp(v1, v2) in [-1, 0], From cce020daac089abbbf6a8101cfb9f5ce8ad80f2f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 05:03:55 +0100 Subject: [PATCH 076/363] Uniformize check_output calls to use moulinette helpers which shall now automatically decode() automatically + also strip() etc --- data/hooks/diagnosis/00-basesystem.py | 3 ++- data/hooks/diagnosis/50-systemresources.py | 5 +++-- src/yunohost/app.py | 14 +++++--------- src/yunohost/backup.py | 7 ++++--- src/yunohost/regenconf.py | 10 +++++----- src/yunohost/service.py | 6 +++--- src/yunohost/user.py | 4 ++-- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/data/hooks/diagnosis/00-basesystem.py b/data/hooks/diagnosis/00-basesystem.py index edfb68beb..ecde668fb 100644 --- a/data/hooks/diagnosis/00-basesystem.py +++ b/data/hooks/diagnosis/00-basesystem.py @@ -149,7 +149,8 @@ class BaseSystemDiagnoser(Diagnoser): # "missing some kernel info (see -v), accuracy might be reduced" # Dunno what to do about that but we probably don't want to harass # users with this warning ... - output, err = call.communicate() + output, _ = call.communicate() + output = output.decode() assert call.returncode in (0, 2, 3), "Return code: %s" % call.returncode # If there are multiple lines, sounds like there was some messages diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index f0fac4974..64517f764 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -1,10 +1,11 @@ #!/usr/bin/env python import os import psutil -import subprocess import datetime import re +from moulinette.utils.process import check_output + from yunohost.diagnosis import Diagnoser @@ -119,7 +120,7 @@ class SystemResourcesDiagnoser(Diagnoser): def analyzed_kern_log(): cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true' - out = subprocess.check_output(cmd, shell=True).strip() + out = check_output(cmd) lines = out.split("\n") if out else [] now = datetime.datetime.now() diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d3cf5a11c..0b3f40390 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -39,7 +39,7 @@ from collections import OrderedDict from moulinette import msignals, m18n, msettings from moulinette.utils.log import getActionLogger from moulinette.utils.network import download_json -from moulinette.utils.process import run_commands +from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import read_file, read_json, read_toml, read_yaml, write_to_file, write_to_json, write_to_yaml, chmod, chown, mkdir from yunohost.service import service_status, _run_service_command @@ -411,10 +411,7 @@ def app_change_url(operation_logger, app, domain, path): # 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() - + nginx_errors = check_output("nginx -t; exit 0") raise YunohostError("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors) logger.success(m18n.n("app_change_url_success", @@ -2139,10 +2136,9 @@ def _get_git_last_commit_hash(repository, reference='HEAD'): """ try: - commit = subprocess.check_output( - "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'".format( - repository, reference), - shell=True) + cmd = "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'"\ + .format(repository, reference) + commit = check_output(cmd) except subprocess.CalledProcessError: logger.exception("unable to get last commit from %s", repository) raise ValueError("Unable to get last commit with git") diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 7c75837b0..dfed5cac1 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -41,6 +41,7 @@ from moulinette import msignals, m18n, msettings from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_yaml +from moulinette.utils.process import check_output from yunohost.app import ( app_info, _is_installed, @@ -2386,7 +2387,7 @@ def _recursive_umount(directory): Args: directory -- a directory path """ - mount_lines = subprocess.check_output("mount").split("\n") + mount_lines = check_output("mount").split("\n") points_to_umount = [line.split(" ")[2] for line in mount_lines @@ -2412,8 +2413,8 @@ 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')) + du_output = check_output(['du', '-sb', path], shell=False) + return int(du_output.split()[0]) def binary_to_human(n, customary=False): diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py index 6b369fc8c..9423b1b36 100644 --- a/src/yunohost/regenconf.py +++ b/src/yunohost/regenconf.py @@ -21,7 +21,6 @@ import os import yaml -import subprocess import shutil import hashlib @@ -30,6 +29,7 @@ from datetime import datetime from moulinette import m18n from moulinette.utils import log, filesystem +from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation @@ -654,10 +654,10 @@ def manually_modified_files(): def manually_modified_files_compared_to_debian_default(ignore_handled_by_regenconf=False): # from https://serverfault.com/a/90401 - files = 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) + files = check_output("dpkg-query -W -f='${Conffiles}\n' '*' \ + | awk 'OFS=\" \"{print $2,$1}' \ + | md5sum -c 2>/dev/null \ + | awk -F': ' '$2 !~ /OK/{print $1}'") files = files.strip().split("\n") if ignore_handled_by_regenconf: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 89d51b740..0b6860e49 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -35,6 +35,7 @@ from datetime import datetime from moulinette import m18n from yunohost.utils.error import YunohostError +from moulinette.utils.process import check_output from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, append_to_file, write_to_file @@ -563,8 +564,7 @@ def _give_lock(action, service, p): while son_PID == 0 and p.poll() is 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 = check_output(cmd_get_son_PID).split("=")[1] son_PID = int(son_PID) time.sleep(1) @@ -720,7 +720,7 @@ def _get_journalctl_logs(service, number="all"): services = _get_services() systemd_service = services.get(service, {}).get("actual_systemd_service", service) try: - return subprocess.check_output("journalctl --no-hostname --no-pager -u {0} -n{1}".format(systemd_service, number), shell=True) + return check_output("journalctl --no-hostname --no-pager -u {0} -n{1}".format(systemd_service, number)) except: import traceback return "error while get services logs from journalctl:\n%s" % traceback.format_exc() diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 63a5e7d28..02faff041 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -35,6 +35,7 @@ import copy from moulinette import msignals, msettings, m18n from moulinette.utils.log import getActionLogger +from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError from yunohost.service import service_status @@ -467,8 +468,7 @@ def user_info(username): else: try: cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] - cmd_result = subprocess.check_output(cmd, stderr=subprocess.STDOUT, - shell=True) + cmd_result = check_output(cmd) except Exception as e: cmd_result = "" logger.warning("Failed to fetch quota info ... : %s " % str(e)) From d5da3d899ac53650364a1ff9b2c58a6e26632478 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 05:04:33 +0100 Subject: [PATCH 077/363] Ignore the __pycache__ folder in diagnosis hook folder --- src/yunohost/hook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 94126fd36..067a5d7a7 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -182,7 +182,8 @@ def hook_list(action, list_by='name', show_info=False): def _append_folder(d, folder): # Iterate over and add hook from a folder for f in os.listdir(folder + action): - if f[0] == '.' or f[-1] == '~' or f.endswith(".pyc"): + if f[0] == '.' or f[-1] == '~' or f.endswith(".pyc") \ + or (f.startswith("__") and f.endswith("__")): continue path = '%s%s/%s' % (folder, action, f) priority, name = _extract_filename_parts(f) From 1ac59f987792f4763ad7323b0f3c59f871672538 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 05:04:54 +0100 Subject: [PATCH 078/363] More encode/decode fixes --- src/yunohost/backup.py | 4 ++-- src/yunohost/certificate.py | 2 +- src/yunohost/diagnosis.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index dfed5cac1..a22c33f15 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -177,11 +177,11 @@ class BackupRestoreTargetsManager(object): or (exclude and isinstance(exclude, list) and not include) if include: - return [target.encode("Utf-8") for target in self.targets[category] + return [target for target in self.targets[category] if self.results[category][target] in include] if exclude: - return [target.encode("Utf-8") for target in self.targets[category] + return [target for target in self.targets[category] if self.results[category][target] not in exclude] diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index fc81ae0a6..421303615 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -638,7 +638,7 @@ def _get_status(domain): cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN organization_name = cert.get_issuer().O - valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") + valid_up_to = datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") days_remaining = (valid_up_to - datetime.utcnow()).days if cert_issuer == _name_self_CA(): diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py index e4dd84f6d..93ece21fc 100644 --- a/src/yunohost/diagnosis.py +++ b/src/yunohost/diagnosis.py @@ -451,7 +451,7 @@ class Diagnoser(): key = "diagnosis_description_" + id_ descr = m18n.n(key) # If no description available, fallback to id - return descr if descr.decode('utf-8') != key else id_ + return descr if descr != key else id_ @staticmethod def i18n(report, force_remove_html_tags=False): From caa5e07d4109e6d04bebaaf4bb2b88f2bc90d3c6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 1 Jan 2021 05:20:04 +0100 Subject: [PATCH 079/363] X-Python-Version is obsolete --- debian/control | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/control b/debian/control index 3d7dc41b9..4e57c7a84 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,6 @@ Priority: extra Maintainer: YunoHost Contributors Build-Depends: debhelper (>=9), dh-systemd, dh-python, python3-all (>= 3.7), python3-yaml, python3-jinja2 Standards-Version: 3.9.6 -X-Python-Version: >= 3.7 Homepage: https://yunohost.org/ Package: yunohost From 9eb6fa1961c07c319bd6f4ab62f245d0a75f5626 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 15:53:50 +0100 Subject: [PATCH 080/363] [enh] display domain_path of app on app list --- src/yunohost/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d32fb59a2..f5291e2ac 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -163,6 +163,9 @@ def app_info(app, full=False): 'version': local_manifest.get('version', '-'), } + if "domain" in settings and "path" in settings: + ret["domain_path"] = settings["domain"] + settings["path"] + if not full: return ret From 165d2b32259f03f7c9c7916dc64d9efea54e6fb2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 17:14:53 +0100 Subject: [PATCH 081/363] [mod] no catchall exceptions --- 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 5d47aefe0..7dc4ee74d 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -162,7 +162,7 @@ def domain_add(operation_logger, domain, dyndns=False): # Force domain removal silently try: domain_remove(domain, True) - except: + except Exception: pass raise From f2dc7bacd115bd72a767bab825215f774b3ccb23 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 17:28:08 +0100 Subject: [PATCH 082/363] [doc] tell users how to get all permissions --- 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 31b86c7ae..ff56c2ac8 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -307,7 +307,7 @@ user: api: GET /users/permissions/ arguments: permission: - help: Name of the permission to fetch info about + help: Name of the permission to fetch info about (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) ### user_permission_update() update: @@ -315,7 +315,7 @@ user: api: PUT /users/permissions/ arguments: permission: - help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) + help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) -a: full: --add help: Group or usernames to grant this permission to @@ -346,7 +346,7 @@ user: api: DELETE /users/permissions/ arguments: permission: - help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) + help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) ssh: subcategory_help: Manage ssh access From 2768def391ab580b011d954c880e7c5557d534bc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 18:32:41 +0100 Subject: [PATCH 083/363] [mod] avoid calling app_list in user_permission_list --- src/yunohost/permission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 547510323..d213ac61c 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -51,7 +51,7 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs """ # Fetch relevant informations - from yunohost.app import app_setting, app_list + from yunohost.app import app_setting, _installed_apps from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract ldap = _get_ldap_interface() permissions_infos = ldap.search('ou=permission,dc=yunohost,dc=org', @@ -60,7 +60,7 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs 'URL', 'additionalUrls', 'authHeader', 'label', 'showTile', 'isProtected']) # Parse / organize information to be outputed - apps = [app["id"] for app in app_list()["apps"]] + apps = sorted(_installed_apps()) apps_base_path = {app: app_setting(app, 'domain') + app_setting(app, 'path') for app in apps if app_setting(app, 'domain') and app_setting(app, 'path')} From effc87da2698eb4aad12485cc076bad1c2be5c14 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 18:19:42 +0100 Subject: [PATCH 084/363] [fix] key name of app label wasn't the real label --- 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 f5291e2ac..89480d40d 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -154,12 +154,13 @@ def app_info(app, full=False): raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) local_manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app)) + permissions = user_permission_list(full=True, absolute_urls=True)["permissions"] settings = _get_app_settings(app) ret = { 'description': _value_for_locale(local_manifest['description']), - 'name': local_manifest['name'], + 'name': permissions.get(app + ".main", {}).get("label", local_manifest['name']), 'version': local_manifest.get('version', '-'), } @@ -180,9 +181,10 @@ def app_info(app, full=False): ret['supports_backup_restore'] = (os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")) and os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore"))) ret['supports_multi_instance'] = is_true(local_manifest.get("multi_instance", False)) - permissions = user_permission_list(full=True, absolute_urls=True)["permissions"] + ret['permissions'] = {p: i for p, i in permissions.items() if p.startswith(app + ".")} ret['label'] = permissions.get(app + ".main", {}).get("label") + if not ret['label']: logger.warning("Failed to get label for app %s ?" % app) return ret From c7d315c7e0f8d2e441f129f6a0c2529b2c6b1275 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 17:44:07 +0100 Subject: [PATCH 085/363] [mod] also display app label on remove_domain with apps --- src/yunohost/domain.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 7dc4ee74d..2bd195f53 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -182,7 +182,7 @@ def domain_remove(operation_logger, domain, force=False): """ from yunohost.hook import hook_callback - from yunohost.app import app_ssowatconf + from yunohost.app import app_ssowatconf, app_info from yunohost.utils.ldap import _get_ldap_interface if not force and domain not in domain_list()['domains']: @@ -204,8 +204,9 @@ def domain_remove(operation_logger, domain, force=False): for app in _installed_apps(): settings = _get_app_settings(app) + label = app_info(app)["name"] if settings.get("domain") == domain: - apps_on_that_domain.append("%s (on https://%s%s)" % (app, domain, settings["path"]) if "path" in settings else app) + apps_on_that_domain.append("%s \"%s\" (on https://%s%s)" % (app, label, domain, settings["path"]) if "path" in settings else app) if apps_on_that_domain: raise YunohostError('domain_uninstall_app_first', apps=", ".join(apps_on_that_domain)) From e51a1b670e050c1a1842e44039e8e5fb64adae87 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 19:13:43 +0100 Subject: [PATCH 086/363] [ux] add command instructions and suggest change-url for domain_uninstall_app_first --- locales/en.json | 2 +- src/yunohost/domain.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index c24fc831c..f8f296a7b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -277,7 +277,7 @@ "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", - "domain_uninstall_app_first": "Those applications are still installed on your domain: {apps}. Please uninstall them before proceeding to domain removal", + "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal", "domain_name_unknown": "Domain '{domain}' unknown", "domain_unknown": "Unknown domain", "domains_available": "Available domains:", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 2bd195f53..d581b8426 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -206,10 +206,10 @@ def domain_remove(operation_logger, domain, force=False): settings = _get_app_settings(app) label = app_info(app)["name"] if settings.get("domain") == domain: - apps_on_that_domain.append("%s \"%s\" (on https://%s%s)" % (app, label, domain, settings["path"]) if "path" in settings else app) + apps_on_that_domain.append(" - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app) if apps_on_that_domain: - raise YunohostError('domain_uninstall_app_first', apps=", ".join(apps_on_that_domain)) + raise YunohostError('domain_uninstall_app_first', apps="\n".join(apps_on_that_domain)) operation_logger.start() ldap = _get_ldap_interface() From 865265ea540bc4cc406ec765b5e8d5e83b992b6b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 21:27:29 +0100 Subject: [PATCH 087/363] [enh] add new option --remove-apps to 'yunohost domain remove' --- data/actionsmap/yunohost.yml | 4 ++++ locales/en.json | 1 + src/yunohost/domain.py | 21 ++++++++++++++++----- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 31b86c7ae..6a90d3719 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -448,6 +448,10 @@ domain: help: Domain to delete extra: pattern: *pattern_domain + -r: + full: --remove-apps + help: Remove apps installed on the domain + action: store_true ### domain_dns_conf() dns-conf: diff --git a/locales/en.json b/locales/en.json index f8f296a7b..fea375d4e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -277,6 +277,7 @@ "domain_dyndns_root_unknown": "Unknown DynDNS root domain", "domain_exists": "The domain already exists", "domain_hostname_failed": "Unable to set new hostname. This might cause an issue later (it might be fine).", + "domain_remove_confirm_apps_removal": "Removing this domain will remove those applications:\n{apps}\n\nAre you sure you want to do that? [{answers}]", "domain_uninstall_app_first": "Those applications are still installed on your domain:\n{apps}\n\nPlease uninstall them using 'yunohost app remove the_app_id' or move them to another domain using 'yunohost app change-url the_app_id' before proceeding to domain removal", "domain_name_unknown": "Domain '{domain}' unknown", "domain_unknown": "Unknown domain", diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index d581b8426..c1a48c34e 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -26,7 +26,7 @@ import os import re -from moulinette import m18n, msettings +from moulinette import m18n, msettings, msignals from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger @@ -172,17 +172,18 @@ def domain_add(operation_logger, domain, dyndns=False): @is_unit_operation() -def domain_remove(operation_logger, domain, force=False): +def domain_remove(operation_logger, domain, remove_apps=False, force=False): """ Delete domains Keyword argument: domain -- Domain to delete + remove_apps -- Remove applications installed on the domain force -- Force the domain removal """ from yunohost.hook import hook_callback - from yunohost.app import app_ssowatconf, app_info + from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface if not force and domain not in domain_list()['domains']: @@ -206,10 +207,20 @@ def domain_remove(operation_logger, domain, force=False): settings = _get_app_settings(app) label = app_info(app)["name"] if settings.get("domain") == domain: - apps_on_that_domain.append(" - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app) + apps_on_that_domain.append((app, " - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app)) if apps_on_that_domain: - raise YunohostError('domain_uninstall_app_first', apps="\n".join(apps_on_that_domain)) + if remove_apps: + answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal', + apps="\n".join([x[1] for x in apps_on_that_domain]), + answers='y/N'), color="yellow") + if answer.upper() != "Y": + raise YunohostError("aborting") + + for app, _ in apps_on_that_domain: + app_remove(app) + else: + raise YunohostError('domain_uninstall_app_first', apps="\n".join([x[1] for x in apps_on_that_domain])) operation_logger.start() ldap = _get_ldap_interface() From bbfd5eb9d042c6d8fbaa85644765d5f1deb7591e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 18:39:36 +0100 Subject: [PATCH 088/363] python3: Gotta add list() when we manipulate the dict during the loop --- 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 0b6860e49..36d6e2797 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -599,7 +599,7 @@ def _get_services(): # some services are marked as None to remove them from YunoHost # filter this - for key, value in services.items(): + for key, value in list(services.items()): if value is None: del services[key] From 0781150f2005b478c3d58984ac300a2406e0342f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 18:43:58 +0100 Subject: [PATCH 089/363] Remove python2.7 lint from CI --- .gitlab/ci/lint.gitlab-ci.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 8db1ee756..d240a3af6 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -3,14 +3,6 @@ ######################################## # later we must fix lint and format-check jobs and remove "allow_failure" -lint27: - stage: lint - image: "before-install" - needs: [] - allow_failure: true - script: - - tox -e py27-lint - lint37: stage: lint image: "before-install" @@ -19,13 +11,6 @@ lint37: script: - tox -e py37-lint -invalidcode27: - stage: lint - image: "before-install" - needs: [] - script: - - tox -e py27-invalidcode - invalidcode37: stage: lint image: "before-install" From c755f1701558ac8791af9c9c3bd15ea45f6139aa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 18:54:00 +0100 Subject: [PATCH 090/363] python -> python3 in ci's yml --- .gitlab/ci/test.gitlab-ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index ef21731f3..6cbb89d0c 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -36,7 +36,7 @@ full-tests: - *install_debs - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns script: - - python -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml + - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml needs: - job: build-yunohost artifacts: true @@ -51,70 +51,70 @@ full-tests: root-tests: extends: .test-stage script: - - python -m pytest tests + - python3 -m pytest tests test-apps: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_apps.py + - python3 -m pytest tests/test_apps.py test-appscatalog: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_appscatalog.py + - python3 -m pytest tests/test_appscatalog.py test-appurl: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_appurl.py + - python3 -m pytest tests/test_appurl.py test-apps-arguments-parsing: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_apps_arguments_parsing.py + - python3 -m pytest tests/test_apps_arguments_parsing.py test-backuprestore: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_backuprestore.py + - python3 -m pytest tests/test_backuprestore.py test-changeurl: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_changeurl.py + - python3 -m pytest tests/test_changeurl.py test-permission: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_permission.py + - python3 -m pytest tests/test_permission.py test-settings: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_settings.py + - python3 -m pytest tests/test_settings.py test-user-group: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_user-group.py + - python3 -m pytest tests/test_user-group.py test-regenconf: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_regenconf.py + - python3 -m pytest tests/test_regenconf.py test-service: extends: .test-stage script: - cd src/yunohost - - python -m pytest tests/test_service.py + - python3 -m pytest tests/test_service.py From 4ddf632e75086d5e431dfabee77ce4538cecb37d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 19:10:00 +0100 Subject: [PATCH 091/363] We don't need that print ? --- src/yunohost/tests/test_backuprestore.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index b88fbd5b3..583456cc5 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -23,8 +23,6 @@ def setup_function(function): global maindomain maindomain = _get_maindomain() - print "" - assert backup_test_dependencies_are_met() clean_tmp_backup_directory() From 2b12b67847e913c16754020127b6da816cc4a854 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 19:48:44 +0100 Subject: [PATCH 092/363] python3: Add some list() where that sounds relevant --- src/yunohost/app.py | 2 +- src/yunohost/backup.py | 2 +- src/yunohost/permission.py | 4 ++-- src/yunohost/service.py | 2 +- src/yunohost/utils/legacy.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0b3f40390..27a8a753f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2301,7 +2301,7 @@ def _value_for_locale(values): continue # Fallback to first value - return values.values()[0] + return list(values.values())[0] def _check_manifest_requirements(manifest, app_instance_name): diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a22c33f15..ad5b995e7 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -600,7 +600,7 @@ class BackupManager(): for hook, infos in ret.items() if any(result["state"] == "failed" for result in infos.values())} - if ret_succeed.keys() != []: + if list(ret_succeed.keys()) != []: self.system_return = ret_succeed # Add files from targets (which they put in the CSV) to the list of diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 547510323..92c2c059f 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -106,7 +106,7 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs infos["label"] = "%s (%s)" % (main_perm_label, infos["label"]) if short: - permissions = permissions.keys() + permissions = list(permissions.keys()) return {'permissions': permissions} @@ -668,7 +668,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): For example: re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ - + We can also have less-trivial regexes like: re:^\/api\/.*|\/scripts\/api.js$ """ diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 36d6e2797..347932add 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -490,7 +490,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, raise YunohostError('service_unknown', service=name) if names is []: - names = services.keys() + names = list(services.keys()) logger.warning(m18n.n("service_regen_conf_is_deprecated")) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 1cc0246f3..11d483754 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -100,7 +100,7 @@ class SetupGroupPermissions(): url = "/" if domain and path else None if permission: - known_users = user_list()["users"].keys() + known_users = list(user_list()["users"].keys()) allowed = [user for user in permission.split(',') if user in known_users] else: allowed = ["all_users"] @@ -235,7 +235,7 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): protected_urls = persistent.get("protected_urls", []) + ["re:" + r for r in persistent.get("protected_regex", [])] unprotected_urls = persistent.get("unprotected_urls", []) + ["re:" + r for r in persistent.get("unprotected_regex", [])] - known_users = user_list()["users"].keys() + known_users = list(user_list()["users"].keys()) for legacy_rule in legacy_rules: if legacy_rule in persistent: From 112054f3454d0103ca0638b1512691aeba0d1c65 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 19:51:16 +0100 Subject: [PATCH 093/363] Gotta decode the output of subprocess --- src/yunohost/tests/test_backuprestore.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 583456cc5..c1f211d1b 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -148,7 +148,7 @@ def clean_tmp_backup_directory(): if tmp_backup_directory_is_empty(): return - mount_lines = subprocess.check_output("mount").split("\n") + mount_lines = subprocess.check_output("mount").decode().split("\n") points_to_umount = [line.split(" ")[2] for line in mount_lines @@ -636,6 +636,7 @@ def test_backup_binds_are_readonly(mocker, monkeypatch): confssh = os.path.join(self.work_dir, "conf/ssh") output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh, shell=True, env={'LANG': 'en_US.UTF-8'}) + output = output.decode() assert "Read-only file system" in output From 58522d4105eb4730f3d3f5f26819d7bc8f1e167d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 20:31:42 +0100 Subject: [PATCH 094/363] Had a buggy exception here ... this syntax ain't supported anymore in python3 --- 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 27a8a753f..669463fcd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2910,7 +2910,7 @@ def _load_apps_catalog(): try: apps_catalog_content = read_json(cache_file) if os.path.exists(cache_file) else None except Exception as e: - raise "Unable to read cache for apps_catalog %s : %s" + raise YunohostError("Unable to read cache for apps_catalog %s : %s" % (cache_file, e), raw_msg=True) # Check that the version of the data matches version .... # ... otherwise it means we updated yunohost in the meantime From de126fcdceca9dcbf177170ebfae81aa1aee2d7b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 2 Jan 2021 20:40:51 +0100 Subject: [PATCH 095/363] Don't allow failure for invalid python3 code + fix unsued variables --- .gitlab/ci/lint.gitlab-ci.yml | 1 - src/yunohost/app.py | 2 +- src/yunohost/backup.py | 2 +- .../data_migrations/0019_extend_permissions_features.py | 2 +- src/yunohost/utils/legacy.py | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index d240a3af6..dabe33d62 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -14,7 +14,6 @@ lint37: invalidcode37: stage: lint image: "before-install" - allow_failure: true needs: [] script: - tox -e py37-invalidcode diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 669463fcd..c32596fa7 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -831,7 +831,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu logger.error(m18n.n("app_install_failed", app=app_id, error=error)) failure_message_with_debug_instructions = operation_logger.error(error) # Something wrong happened in Yunohost's code (most probably hook_exec) - except Exception as e: + except Exception: import traceback error = m18n.n('unexpected_error', error="\n" + traceback.format_exc()) logger.error(m18n.n("app_install_failed", app=app_id, error=error)) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index ad5b995e7..18736ef81 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1644,7 +1644,7 @@ class BackupMethod(object): try: subprocess.check_call(["mount", "--rbind", src, dest]) subprocess.check_call(["mount", "-o", "remount,ro,bind", dest]) - except Exception as e: + except Exception: 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 diff --git a/src/yunohost/data_migrations/0019_extend_permissions_features.py b/src/yunohost/data_migrations/0019_extend_permissions_features.py index b20b92e62..6c292e014 100644 --- a/src/yunohost/data_migrations/0019_extend_permissions_features.py +++ b/src/yunohost/data_migrations/0019_extend_permissions_features.py @@ -98,7 +98,7 @@ class MyMigration(Migration): # Migrate old settings migrate_legacy_permission_settings() - except Exception as e: + except Exception: logger.warn(m18n.n("migration_0019_migration_failed_trying_to_rollback")) os.system("systemctl stop slapd") os.system("rm -r /etc/ldap/slapd.d") # To be sure that we don't keep some part of the old config diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 11d483754..ca187baa1 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -22,7 +22,7 @@ class SetupGroupPermissions(): try: objects = ldap.search(target + ",dc=yunohost,dc=org") # ldap search will raise an exception if no corresponding object is found >.> ... - except Exception as e: + except Exception: logger.debug("%s does not exist, no need to delete it" % target) return From c272d20a31ad4619e4153d70db8b5c6fcf226ba4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 3 Jan 2021 04:00:51 +0100 Subject: [PATCH 096/363] Zblerg, forgot an unused 'e' --- 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 18736ef81..6b6fcf2df 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2166,7 +2166,7 @@ def backup_list(with_info=False, human_readable=False): d[archive] = backup_info(archive, human_readable=human_readable) except YunohostError as e: logger.warning(str(e)) - except Exception as e: + except Exception: import traceback logger.warning("Could not check infos for archive %s: %s" % (archive, '\n' + traceback.format_exc())) From fee888d330e834ed27443a57cf31d95db2e60895 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 3 Jan 2021 23:02:57 +0100 Subject: [PATCH 097/363] Small debug line to try to understand why the test are failing on the CI --- src/yunohost/hook.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 53ed5f2e5..80afa4890 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -400,6 +400,9 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): _env = os.environ.copy() _env.update(env) + # FIXME : this is a temporary debug to better understand why the test are failing on the CI but not when ran manually... + logger.debug(_env) + returncode = call_async_output( cmd, loggers, shell=True, cwd=chdir, stdinfo=stdinfo, env=_env From 9bf4a12f3299319ffe6fa57592d50011bb8bc0dc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 3 Jan 2021 23:26:46 +0100 Subject: [PATCH 098/363] Zblerg debug ain't displayed on the CI --- 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 d0e7166a0..3b899e0ad 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -402,7 +402,7 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): _env.update(env) # FIXME : this is a temporary debug to better understand why the test are failing on the CI but not when ran manually... - logger.debug(_env) + logger.warning(_env) returncode = call_async_output( cmd, loggers, shell=True, cwd=chdir, From 9807b76d3430aa74c917d360b12a52bf32634f70 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 3 Jan 2021 23:53:00 +0100 Subject: [PATCH 099/363] Turns out msettings interface is None is ran from pytest because we bypass the usual moulinette init :/ --- src/yunohost/hook.py | 3 --- src/yunohost/tests/conftest.py | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 3b899e0ad..8361b8505 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -401,9 +401,6 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): _env = os.environ.copy() _env.update(env) - # FIXME : this is a temporary debug to better understand why the test are failing on the CI but not when ran manually... - logger.warning(_env) - returncode = call_async_output( cmd, loggers, shell=True, cwd=chdir, stdinfo=stdinfo, env=_env diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 2bda72852..1aaefb993 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -3,7 +3,7 @@ import pytest import sys import moulinette -from moulinette import m18n +from moulinette import m18n, msettings from yunohost.utils.error import YunohostError from contextlib import contextmanager sys.path.append("..") @@ -78,3 +78,4 @@ def pytest_cmdline_main(config): sys.path.insert(0, "/usr/lib/moulinette/") import yunohost yunohost.init(debug=config.option.yunodebug) + msettings["interface"] = "test" From b0accd506ee99f21c26fa7023fd281d58de1b8d0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Jan 2021 01:06:19 +0100 Subject: [PATCH 100/363] Report / patch old usage of foo= in install/backup/restore scripts --- src/yunohost/app.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 625cac9a7..eeb657cdd 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -3138,6 +3138,26 @@ def _patch_legacy_helpers(app_folder): "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?", "replace": r"", "important": False + }, + # Old $1, $2 in backup/restore scripts... + "app=$2": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"app=\$2", + "replace": r"app=$YNH_APP_INSTANCE_NAME", + "important": True + }, + # Old $1, $2 in backup/restore scripts... + "backup_dir=$1": { + "only_for": ["scripts/backup", "scripts/restore"], + "pattern": r"backup_dir=\$1", + "replace": r"backup_dir=.", + "important": True + }, + # Old $1, $2 in install scripts... + # We ain't patching that shit because it ain't trivial to patch all args... + "domain=$1": { + "only_for": ["scripts/install"], + "important": True } } @@ -3156,6 +3176,11 @@ def _patch_legacy_helpers(app_folder): show_warning = False for helper, infos in stuff_to_replace.items(): + + # Ignore if not relevant for this file + if infos.get("only_for") and not any(filename.endswith(f) for f in infos["only_for"]): + continue + # If helper is used, attempt to patch the file if helper in content and infos["pattern"]: content = infos["pattern"].sub(infos["replace"], content) @@ -3163,11 +3188,11 @@ def _patch_legacy_helpers(app_folder): if infos["important"]: show_warning = True - # If the helpert is *still* in the content, it means that we + # If the helper is *still* in the content, it means that we # couldn't patch the deprecated helper in the previous lines. In # that case, abort the install or whichever step is performed if helper in content and infos["important"]: - raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.") + raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", raw_msg=True) if replaced_stuff: From 25ed25e6e93bc6ab07e628a55d6a1eca3905057c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 4 Jan 2021 03:50:25 +0100 Subject: [PATCH 101/363] Annnnd our legacy app is using restore_dir in restore script... --- src/yunohost/app.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index eeb657cdd..c0408006c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -3153,6 +3153,13 @@ def _patch_legacy_helpers(app_folder): "replace": r"backup_dir=.", "important": True }, + # Old $1, $2 in backup/restore scripts... + "restore_dir=$1": { + "only_for": ["scripts/restore"], + "pattern": r"restore_dir=\$1", + "replace": r"restore_dir=.", + "important": True + }, # Old $1, $2 in install scripts... # We ain't patching that shit because it ain't trivial to patch all args... "domain=$1": { From c2a730f26ce8c029627b833a42013827a4674994 Mon Sep 17 00:00:00 2001 From: cricriiiiii Date: Tue, 17 Nov 2020 16:39:28 +0100 Subject: [PATCH 102/363] user update -p without argument allowed when not giving an argument (to avoid clear passwords in bash history) to "yunohost user update -p", now you are prompted for a password --- data/actionsmap/yunohost.yml | 3 +++ src/yunohost/user.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ff56c2ac8..8eee048f2 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -165,8 +165,11 @@ user: full: --change-password help: New password to set metavar: PASSWORD + nargs: "?" + const: 0 extra: pattern: *pattern_password + comment: good_practices_about_user_password --add-mailforward: help: Mailforward addresses to add nargs: "*" diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 67fd43a03..6968744b3 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -325,7 +325,9 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] - if change_password: + if change_password is not None: + if not change_password: + change_password = msignals.prompt(m18n.n("ask_password"), True, True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) From 4c25f442c1c814e73c91b4259e3342b628fbb901 Mon Sep 17 00:00:00 2001 From: cricriiiiii Date: Fri, 11 Dec 2020 23:32:43 +0100 Subject: [PATCH 103/363] checking that we are in the cli interface --- src/yunohost/user.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 6968744b3..edb1c7c8f 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -325,8 +325,12 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= if lastname and firstname: new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] + # change_password is None if user_update is not called to change the password if change_password is not None: - if not change_password: + # when in the cli interface if the option to change the password is called + # without a specified value, change_password will be set to the const 0. + # In this case we prompt for the new password. + if msettings.get('interface') == 'cli' and not change_password: change_password = msignals.prompt(m18n.n("ask_password"), True, True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) From bb914db82e2aea2275af7885632aab1ce3caa667 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Wed, 6 Jan 2021 01:12:27 +0100 Subject: [PATCH 104/363] [fix] Work around to avoid moulinette bug --- data/actionsmap/yunohost.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8eee048f2..843ada746 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -169,7 +169,6 @@ user: const: 0 extra: pattern: *pattern_password - comment: good_practices_about_user_password --add-mailforward: help: Mailforward addresses to add nargs: "*" From 9d79bd8bee14f37f2ce0577a7b5a153da75a1980 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 6 Jan 2021 01:35:02 +0100 Subject: [PATCH 105/363] readd comment of https://github.com/YunoHost/yunohost/pull/1075/commits/aefdd424d8dcb3209dc51c381af62debf9914039 --- data/actionsmap/yunohost.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 843ada746..8eee048f2 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -169,6 +169,7 @@ user: const: 0 extra: pattern: *pattern_password + comment: good_practices_about_user_password --add-mailforward: help: Mailforward addresses to add nargs: "*" From 4a0526ec91ae4c196819ccd5cfe45774bf12208a Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 6 Jan 2021 12:04:09 +0100 Subject: [PATCH 106/363] Add doc --- data/helpers.d/permission | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/helpers.d/permission b/data/helpers.d/permission index fb0e8722b..1791425b5 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -369,6 +369,12 @@ ynh_permission_has_user() { yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user" } +# Check if a legacy permissions exist +# +# usage: ynh_legacy_permissions_exists +# | exit: Return 1 if the permission doesn't exist, 0 otherwise +# +# Requires YunoHost version 4.1.2 or higher. ynh_legacy_permissions_exists () { for permission in "skipped" "unprotected" "protected" do @@ -379,6 +385,17 @@ ynh_legacy_permissions_exists () { return 1 } +# Remove all legacy permissions +# +# usage: ynh_legacy_permissions_delete_all +# +# example: +# if ynh_legacy_permissions_exists +# then +# ynh_legacy_permissions_delete_all +# # You can recreate the required permissions here with ynh_permission_create +# fi +# Requires YunoHost version 4.1.2 or higher. ynh_legacy_permissions_delete_all () { for permission in "skipped" "unprotected" "protected" do From 239dc539a198715073c4f5379980f7a43cd79eb3 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 6 Jan 2021 16:34:55 +0100 Subject: [PATCH 107/363] use read_yaml for json because lol --- src/yunohost/utils/legacy.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 434746a28..f3269cce1 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -2,7 +2,7 @@ import os from moulinette import m18n from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger -from moulinette.utils.filesystem import read_json, write_to_json, read_yaml +from moulinette.utils.filesystem import write_to_json, read_yaml from yunohost.user import user_list, user_group_create, user_group_update from yunohost.app import app_setting, _installed_apps, _get_app_settings, _set_app_settings @@ -211,10 +211,12 @@ def migrate_legacy_permission_settings(app=None): def translate_legacy_rules_in_ssowant_conf_json_persistent(): - if not os.path.exists("/etc/ssowat/conf.json.persistent"): + persistent_file_name = "/etc/ssowat/conf.json.persistent" + if not os.path.exists(persistent_file_name): return - persistent = read_json("/etc/ssowat/conf.json.persistent") + # Ugly hack to try not to misarably fail migration + persistent = read_yaml(persistent_file_name) legacy_rules = [ "skipped_urls", @@ -271,6 +273,6 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): "uris": protected_urls + persistent["permissions"].get("custom_protected", {}).get("uris", []), } - write_to_json("/etc/ssowat/conf.json.persistent", persistent, sort_keys=True, indent=4) + write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) logger.warning("Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system") From 121bcbcc486f36876cd9f6acce7b229af085c2cc Mon Sep 17 00:00:00 2001 From: ppr Date: Sat, 2 Jan 2021 16:39:43 +0000 Subject: [PATCH 108/363] Translated using Weblate (French) Currently translated at 99.6% (628 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 90190c223..47706d038 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -54,7 +54,7 @@ "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine: {apps}. Veuillez d’abord les désinstaller avant de supprimer ce domaine", + "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine '{domain}' :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine '{domain}', vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'", "domain_unknown": "Domaine inconnu", "done": "Terminé", "downloading": "Téléchargement en cours …", @@ -690,5 +690,6 @@ "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimée pour la permission '{permission:s}'", "migration_0019_rollback_success": "Retour à l'état antérieur du système.", "invalid_number": "Doit être un nombre", - "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives" + "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", + "diagnosis_basesystem_hardware_model": "Le modèle ou l'architecture du serveur est '{model}'." } From 69f9ac4b86bd533ab9e57f0fbab05513cd62d587 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 4 Jan 2021 19:22:47 +0000 Subject: [PATCH 109/363] Translated using Weblate (German) Currently translated at 58.0% (366 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 05b6b2a5e..7811d3677 100644 --- a/locales/de.json +++ b/locales/de.json @@ -181,7 +181,7 @@ "certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden", "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", + "domain_cannot_remove_main": "Die primäre Domain konnten nicht entfernt werden. Lege zuerst einen neue primäre Domain Sie können die Domäne '{domain:s}' nicht entfernen, weil Sie die Hauptdomäne ist. Sie müssen zuerst eine andere Domäne als Hauptdomäne festlegen. Sie können das mit dem Befehl 'yunohost domain main-domain -n tun. Hier ist eine Liste der möglichen Domänen: {other_domains:s}", "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": "Die ACME Challenge kann im Moment nicht für {domain} ausgeführt werden, weil in ihrer nginx conf das entsprechende Code-Snippet fehlt... Bitte stellen Sie sicher, dass Ihre nginx-Konfiguration mit 'yunohost tools regen-conf nginx --dry-run --with-diff' auf dem neuesten Stand ist.", "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file:s})", @@ -472,5 +472,8 @@ "diagnosis_http_hairpinning_issue_details": "Das ist wahrscheinlich aufgrund Ihrer ISP Box / Router. Als Konsequenz können Personen von ausserhalb Ihres Netzwerkes aber nicht von innerhalb Ihres lokalen Netzwerkes (wie wahrscheinlich Sie selber?) wie gewohnt auf Ihren Server zugreifen, wenn Sie ihre Domäne oder Ihre öffentliche IP verwenden. Sie können die Situation wahrscheinlich verbessern, indem Sie ein einen Blick in https://yunohost.org/dns_local_network werfen", "diagnosis_http_nginx_conf_not_up_to_date": "Jemand hat anscheinend die Konfiguration von Nginx manuell geändert. Diese Änderung verhindert, dass Yunohost eine Diagnose durchführen kann, wenn er via HTTP erreichbar ist.", "diagnosis_http_bad_status_code": "Anscheinend beantwortet ein anderes Gerät als Ihr Server die Anfrage (Vielleicht ihr Internetrouter).
1. Die häufigste Ursache ist, dass Port 80 (und 443) nicht richtig auf Ihren Server weitergeleitet wird.
2. Bei komplexeren Setups: Vergewissern Sie sich, dass keine Firewall und keine Reverse-Proxy interferieren.", - "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'." + "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", + "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", + "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}" } From 987f08dcd91de02bbfce66b370855e78c49902c5 Mon Sep 17 00:00:00 2001 From: ppr Date: Sun, 3 Jan 2021 22:07:11 +0000 Subject: [PATCH 110/363] Translated using Weblate (French) Currently translated at 99.5% (628 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 47706d038..f50604539 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -691,5 +691,6 @@ "migration_0019_rollback_success": "Retour à l'état antérieur du système.", "invalid_number": "Doit être un nombre", "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", - "diagnosis_basesystem_hardware_model": "Le modèle ou l'architecture du serveur est '{model}'." + "diagnosis_basesystem_hardware_model": "Le modèle ou l'architecture du serveur est '{model}'.", + "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système." } From 6705e707c55865fc1eeac3e61eb04e7f121ce6ba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 6 Jan 2021 18:43:02 +0100 Subject: [PATCH 111/363] Fix translation --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index f50604539..91ec63a5d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -691,6 +691,6 @@ "migration_0019_rollback_success": "Retour à l'état antérieur du système.", "invalid_number": "Doit être un nombre", "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", - "diagnosis_basesystem_hardware_model": "Le modèle ou l'architecture du serveur est '{model}'.", + "diagnosis_basesystem_hardware_model": "Le modèle du serveur est '{model}'.", "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système." } From 2fd5f3886ab6d24b07fb49c41d1e957d38b8f9ea Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 6 Jan 2021 20:34:13 +0100 Subject: [PATCH 112/363] fix locale test --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 91ec63a5d..83ad72612 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -54,7 +54,7 @@ "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine '{domain}' :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine '{domain}', vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'", + "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine, vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'", "domain_unknown": "Domaine inconnu", "done": "Terminé", "downloading": "Téléchargement en cours …", From 54fb87fa543ccb188c8b546d26f095abc945b2a4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 7 Jan 2021 00:52:20 +0100 Subject: [PATCH 113/363] Update changelog for 4.1.3 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index 9b824d9de..c3eaed4ca 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (4.1.3) testing; urgency=low + + - [enh] Do not advertise upgrades for bad-quality apps ([#1066](https://github.com/yunohost/yunohost/pull/1066)) + - [enh] Display domain_path of app in the output of app list ([#1120](https://github.com/yunohost/yunohost/pull/1120)) + - [enh] Diagnosis: report usage of backports repository in apt's sources.list ([#1069](https://github.com/yunohost/yunohost/pull/1069)) + - [mod] Code cleanup, misc fixes (165d2b32, [#1121](https://github.com/yunohost/yunohost/pull/1121), [#1122](https://github.com/yunohost/yunohost/pull/1122), [#1123](https://github.com/yunohost/yunohost/pull/1123), [#1131](https://github.com/yunohost/yunohost/pull/1131)) + - [mod] Also display app label on remove_domain with apps ([#1124](https://github.com/yunohost/yunohost/pull/1124)) + - [enh] Be able to change user password in CLI without writing it in clear ([#1075](https://github.com/YunoHost/yunohost/pull/1075)) + - [enh] New permissions helpers ([#1117](https://github.com/yunohost/yunohost/pull/1117)) + - [i18n] Translations updated for French, German + + Thanks to all contributors <3 ! (C. Wehrli, cricriiiiii, Kay0u, Bram, ljf, ppr) + + -- Alexandre Aubin Thu, 07 Jan 2021 00:46:09 +0100 + yunohost (4.1.2) testing; urgency=low - [enh] diagnosis: Detect moar hardware name (b685a274) From f56a00d4a7d344ffd1ca5de841171734f81508ab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 8 Jan 2021 03:14:07 +0100 Subject: [PATCH 114/363] Update changelog for 4.1.4 --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index c3eaed4ca..d1a1b3673 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (4.1.4) stable; urgency=low + + - [fix] firewall: force source port for UPnP. ([#1109](https://github.com/yunohost/yunohost/pull/1109)) + - Stable release + + Thanks to all contributors <3 ! (Léo Le Bouter) + + -- Alexandre Aubin Fri, 08 Jan 2021 03:09:14 +0100 + yunohost (4.1.3) testing; urgency=low - [enh] Do not advertise upgrades for bad-quality apps ([#1066](https://github.com/yunohost/yunohost/pull/1066)) From b25cde0b672c62197880d1db67197684527ea4ee Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 8 Jan 2021 15:57:41 +0100 Subject: [PATCH 115/363] [fix] Make sure relay_user var exists in all cases (otherwise in the jinja template later, relay_user != "" is True if the var doesn't exists...) --- data/hooks/conf_regen/19-postfix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 29787576e..1af4f345f 100755 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -26,11 +26,13 @@ do_pre_regen() { # Add possibility to specify a relay # Could be useful with some isp with no 25 port open or more complex setup + export relay_port="" + export relay_user="" export relay_host="$(yunohost settings get 'smtp.relay.host')" if [ -n "${relay_host}" ] then - export relay_port="$(yunohost settings get 'smtp.relay.port')" - export relay_user="$(yunohost settings get 'smtp.relay.user')" + relay_port="$(yunohost settings get 'smtp.relay.port')" + relay_user="$(yunohost settings get 'smtp.relay.user')" relay_password="$(yunohost settings get 'smtp.relay.password')" # Avoid to display "Relay account paswword" to other users From 48672b1ec0a5e2303a59292f8e2cb3356401307e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 8 Jan 2021 16:21:25 +0100 Subject: [PATCH 116/363] Update changelog for 4.1.4.1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index d1a1b3673..82245665b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.1.4.1) stable; urgency=low + + - [hotfix] Postfix conf always included the relay snippets (b25cde0b) + + -- Alexandre Aubin Fri, 08 Jan 2021 16:21:07 +0100 + yunohost (4.1.4) stable; urgency=low - [fix] firewall: force source port for UPnP. ([#1109](https://github.com/yunohost/yunohost/pull/1109)) From 8f1b05f3cf9cb3367e0fb8b882da981ca0c8d82a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Jan 2021 18:08:23 +0100 Subject: [PATCH 117/363] [fix] Prevent info from being redacted (because of foobar_key=) by the logging system --- data/helpers.d/utils | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 3ab56747e..70643c64a 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -583,12 +583,12 @@ ynh_app_upstream_version () { if [[ "$manifest" != "" ]] && [[ -e "$manifest" ]]; then - version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") + version_key_=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") else - version_key=$YNH_APP_MANIFEST_VERSION + version_key_=$YNH_APP_MANIFEST_VERSION fi - echo "${version_key/~ynh*/}" + echo "${version_key_/~ynh*/}" } # Read package version from the manifest @@ -611,8 +611,8 @@ ynh_app_package_version () { # Manage arguments with getopts ynh_handle_getopts_args "$@" - version_key=$YNH_APP_MANIFEST_VERSION - echo "${version_key/*~ynh/}" + version_key_=$YNH_APP_MANIFEST_VERSION + echo "${version_key_/*~ynh/}" } # Checks the app version to upgrade with the existing app version and returns: From 00508c96980eded5ba9b1e0b28b72c3056c43ffd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Jan 2021 18:10:41 +0100 Subject: [PATCH 118/363] For some reason sometimes submetadata is None ... --- 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 cf108b989..7e9ae18e6 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -257,7 +257,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su except Exception: continue - if submetadata.get("parent") == base_filename: + if submetadata and submetadata.get("parent") == base_filename: yield { "name": filename[:-len(METADATA_FILE_EXT)], "description": _get_description_from_name(filename[:-len(METADATA_FILE_EXT)]), From ac4b62cebc1214862767be564d199d62bc16e698 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Jan 2021 18:16:08 +0100 Subject: [PATCH 119/363] Reduce the noise in logs because of ynh_app_setting --- data/helpers.d/setting | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index 883bd9dfe..e3c9c2f34 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -78,6 +78,7 @@ ynh_app_setting_delete() { # ynh_app_setting() { + set +o xtrace # set +x ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python2.7 - < Date: Sat, 9 Jan 2021 19:00:00 +0100 Subject: [PATCH 120/363] Update changelog for 4.1.4.2 --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 82245665b..d7abb6c92 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +yunohost (4.1.4.2) stable; urgency=low + + - [fix] Prevent info from being redacted (because of foobar_key=) by the logging system (8f1b05f3) + - [fix] For some reason sometimes submetadata is None ... (00508c96) + - [enh] Reduce the noise in logs because of ynh_app_setting (ac4b62ce) + + -- Alexandre Aubin Sat, 09 Jan 2021 18:59:01 +0100 + yunohost (4.1.4.1) stable; urgency=low - [hotfix] Postfix conf always included the relay snippets (b25cde0b) From 30dde208dc062e5a0477328145a8e1ddb9e67360 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 9 Jan 2021 23:57:09 +0100 Subject: [PATCH 121/363] Fix ynh_replace_vars in case var is define but empty --- data/helpers.d/utils | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 70643c64a..62d7b0c0b 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -393,7 +393,8 @@ ynh_replace_vars () { for one_var in "${uniques_vars[@]}" do # Validate that one_var is indeed defined - test -n "${!one_var:-}" || ynh_die --message="\$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" + # Explanation for the weird '+x' syntax: https://stackoverflow.com/a/13864829 + test -n "${one_var+x}" || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" # Escape delimiter in match/replace string match_string="__${one_var^^}__" From 74d956f92b07a2179cfc5a37266d33a5ee106534 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Fri, 1 Jan 2021 21:30:40 +0100 Subject: [PATCH 122/363] [enh] add --force to 'yunohost domain remove --remove-apps' --- data/actionsmap/yunohost.yml | 4 ++++ src/yunohost/domain.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 6a90d3719..37ed79141 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -452,6 +452,10 @@ domain: full: --remove-apps help: Remove apps installed on the domain action: store_true + -f: + full: --force + help: Do not ask confirmation to remove apps + action: store_true ### domain_dns_conf() dns-conf: diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index c1a48c34e..879e47c6c 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -179,7 +179,8 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): Keyword argument: domain -- Domain to delete remove_apps -- Remove applications installed on the domain - force -- Force the domain removal + force -- Force the domain removal and don't not ask confirmation to + remove apps if remove_apps is specified """ from yunohost.hook import hook_callback @@ -211,11 +212,12 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): if apps_on_that_domain: if remove_apps: - answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal', - apps="\n".join([x[1] for x in apps_on_that_domain]), - answers='y/N'), color="yellow") - if answer.upper() != "Y": - raise YunohostError("aborting") + if not yes: + answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal', + apps="\n".join([x[1] for x in apps_on_that_domain]), + answers='y/N'), color="yellow") + if answer.upper() != "Y": + raise YunohostError("aborting") for app, _ in apps_on_that_domain: app_remove(app) From fffbd2d10f75fa69e02d72941b50b9b274525690 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 10 Jan 2021 01:17:34 +0100 Subject: [PATCH 123/363] [fix] only ask about remove apps from a domain being remove in cli --- 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 879e47c6c..8354b2b26 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -212,7 +212,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): if apps_on_that_domain: if remove_apps: - if not yes: + if msettings.get('interface') == "cli" and not force: answer = msignals.prompt(m18n.n('domain_remove_confirm_apps_removal', apps="\n".join([x[1] for x in apps_on_that_domain]), answers='y/N'), color="yellow") From b5bbb44a95a4aba68234bf3d6309144b69dad6b0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 10 Jan 2021 01:51:39 +0100 Subject: [PATCH 124/363] Make sure to use a keyword argument since we have a new arg --- src/yunohost/domain.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 8354b2b26..949a964f2 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -161,7 +161,7 @@ def domain_add(operation_logger, domain, dyndns=False): except Exception: # Force domain removal silently try: - domain_remove(domain, True) + domain_remove(domain, force=True) except Exception: pass raise @@ -187,6 +187,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): from yunohost.app import app_ssowatconf, app_info, app_remove from yunohost.utils.ldap import _get_ldap_interface + # the 'force' here is related to the exception happening in domain_add ... + # we don't want to check the domain exists because the ldap add may have + # failed if not force and domain not in domain_list()['domains']: raise YunohostError('domain_name_unknown', domain=domain) From 807b577cf27b363bf6db6daade2d6ad836cdd08e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 10 Jan 2021 01:59:24 +0100 Subject: [PATCH 125/363] Update changelog for 4.1.4.3 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index d7abb6c92..dfe5f65b7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.1.4.3) stable; urgency=low + + - [fix] ynh_replace_vars in case var is defined but empty (30dde208) + + -- Alexandre Aubin Sun, 10 Jan 2021 01:58:35 +0100 + yunohost (4.1.4.2) stable; urgency=low - [fix] Prevent info from being redacted (because of foobar_key=) by the logging system (8f1b05f3) From a665c702d0a96d7617892acf3cf3e39c62790cd0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 10 Jan 2021 02:07:24 +0100 Subject: [PATCH 126/363] Annnd of course we have references to python2 in bash stuff and other misc scripts --- data/helpers.d/setting | 2 +- data/helpers.d/utils | 2 +- data/hooks/conf_regen/01-yunohost | 2 +- debian/rules | 2 +- doc/generate_helper_doc.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index af52b8321..d1babd140 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -78,7 +78,7 @@ ynh_app_setting_delete() { # ynh_app_setting() { - ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python2.7 - < $output_path } diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index c4120d487..6ac61d07a 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -115,7 +115,7 @@ do_post_regen() { } _update_services() { - python2 - << EOF + python3 - << EOF import yaml diff --git a/debian/rules b/debian/rules index 8afe372b5..54ebc5781 100755 --- a/debian/rules +++ b/debian/rules @@ -5,7 +5,7 @@ #export DH_VERBOSE=1 %: - dh ${@} --with=python2,systemd + dh ${@} --with=python3,systemd override_dh_auto_build: # Generate bash completion file diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index bc9611c8f..908d6300e 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -1,4 +1,4 @@ -#!/usr/env/python2.7 +#!/usr/env/python3 import os import glob From bca527aca5684ac31a42c4e6a727e7fa7db86360 Mon Sep 17 00:00:00 2001 From: Saxodwarf <40502346+Saxodwarf@users.noreply.github.com> Date: Sun, 10 Jan 2021 12:09:04 +0100 Subject: [PATCH 127/363] Add the -F flag to prevent grep from using the user password as a regex pattern --- src/yunohost/utils/password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e7ff6c275..14f42ca31 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -171,7 +171,7 @@ class PasswordValidator(object): # Grep the password in the file # We use '-f -' to feed the pattern (= the password) through # stdin to avoid it being shown in ps -ef --forest... - command = "grep -q -f - %s" % MOST_USED_PASSWORDS + command = "grep -q -F -f - %s" % MOST_USED_PASSWORDS p = subprocess.Popen(command.split(), stdin=subprocess.PIPE) p.communicate(input=password) return not bool(p.returncode) From c56883d027ad8cce434a2edce536264fc547267c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jan 2021 14:15:45 +0100 Subject: [PATCH 128/363] Gotta explicitly return 0, otherwise the return code of last command is used, which in that case is 1 ... --- data/helpers.d/apt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 7c6de912d..1998c80f0 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -550,7 +550,7 @@ ynh_pin_repo () { fi # Sury pinning is managed by the regenconf in the core... - [[ "$name" != "extra_php_version" ]] || return + [[ "$name" != "extra_php_version" ]] || return 0 mkdir --parents "/etc/apt/preferences.d" echo "Package: $package From 9d2dac5062d55b85fdcc47195453726610e64e26 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jan 2021 14:19:03 +0100 Subject: [PATCH 129/363] Update changelog for 4.1.4.4 --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index dfe5f65b7..df242d76f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (4.1.4.4) stable; urgency=low + + - [fix] Add the -F flag to grep command for fixed string mode, prevent special chars in the password to be interpreted as regex pattern ([#1132](https://github.com/yunohost/yunohost/pull/1132)) + - [fix] apt helpers: explicitly return 0, otherwise the return code of last command is used, which in that case is 1 ... (c56883d0) + + Thanks to all contributors <3 ! (Saxodwarf) + + -- Alexandre Aubin Mon, 11 Jan 2021 14:17:37 +0100 + yunohost (4.1.4.3) stable; urgency=low - [fix] ynh_replace_vars in case var is defined but empty (30dde208) From 1d2b1d960176beca26d2bade4ccec1be58ecb2b8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 11 Jan 2021 23:26:29 +0100 Subject: [PATCH 130/363] [internally screaming] Follow-up of 116a15f9f1 ... breaking cert regen conf on some setup ... --- src/yunohost/certificate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c9451c2be..2e4f1623d 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -818,11 +818,11 @@ def _regen_dnsmasq_if_needed(): for domainconf in domainsconf: # Look for the IP, it's in the lines with this format : - # address=/the.domain.tld/11.22.33.44 + # host-record=the.domain.tld,11.22.33.44 for line in open(domainconf).readlines(): - if not line.startswith("address"): + if not line.startswith("host-record"): continue - ip = line.strip().split("/")[2] + ip = line.strip().split(",")[-1] # Compared found IP to current IPv4 / IPv6 # IPv6 IPv4 From 5999f13142297de03ce247e8efc5c1d28ef0c270 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 12 Jan 2021 10:21:20 +0100 Subject: [PATCH 131/363] fix boolean arg parse --- src/yunohost/app.py | 4 ++-- .../tests/test_apps_arguments_parsing.py | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8f75889c5..007ee2d37 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2537,10 +2537,10 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser): if isinstance(question.value, bool): return 1 if question.value else 0 - if str(question.value).lower() in ["1", "yes", "y"]: + if str(question.value).lower() in ["1", "yes", "y", "true"]: return 1 - if str(question.value).lower() in ["0", "no", "n"]: + if str(question.value).lower() in ["0", "no", "n", "false"]: return 0 raise YunohostError('app_argument_choice_invalid', name=question.name, diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 88c235252..ff5d1d15d 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -616,6 +616,18 @@ def test_parse_args_in_yunohost_format_boolean_all_yes(): _parse_args_in_yunohost_format({"some_boolean": True}, questions) == expected_result ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "True"}, questions) == + expected_result + ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions) == + expected_result + ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "true"}, questions) == + expected_result + ) def test_parse_args_in_yunohost_format_boolean_all_no(): @@ -653,6 +665,18 @@ def test_parse_args_in_yunohost_format_boolean_all_no(): _parse_args_in_yunohost_format({"some_boolean": False}, questions) == expected_result ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "False"}, questions) == + expected_result + ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions) == + expected_result + ) + assert ( + _parse_args_in_yunohost_format({"some_boolean": "false"}, questions) == + expected_result + ) # XXX apparently boolean are always False (0) by default, I'm not sure what to think about that From e11741ca949e26292daaa5ef053c414f380bc789 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 12 Jan 2021 10:53:12 +0100 Subject: [PATCH 132/363] fix default arg parse for password --- src/yunohost/app.py | 4 ++++ src/yunohost/tests/test_apps_arguments_parsing.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8f75889c5..bf51d0c4a 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2423,6 +2423,10 @@ class YunoHostArgumentFormatParser(object): if parsed_question.ask is None: parsed_question.ask = "Enter value for '%s':" % parsed_question.name + + # Empty value is parsed as empty string + if parsed_question.default == "": + parsed_question.default = None return parsed_question diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 88c235252..b501e5c4f 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -260,6 +260,10 @@ def test_parse_args_in_yunohost_format_password_no_input_optional(): assert _parse_args_in_yunohost_format(answers, questions) == expected_result + questions = [{"name": "some_password", "type": "password", "optional": True, "default": ""}] + + assert _parse_args_in_yunohost_format(answers, questions) == expected_result + def test_parse_args_in_yunohost_format_password_optional_with_input(): questions = [ From 2ace0741c4ac97c986643346b123b70e3763a3ef Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 12 Jan 2021 13:53:38 +0100 Subject: [PATCH 133/363] set ynh_check_app_version_changed as deprecated --- data/helpers.d/utils | 45 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 62d7b0c0b..851ce1cc3 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -620,49 +620,26 @@ ynh_app_package_version () { # # - UPGRADE_APP if the upstream app version has changed # - UPGRADE_PACKAGE if only the YunoHost package has changed -# -# It stops the current script without error if the package is up-to-date +# - UPGRADE_FULL if both the upstream app version and the YunoHost package version have changed +# - UPGRADE_FORCED if the upstream app version and the current installed app version are the same but the parameter --force has been used +# - DOWNGRADE_FORCED if the upstream app version is bellow the current installed app version but the parameter --force has been used # # This helper should be used to avoid an upgrade of an app, or the upstream part # of it, when it's not needed # # To force an upgrade, even if the package is up to date, -# you have to set the variable YNH_FORCE_UPGRADE before. -# example: sudo YNH_FORCE_UPGRADE=1 yunohost app upgrade MyApp +# you have to use the parameter --force (or -F). +# example: sudo yunohost app upgrade MyApp --force +# +# Deprecated helper, use the YNH_APP_UPGRADE_TYPE variable instead. # # usage: ynh_check_app_version_changed # # Requires YunoHost version 3.5.0 or higher. ynh_check_app_version_changed () { - local force_upgrade=${YNH_FORCE_UPGRADE:-0} - local package_check=${PACKAGE_CHECK_EXEC:-0} + ynh_print_warn --message="The helper ynh_check_app_version_changed is deprecated. Use the YNH_APP_UPGRADE_TYPE variable instead." + local return_value=${YNH_APP_UPGRADE_TYPE} - # By default, upstream app version has changed - local return_value="UPGRADE_APP" - - local current_version=$(ynh_read_manifest --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json" --manifest_key="version" || echo 1.0) - local current_upstream_version="$(ynh_app_upstream_version --manifest="/etc/yunohost/apps/$YNH_APP_INSTANCE_NAME/manifest.json")" - local update_version=$(ynh_read_manifest --manifest="../manifest.json" --manifest_key="version" || echo 1.0) - local update_upstream_version="$(ynh_app_upstream_version)" - - if [ "$current_version" == "$update_version" ] - then - # Complete versions are the same - if [ "$force_upgrade" != "0" ] - then - ynh_print_info --message="Upgrade forced by YNH_FORCE_UPGRADE." - unset YNH_FORCE_UPGRADE - elif [ "$package_check" != "0" ] - then - ynh_print_info --message="Upgrade forced for package check." - else - ynh_die "Up-to-date, nothing to do" 0 - fi - elif [ "$current_upstream_version" == "$update_upstream_version" ] - then - # Upstream versions are the same, only YunoHost package versions differ - return_value="UPGRADE_PACKAGE" - fi echo $return_value } @@ -700,12 +677,12 @@ ynh_compare_current_package_version() { # Check the syntax of the versions if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]] then - ynh_die "Invalid argument for version." + ynh_die --message="Invalid argument for version." fi # Check validity of the comparator if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then - ynh_die "Invialid comparator must be : lt, le, eq, ne, ge, gt" + ynh_die --message="Invialid comparator must be : lt, le, eq, ne, ge, gt" fi # Return the return value of dpkg --compare-versions From 5b1cee4b7092ce1ab783a7ea219b9939f615484a Mon Sep 17 00:00:00 2001 From: Kayou Date: Wed, 13 Jan 2021 11:21:41 +0100 Subject: [PATCH 134/363] One priority arg is enough --- data/helpers.d/php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/php b/data/helpers.d/php index a2caf5387..0dd589956 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -367,7 +367,7 @@ ynh_install_php () { fi # Add an extra repository for those packages - ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --priority=995 --name=extra_php_version --priority=600 + ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 # Install requested dependencies from this extra repository. # Install PHP-FPM first, otherwise PHP will install apache as a dependency. From 090cd0b13a6b7755111288b91232375863c437f0 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 13 Jan 2021 11:34:41 +0100 Subject: [PATCH 135/363] copy the behavior of the old ynh_check_app_version_changed helper --- data/helpers.d/utils | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 851ce1cc3..24121df30 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -618,11 +618,8 @@ ynh_app_package_version () { # Checks the app version to upgrade with the existing app version and returns: # -# - UPGRADE_APP if the upstream app version has changed # - UPGRADE_PACKAGE if only the YunoHost package has changed -# - UPGRADE_FULL if both the upstream app version and the YunoHost package version have changed -# - UPGRADE_FORCED if the upstream app version and the current installed app version are the same but the parameter --force has been used -# - DOWNGRADE_FORCED if the upstream app version is bellow the current installed app version but the parameter --force has been used +# - UPGRADE_APP otherwise # # This helper should be used to avoid an upgrade of an app, or the upstream part # of it, when it's not needed @@ -631,15 +628,17 @@ ynh_app_package_version () { # you have to use the parameter --force (or -F). # example: sudo yunohost app upgrade MyApp --force # -# Deprecated helper, use the YNH_APP_UPGRADE_TYPE variable instead. -# # usage: ynh_check_app_version_changed # # Requires YunoHost version 3.5.0 or higher. ynh_check_app_version_changed () { - ynh_print_warn --message="The helper ynh_check_app_version_changed is deprecated. Use the YNH_APP_UPGRADE_TYPE variable instead." local return_value=${YNH_APP_UPGRADE_TYPE} + if [ "$return_value" == "UPGRADE_FULL" ] || [ "$return_value" == "UPGRADE_FORCED" ] || [ "$return_value" == "DOWNGRADE_FORCED" ] + then + return_value="UPGRADE_APP" + fi + echo $return_value } From d5efb06b6221bdb48d6ef343c91d834fb363a059 Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Thu, 14 Jan 2021 11:21:07 +0100 Subject: [PATCH 136/363] Upgrade n to v7.0.0 --- data/helpers.d/nodejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index f84f908b4..2e1c787cf 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -1,6 +1,6 @@ #!/bin/bash -n_version=6.7.0 +n_version=7.0.0 n_install_dir="/opt/node_n" 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. @@ -18,7 +18,7 @@ ynh_install_n () { # Build an app.src for n mkdir --parents "../conf" echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=92e00fa86d1c4e8dc6ca8df7e75fc93afe8f71949890ef67c40555df4efc4abe" > "../conf/n.src" +SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > "../conf/n.src" # Download and extract n ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n # Install n From 7f1938df6ff8ef334cd68543a1129c26239c8871 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 14 Jan 2021 15:47:10 +0100 Subject: [PATCH 137/363] ynh_exec_warn_less dyndns backup --- data/hooks/backup/42-conf_ynh_dyndns | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/backup/42-conf_ynh_dyndns b/data/hooks/backup/42-conf_ynh_dyndns index 323464108..3dbcc2780 100644 --- a/data/hooks/backup/42-conf_ynh_dyndns +++ b/data/hooks/backup/42-conf_ynh_dyndns @@ -7,5 +7,5 @@ mkdir -p $YNH_CWD cd "$YNH_CWD" # Backup the configuration -ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory -ynh_backup --src_path="/etc/cron.d/yunohost-dyndns" --not_mandatory +ynh_exec_warn_less ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory +ynh_exec_warn_less ynh_backup --src_path="/etc/cron.d/yunohost-dyndns" --not_mandatory From 7e3735394328d9abbfb0727de75772c5acc8a878 Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Thu, 7 Jan 2021 15:23:24 +0000 Subject: [PATCH 138/363] Translated using Weblate (Catalan) Currently translated at 100.0% (631 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index a99a8c5f5..cf0f4449b 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -606,7 +606,7 @@ "diagnosis_dns_point_to_doc": "Consulteu la documentació a https://yunohost.org/dns_config si necessiteu ajuda per configurar els registres DNS.", "diagnosis_mail_outgoing_port_25_ok": "El servidor de correu electrònic SMTP pot enviar correus electrònics (el port de sortida 25 no està bloquejat).", "diagnosis_mail_outgoing_port_25_blocked_details": "Primer heu d'intentar desbloquejar el port 25 en la interfície del vostre router o en la interfície del vostre allotjador. (Alguns proveïdors d'allotjament demanen enviar un tiquet de suport en aquests casos).", - "diagnosis_mail_ehlo_ok": "El servidor de correu electrònic SMTP no és accessible des de l'exterior i per tant no pot rebre correus electrònics!", + "diagnosis_mail_ehlo_ok": "El servidor de correu electrònic SMTP és accessible des de l'exterior i per tant pot rebre correus electrònics!", "diagnosis_mail_ehlo_unreachable": "El servidor de correu electrònic SMTP no és accessible des de l'exterior amb IPv{ipversion}. No podrà rebre correus electrònics.", "diagnosis_mail_ehlo_bad_answer": "Un servei no SMTP a respost en el port 25 amb IPv{ipversion}", "diagnosis_mail_ehlo_bad_answer_details": "Podria ser que sigui per culpa d'una altra màquina responent en lloc del servidor.", From 00944c243de9045d89ee7be8b318e5ed341a355a Mon Sep 17 00:00:00 2001 From: xaloc33 Date: Thu, 7 Jan 2021 13:06:57 +0000 Subject: [PATCH 139/363] Translated using Weblate (Catalan) Currently translated at 100.0% (631 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index cf0f4449b..716ffce5f 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -140,7 +140,7 @@ "domain_dyndns_already_subscribed": "Ja us heu subscrit a un domini DynDNS", "domain_dyndns_root_unknown": "Domini DynDNS principal desconegut", "domain_hostname_failed": "No s'ha pogut establir un nou nom d'amfitrió. Això podria causar problemes més tard (podria no passar res).", - "domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini: {apps}. Desinstal·leu les abans d'eliminar el domini", + "domain_uninstall_app_first": "Aquestes aplicacions encara estan instal·lades en el vostre domini:\n{apps}\n\nDesinstal·leu-les utilitzant l'ordre «yunohost app remove id_de_lapplicació» o moveu-les a un altre domini amb «yunohost app change-url id_de_lapplicació» abans d'eliminar el domini", "domain_unknown": "Domini desconegut", "domains_available": "Dominis disponibles:", "done": "Fet", @@ -712,5 +712,7 @@ "app_label_deprecated": "Aquesta ordre està desestimada! Si us plau utilitzeu la nova ordre «yunohost user permission update» per gestionar l'etiqueta de l'aplicació.", "app_argument_password_no_default": "Hi ha hagut un error al analitzar l'argument de la contrasenya «{name}»: l'argument de contrasenya no pot tenir un valor per defecte per raons de seguretat", "additional_urls_already_removed": "URL addicional «{url:s}» ja ha estat eliminada per al permís «{permission:s}»", - "additional_urls_already_added": "URL addicional «{url:s}» ja ha estat afegida per al permís «{permission:s}»" + "additional_urls_already_added": "URL addicional «{url:s}» ja ha estat afegida per al permís «{permission:s}»", + "diagnosis_backports_in_sources_list": "Sembla que apt (el gestor de paquets) està configurat per utilitzar el repositori backports. A menys de saber el que esteu fent, recomanem fortament no instal·lar paquets de backports, ja que poder causar inestabilitats o conflictes en el sistema.", + "diagnosis_basesystem_hardware_model": "El model del servidor és {model}" } From f46a3e1ef1da7b07a78ad192ea89c49c6c6767a5 Mon Sep 17 00:00:00 2001 From: "Jorge-vitrubio.net" Date: Thu, 7 Jan 2021 17:03:41 +0000 Subject: [PATCH 140/363] Translated using Weblate (Spanish) Currently translated at 92.5% (584 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/es/ --- locales/es.json | 127 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/locales/es.json b/locales/es.json index 50d1a6e1b..21944c372 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,7 +1,7 @@ { "action_invalid": "Acción no válida '{action:s} 1'", "admin_password": "Contraseña administrativa", - "admin_password_change_failed": "No se puede cambiar la contraseña", + "admin_password_change_failed": "No se pudo cambiar la contraseña", "admin_password_changed": "La contraseña de administración fue cambiada", "app_already_installed": "{app:s} ya está instalada", "app_argument_choice_invalid": "Use una de estas opciones «{choices:s}» para el argumento «{name:s}»", @@ -12,7 +12,7 @@ "app_install_files_invalid": "Estos archivos no se pueden instalar", "app_manifest_invalid": "Algo va mal con el manifiesto de la aplicación: {error}", "app_not_correctly_installed": "La aplicación {app:s} 8 parece estar incorrectamente instalada", - "app_not_installed": "No se pudo encontrar la aplicación «{app:s}» en la lista de aplicaciones instaladas: {all_apps}", + "app_not_installed": "No se pudo encontrar «{app:s}» en la lista de aplicaciones instaladas: {all_apps}", "app_not_properly_removed": "La {app:s} 0 no ha sido desinstalada correctamente", "app_removed": "Eliminado {app:s}", "app_requirements_checking": "Comprobando los paquetes necesarios para {app}…", @@ -28,8 +28,8 @@ "ask_main_domain": "Dominio principal", "ask_new_admin_password": "Nueva contraseña administrativa", "ask_password": "Contraseña", - "backup_app_failed": "No se pudo respaldar la aplicación «{app:s}»", - "backup_archive_app_not_found": "No se pudo encontrar la aplicación «{app:s}» en el archivo de respaldo", + "backup_app_failed": "No se pudo respaldar «{app:s}»", + "backup_archive_app_not_found": "No se pudo encontrar «{app:s}» en el archivo de respaldo", "backup_archive_name_exists": "Ya existe un archivo de respaldo con este nombre.", "backup_archive_name_unknown": "Copia de seguridad local desconocida '{name:s}'", "backup_archive_open_failed": "No se pudo abrir el archivo de respaldo", @@ -44,7 +44,7 @@ "backup_output_directory_forbidden": "Elija un directorio de salida diferente. Las copias de seguridad no se pueden crear en /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var o /home/yunohost.backup/archives subcarpetas", "backup_output_directory_not_empty": "Debe elegir un directorio de salida vacío", "backup_output_directory_required": "Debe proporcionar un directorio de salida para la copia de seguridad", - "backup_running_hooks": "Ejecutando los hooks de copia de seguridad...", + "backup_running_hooks": "Ejecutando los hooks de copia de respaldo...", "custom_app_url_required": "Debe proporcionar una URL para actualizar su aplicación personalizada {app:s}", "domain_cert_gen_failed": "No se pudo generar el certificado", "domain_created": "Dominio creado", @@ -54,7 +54,7 @@ "domain_dyndns_already_subscribed": "Ya se ha suscrito a un dominio de 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": "Estas aplicaciones están todavía instaladas en tu dominio:\n{apps}\n\nPor favor desinstálalas utilizando yunohost app remove the_app_id o cambialas a otro dominio usando yunohost app change-url the_app_id antes de continuar con el borrado del dominio.", "domain_unknown": "Dominio desconocido", "done": "Hecho.", "downloading": "Descargando…", @@ -168,9 +168,9 @@ "certmanager_certificate_fetching_or_enabling_failed": "El intento de usar el nuevo certificado para {domain:s} no ha funcionado…", "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! (Puede usar --force si sabe lo que está haciendo)", - "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_domain_http_not_working": "Parece que no se puede acceder al dominio {domain:s} a través de HTTP. Por favor compruebe en los diagnósticos la categoría 'Web'para más información. (Si sabe lo que está haciendo, utilice '--no-checks' para no realizar estas comprobaciones.)", "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 de 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 (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)", + "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. Por favor comprueba los 'registros DNS' (básicos) la categoría de diagnósticos para mayor información. Si recientemente modificó su registro 'A', espere a que se propague (algunos verificadores de propagación de DNS están disponibles en línea). (Si sabe lo que está haciendo, use '--no-checks' para desactivar esos cheques)", "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": "Instalado correctamente un certificado autofirmado para el dominio «{domain:s}»", "certmanager_cert_install_success": "Instalado correctamente un certificado de Let's Encrypt para el dominio «{domain:s}»", @@ -184,7 +184,7 @@ "certmanager_unable_to_parse_self_CA_name": "No se pudo procesar el nombre de la autoridad de autofirma (archivo: {file:s})", "domains_available": "Dominios disponibles:", "backup_archive_broken_link": "No se pudo acceder al archivo de respaldo (enlace roto a {path:s})", - "certmanager_acme_not_configured_for_domain": "El certificado para el dominio «{domain:s}» no parece que esté instalado correctamente. Ejecute primero «cert-install» para este dominio.", + "certmanager_acme_not_configured_for_domain": "El reto ACME no ha podido ser realizado para {domain} porque su configuración de nginx no tiene el el código correcto... Por favor, asegurate que la configuración de nginx es correcta ejecutando en el terminal `yunohost tools regen-conf nginx --dry-run --with-diff`.", "certmanager_http_check_timeout": "Tiempo de espera agotado cuando el servidor intentaba conectarse consigo mismo a través de HTTP usando una dirección IP pública (dominio «{domain:s}» con IP «{ip:s}»). Puede que esté experimentando un problema de redirección («hairpinning»), o que el cortafuegos o el enrutador de su servidor esté mal configurado.", "certmanager_couldnt_fetch_intermediate_cert": "Tiempo de espera agotado intentando obtener el certificado intermedio de Let's Encrypt. Cancelada la instalación o renovación del certificado. Vuelva a intentarlo más tarde.", "domain_hostname_failed": "No se pudo establecer un nuevo nombre de anfitrión («hostname»). Esto podría causar problemas más tarde (no es seguro... podría ir bien).", @@ -197,16 +197,16 @@ "app_location_unavailable": "Este URL o no está disponible o está en conflicto con otra(s) aplicación(es) instalada(s):\n{apps:s}", "app_already_up_to_date": "La aplicación {app:s} ya está actualizada", "app_upgrade_some_app_failed": "No se pudieron actualizar algunas aplicaciones", - "app_make_default_location_already_used": "No puede hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por otra aplicación «{other_app}»", - "app_upgrade_app_name": "Actualizando ahora {app}…", + "app_make_default_location_already_used": "No pudo hacer que la aplicación «{app}» sea la predeterminada en el dominio, «{domain}» ya está siendo usado por la aplicación «{other_app}»", + "app_upgrade_app_name": "Ahora actualizando {app}…", "backup_abstract_method": "Este método de respaldo aún no se ha implementado", "backup_applying_method_borg": "Enviando todos los archivos para la copia de seguridad al repositorio de borg-backup…", - "backup_applying_method_copy": "Copiando todos los archivos a la copia de seguridad…", + "backup_applying_method_copy": "Copiando todos los archivos en la copia de respaldo…", "backup_applying_method_custom": "Llamando al método de copia de seguridad personalizado «{method:s}»…", "backup_applying_method_tar": "Creando el archivo TAR de respaldo…", "backup_archive_system_part_not_available": "La parte del sistema «{part:s}» no está disponible en esta copia de seguridad", "backup_archive_writing_error": "No se pudieron añadir los archivos «{source:s}» (llamados en el archivo «{dest:s}») para ser respaldados en el archivo comprimido «{archive:s}»", - "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s} MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)", + "backup_ask_for_copying_if_needed": "¿Quiere realizar la copia de seguridad usando {size:s}MB temporalmente? (Se usa este modo ya que algunos archivos no se pudieron preparar usando un método más eficiente.)", "backup_borg_not_implemented": "El método de respaldo de Borg aún no ha sido implementado", "backup_cant_mount_uncompress_archive": "No se pudo montar el archivo descomprimido como protegido contra escritura", "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo", @@ -218,7 +218,7 @@ "backup_php5_to_php7_migration_may_fail": "No se pudo convertir su archivo para que sea compatible con PHP 7, puede que no pueda restaurar sus aplicaciones de PHP (motivo: {error:s})", "backup_system_part_failed": "No se pudo respaldar la parte del sistema «{part:s}»", "backup_with_no_backup_script_for_app": "La aplicación «{app:s}» no tiene un guión de respaldo. Omitiendo.", - "backup_with_no_restore_script_for_app": "La aplicación «{app:s}» no tiene un guión de restauración, no podrá restaurar automáticamente la copia de seguridad de esta aplicación.", + "backup_with_no_restore_script_for_app": "«{app:s}» no tiene un script de restauración, no podá restaurar automáticamente la copia de seguridad de esta aplicación.", "dyndns_could_not_check_provide": "No se pudo verificar si {provider:s} puede ofrecer {domain:s}.", "dyndns_domain_not_provided": "El proveedor de DynDNS {provider:s} no puede proporcionar el dominio {domain:s}.", "experimental_feature": "Aviso : esta funcionalidad es experimental y no se considera estable, no debería usarla a menos que sepa lo que está haciendo.", @@ -446,14 +446,14 @@ "dyndns_could_not_check_available": "No se pudo comprobar si {domain:s} está disponible en {provider:s}.", "domain_dns_conf_is_just_a_recommendation": "Esta orden muestra la configuración *recomendada*. No configura el DNS en realidad. Es su responsabilidad configurar la zona de DNS en su registrador según esta recomendación.", "dpkg_lock_not_available": "Esta orden no se puede ejecutar en este momento ,parece que programa está usando el bloqueo de dpkg (el gestor de paquetes del sistema)", - "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/apt (los gestores de paquetes del sistema) parecen estar en un estado roto... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo dpkg --configure -a`.", + "dpkg_is_broken": "No puede hacer esto en este momento porque dpkg/APT (los gestores de paquetes del sistema) parecen estar mal configurados... Puede tratar de solucionar este problema conectando a través de SSH y ejecutando `sudo apt install --fix-broken` y/o `sudo dpkg --configure -a`.", "confirm_app_install_thirdparty": "¡PELIGRO! Esta aplicación no forma parte del catálogo de aplicaciones de Yunohost. La instalación de aplicaciones de terceros puede comprometer la integridad y la seguridad de su sistema. Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'", "confirm_app_install_danger": "¡PELIGRO! ¡Se sabe que esta aplicación sigue siendo experimental (si no explícitamente no funciona)! Probablemente NO debería instalarlo a menos que sepa lo que está haciendo. NO se proporcionará SOPORTE si esta aplicación no funciona o rompe su sistema ... Si de todos modos está dispuesto a correr ese riesgo, escriba '{answers:s}'", "confirm_app_install_warning": "Aviso: esta aplicación puede funcionar pero no está bien integrada en YunoHost. Algunas herramientas como la autentificación única y respaldo/restauración podrían no estar disponibles. ¿Instalar de todos modos? [{answers:s}] ", "backup_unable_to_organize_files": "No se pudo usar el método rápido de organización de los archivos en el archivo", - "backup_permission": "Permiso de respaldo para la aplicación {app:s}", + "backup_permission": "Permiso de respaldo para {app:s}", "backup_output_symlink_dir_broken": "El directorio de su archivo «{path:s}» es un enlace simbólico roto. Tal vez olvidó (re)montarlo o conectarlo al medio de almacenamiento al que apunta.", - "backup_mount_archive_for_restore": "Preparando el archivo para la restauración…", + "backup_mount_archive_for_restore": "Preparando el archivo para restaurarlo…", "backup_method_tar_finished": "Creado el archivo TAR de respaldo", "backup_method_custom_finished": "Terminado el método «{method:s}» de respaldo personalizado", "backup_method_copy_finished": "Terminada la copia de seguridad", @@ -463,10 +463,10 @@ "ask_new_path": "Nueva ruta", "ask_new_domain": "Nuevo dominio", "app_upgrade_several_apps": "Las siguientes aplicaciones se actualizarán: {apps}", - "app_start_restore": "Restaurando aplicación «{app}»…", + "app_start_restore": "Restaurando «{app}»…", "app_start_backup": "Obteniendo archivos para el respaldo de «{app}»…", - "app_start_remove": "Eliminando aplicación «{app}»…", - "app_start_install": "Instalando aplicación «{app}»…", + "app_start_remove": "Eliminando «{app}»…", + "app_start_install": "Instalando «{app}»…", "app_not_upgraded": "La aplicación '{failed_app}' no se pudo actualizar y, como consecuencia, se cancelaron las actualizaciones de las siguientes aplicaciones: {apps}", "app_action_cannot_be_ran_because_required_services_down": "Estos servicios necesarios deberían estar funcionando para ejecutar esta acción: {services}. Pruebe a reiniciarlos para continuar (y posiblemente investigar por qué están caídos).", "already_up_to_date": "Nada que hacer. Todo está actualizado.", @@ -509,7 +509,7 @@ "diagnosis_basesystem_ynh_main_version": "El servidor está ejecutando YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_inconsistent_versions": "Está ejecutando versiones inconsistentes de los paquetes de YunoHost ... probablemente debido a una actualización parcial o fallida.", "diagnosis_failed_for_category": "Error de diagnóstico para la categoría '{category}': {error}", - "diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡Aún no se ha rediagnosticado!)", + "diagnosis_cache_still_valid": "(Caché aún válida para el diagnóstico de {category}. ¡No se volvera a comprobar de momento!)", "diagnosis_found_errors_and_warnings": "¡Encontrado(s) error(es) significativo(s) {errors} (y aviso(s) {warnings}) relacionado(s) con {category}!", "diagnosis_display_tip_web": "Puede ir a la sección de diagnóstico (en la pantalla principal) para ver los problemas encontrados.", "diagnosis_display_tip_cli": "Puede ejecutar «yunohost diagnosis show --issues» para mostrar los problemas encontrados.", @@ -527,7 +527,7 @@ "diagnosis_no_cache": "Todavía no hay una caché de diagnóstico para la categoría '{category}'", "diagnosis_ip_no_ipv4": "El servidor no cuenta con ipv4 funcional.", "diagnosis_ip_not_connected_at_all": "¿¡Está conectado el servidor a internet!?", - "diagnosis_ip_broken_resolvconf": "DNS parece no funcionar en tu servidor, lo que parece estar relacionado con /etc/resolv.conf no apuntando a 127.0.0.1.", + "diagnosis_ip_broken_resolvconf": "La resolución de nombres de dominio parece no funcionar en tu servidor, lo que parece estar relacionado con que /etc/resolv.conf no apunta a 127.0.0.1.", "diagnosis_dns_missing_record": "Según la configuración DNS recomendada, deberías añadir un registro DNS\ntipo: {type}\nnombre: {name}\nvalor: {value}", "diagnosis_diskusage_low": "El almacenamiento {mountpoint} (en dispositivo {device}) solo tiene {free} ({free_percent}%) de espacio disponible. Ten cuidado.", "diagnosis_services_bad_status_tip": "Puedes intentar reiniciar el servicio, y si no funciona, echar un vistazo a los logs del servicio usando 'yunohost service log {service}' o a través de la sección 'Servicios' en webadmin.", @@ -535,11 +535,11 @@ "diagnosis_ip_no_ipv6": "El servidor no cuenta con IPv6 funcional.", "diagnosis_ip_dnsresolution_working": "¡DNS no está funcionando!", "diagnosis_ip_broken_dnsresolution": "Parece que no funciona la resolución de nombre de dominio por alguna razón... ¿Hay algún firewall bloqueando peticiones DNS?", - "diagnosis_ip_weird_resolvconf": "Parece que DNS funciona, pero ten cuidado, porque estás utilizando /etc/resolv.conf modificado.", - "diagnosis_ip_weird_resolvconf_details": "En su lugar, este fichero debería ser un enlace simbólico a /etc/resolvconf/run/resolv.conf apuntando a 127.0.0.1 (dnsmasq). Los servidores de nombre de domino deben configurarse a través de /etc/resolv.dnsmasq.conf.", - "diagnosis_dns_good_conf": "Buena configuración DNS para el dominio {domain} (categoría {category})", - "diagnosis_dns_bad_conf": "Configuración mala o faltante de los DNS para el dominio {domain} (categoría {category})", - "diagnosis_dns_discrepancy": "El registro DNS con tipo {type} y nombre {name} no se corresponde a la configuración recomendada.\nValor actual: {current}\nValor esperado: {value}", + "diagnosis_ip_weird_resolvconf": "La resolución de nombres de dominio DNS funciona, aunque parece que estás utilizando /etc/resolv.conf personalizada.", + "diagnosis_ip_weird_resolvconf_details": "El fichero /etc/resolv.conf debería ser un enlace simbólico a /etc/resolvconf/run/resolv.conf a su vez debe apuntar a 127.0.0.1 (dnsmasq). Si lo que quieres es configurar la resolución DNS manualmente, porfavor modifica /etc/resolv.dnsmasq.conf.", + "diagnosis_dns_good_conf": "La configuración de registros DNS es correcta para {domain} (categoría {category})", + "diagnosis_dns_bad_conf": "Algunos registros DNS faltan o están mal cofigurados para el dominio {domain} (categoría {category})", + "diagnosis_dns_discrepancy": "El siguiente registro DNS parace que no sigue la configuración recomendada
Tipo: {type}
Nombre: {name}
Valor Actual: {current}
Valor esperado: {value}", "diagnosis_services_bad_status": "El servicio {service} está {status} :(", "diagnosis_diskusage_verylow": "El almacenamiento {mountpoint} (en el dispositivo {device}) sólo tiene {free} ({free_percent}%) de espacio disponible. Deberías considerar la posibilidad de limpiar algo de espacio.", "diagnosis_diskusage_ok": "¡El almacenamiento {mountpoint} (en el dispositivo {device}) todavía tiene {free} ({free_percent}%) de espacio libre!", @@ -556,8 +556,8 @@ "diagnosis_mail_ougoing_port_25_ok": "El puerto de salida 25 no esta bloqueado y los correos electrónicos pueden ser enviados a otros servidores.", "diagnosis_mail_outgoing_port_25_blocked": "El puerto de salida 25 parece estar bloqueado. Intenta desbloquearlo con el panel de configuración de tu proveedor de servicios de Internet (o proveedor de halbergue). Mientras tanto, el servidor no podrá enviar correos electrónicos a otros servidores.", "diagnosis_regenconf_allgood": "Todos los archivos de configuración están en linea con la configuración recomendada!", - "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} fue modificado manualmente.", - "diagnosis_regenconf_manually_modified_details": "Esto este probablemente BIEN siempre y cuando sepas lo que estas haciendo ;) !", + "diagnosis_regenconf_manually_modified": "El archivo de configuración {file} parece que ha sido modificado manualmente.", + "diagnosis_regenconf_manually_modified_details": "¡Esto probablemente esta BIEN si sabes lo que estás haciendo! YunoHost dejará de actualizar este fichero automáticamente... Pero ten en cuenta que las actualizaciones de YunoHost pueden contener importantes cambios que están recomendados. Si quieres puedes comprobar las diferencias mediante yunohost tools regen-conf {category} --dry-run --with-diff o puedes forzar el volver a las opciones recomendadas mediante el comando yunohost tools regen-conf {category} --force", "diagnosis_regenconf_manually_modified_debian": "El archivos de configuración {file} fue modificado manualmente comparado con el valor predeterminado de Debian.", "diagnosis_regenconf_manually_modified_debian_details": "Esto este probablemente BIEN, pero igual no lo pierdas de vista...", "diagnosis_security_all_good": "Ninguna vulnerabilidad critica de seguridad fue encontrada.", @@ -586,26 +586,26 @@ "log_app_config_apply": "Aplica la configuración de la aplicación '{}'", "log_app_config_show_panel": "Muestra el panel de configuración de la aplicación '{}'", "log_app_action_run": "Inicializa la acción de la aplicación '{}'", - "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en el grupo de sistema, pero YunoHost lo suprimirá …", + "group_already_exist_on_system_but_removing_it": "El grupo {group} ya existe en los grupos del sistema, pero YunoHost lo suprimirá …", "global_settings_setting_pop3_enabled": "Habilita el protocolo POP3 para el servidor de correo electrónico", "domain_cannot_remove_main_add_new_one": "No se puede remover '{domain:s}' porque es su principal y único dominio. Primero debe agregar un nuevo dominio con la linea de comando 'yunohost domain add ', entonces configurarlo como dominio principal con 'yunohost domain main-domain -n ' y finalmente borrar el dominio '{domain:s}' con 'yunohost domain remove {domain:s}'.'", "diagnosis_never_ran_yet": "Este servidor todavía no tiene reportes de diagnostico. Puede iniciar un diagnostico completo desde la interface administrador web o con la linea de comando 'yunohost diagnosis run'.", "diagnosis_unknown_categories": "Las siguientes categorías están desconocidas: {categories}", "diagnosis_http_unreachable": "El dominio {domain} esta fuera de alcance desde internet y a través de HTTP.", - "diagnosis_http_bad_status_code": "El sistema de diagnostico no pudo comunicarse con su servidor. Puede ser otra maquina que contesto en lugar del servidor. Debería verificar en su firewall que el re-direccionamiento del puerto 80 esta correcto.", + "diagnosis_http_bad_status_code": "Parece que otra máquina (quizás el router de conexión a internet) haya respondido en vez de tu servidor.
1. La causa más común es que el puerto 80 (y el 443) no hayan sido redirigidos a tu servidor.
2. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso están interfiriendo.", "diagnosis_http_connection_error": "Error de conexión: Ne se pudo conectar al dominio solicitado.", - "diagnosis_http_timeout": "El intento de contactar a su servidor desde internet corrió fuera de tiempo. Al parece esta incomunicado. Debería verificar que nginx corre en el puerto 80, y que la redireción del puerto 80 no interfiere con en el firewall.", + "diagnosis_http_timeout": "Tiempo de espera agotado al intentar contactar tu servidor desde el exterior. Parece que no sea alcanzable.
1. La causa más común es que el puerto 80 (y el 443) no estén correctamente redirigidos a tu servidor.
2. Deberías asegurarte que el servicio nginx está en marcha.
3. En situaciones más complejas: asegurate de que ni el cortafuegos ni el proxy inverso estén interfiriendo.", "diagnosis_http_ok": "El Dominio {domain} es accesible desde internet a través de HTTP.", "diagnosis_http_could_not_diagnose": "No se pudo verificar si el dominio es accesible desde internet.", "diagnosis_http_could_not_diagnose_details": "Error: {error}", - "diagnosis_ports_forwarding_tip": "Para solucionar este incidente, debería configurar el \"port forwading\" en su router como especificado en https://yunohost.org/isp_box_config", + "diagnosis_ports_forwarding_tip": "Para solucionar este incidente, lo más seguro deberías configurar la redirección de los puertos en el router como se especifica en https://yunohost.org/isp_box_config", "certmanager_warning_subdomain_dns_record": "El subdominio '{subdomain:s}' no se resuelve en la misma dirección IP que '{domain:s}'. Algunas funciones no estarán disponibles hasta que solucione esto y regenere el certificado.", "domain_cannot_add_xmpp_upload": "No puede agregar dominios que comiencen con 'xmpp-upload'. Este tipo de nombre está reservado para la función de carga XMPP integrada en YunoHost.", "yunohost_postinstall_end_tip": "¡La post-instalación completada! Para finalizar su configuración, considere:\n - agregar un primer usuario a través de la sección 'Usuarios' del webadmin (o 'yunohost user create ' en la línea de comandos);\n - diagnostique problemas potenciales a través de la sección 'Diagnóstico' de webadmin (o 'ejecución de diagnóstico yunohost' en la línea de comandos);\n - leyendo las partes 'Finalizando su configuración' y 'Conociendo a Yunohost' en la documentación del administrador: https://yunohost.org/admindoc.", "diagnosis_dns_point_to_doc": "Por favor, consulta la documentación en https://yunohost.org/dns_config si necesitas ayuda para configurar los registros DNS.", "diagnosis_ip_global": "IP Global: {global}", "diagnosis_mail_outgoing_port_25_ok": "El servidor de email SMTP puede mandar emails (puerto saliente 25 no está bloqueado).", - "diagnosis_mail_outgoing_port_25_blocked_details": "Deberías intentar desbloquear el puerto 25 saliente en la interfaz de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).", + "diagnosis_mail_outgoing_port_25_blocked_details": "Primeramente deberías intentar desbloquear el puerto de salida 25 en la interfaz de control de tu router o en la interfaz de tu provedor de hosting. (Algunos hosting pueden necesitar que les abras un ticket de soporte para esto).", "diagnosis_swap_tip": "Por favor tenga cuidado y sepa que si el servidor contiene swap en una tarjeta SD o un disco duro de estado sólido, esto reducirá drásticamente la vida útil del dispositivo.", "diagnosis_domain_expires_in": "{domain} expira en {days} días.", "diagnosis_domain_expiration_error": "¡Algunos dominios expirarán MUY PRONTO!", @@ -631,5 +631,62 @@ "app_manifest_install_ask_path": "Seleccione el path donde esta aplicación debería ser instalada", "app_manifest_install_ask_domain": "Seleccione el dominio donde esta app debería ser instalada", "app_label_deprecated": "Este comando está depreciado! Favor usar el nuevo comando 'yunohost user permission update' para administrar la etiqueta de app.", - "app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad" + "app_argument_password_no_default": "Error al interpretar argumento de contraseña'{name}': El argumento de contraseña no puede tener un valor por defecto por razón de seguridad", + "migration_0015_not_enough_free_space": "¡El espacio es muy bajo en `/var/`! Deberías tener almenos 1Gb de espacio libre para ejecutar la migración.", + "migration_0015_not_stretch": "¡La distribución actual de Debian no es Stretch!", + "migration_0015_yunohost_upgrade": "Iniciando la actualización del núcleo de YunoHost...", + "migration_0015_still_on_stretch_after_main_upgrade": "Algo fue mal durante la actualización principal, el sistema parece que está todavía en Debian Stretch", + "migration_0015_main_upgrade": "Comenzando la actualización principal...", + "migration_0015_patching_sources_list": "Adaptando las sources.lists...", + "migration_0015_start": "Comenzando la migración a Buster", + "migration_description_0019_extend_permissions_features": "Extiende/rehaz el sistema de gestión de permisos de la aplicación", + "migration_description_0018_xtable_to_nftable": "Migra las viejas reglas de tráfico de red al nuevo sistema nftable", + "migration_description_0017_postgresql_9p6_to_11": "Migra las bases de datos de PostgreSQL 9.6 a 11", + "migration_description_0016_php70_to_php73_pools": "Migra el «pool» de ficheros php7.0-fpm a php7.3", + "migration_description_0015_migrate_to_buster": "Actualiza el sistema a Debian Buster y YunoHost 4.x", + "migrating_legacy_permission_settings": "Migrando los antiguos parámetros de permisos...", + "invalid_regex": "Regex no valido: «{regex:s}»", + "global_settings_setting_backup_compress_tar_archives": "Cuando se creen nuevas copias de respaldo, comprimir los archivos (.tar.gz) en lugar de descomprimir los archivos (.tar). N.B.: activar esta opción quiere decir que los archivos serán más pequeños pero que el proceso tardará más y utilizará más CPU.", + "global_settings_setting_smtp_relay_password": "Clave de uso del SMTP", + "global_settings_setting_smtp_relay_user": "Cuenta de uso de SMTP", + "global_settings_setting_smtp_relay_port": "Puerto de envio / relay SMTP", + "global_settings_setting_smtp_relay_host": "El servidor relay de SMTP para enviar correo en lugar de esta instalación YunoHost. Útil si estás en una de estas situaciones: tu puerto 25 esta bloqueado por tu ISP o VPS, si estás en usado una IP marcada como residencial o DUHL, si no puedes configurar un DNS inverso o si el servidor no está directamente expuesto a internet y quieres utilizar otro servidor para enviar correos.", + "global_settings_setting_smtp_allow_ipv6": "Permitir el uso de IPv6 para enviar y recibir correo", + "domain_name_unknown": "Dominio «{domain}» desconocido", + "diagnosis_processes_killed_by_oom_reaper": "Algunos procesos fueron terminados por el sistema recientemente porque se quedó sin memoria. Típicamente es sintoma de falta de memoria o de un proceso que se adjudicó demasiada memoria.
Resumen de los procesos terminados:
\n{kills_summary}", + "diagnosis_http_nginx_conf_not_up_to_date_details": "Para arreglar este asunto, estudia las diferencias mediante el comando yunohost tools regen-conf nginx --dry-run --with-diff y si te parecen bien aplica los cambios mediante yunohost tools regen-conf nginx --force.", + "diagnosis_http_nginx_conf_not_up_to_date": "Parece que la configuración nginx de este dominio haya sido modificada manualmente, esto no deja que YunoHost pueda diagnosticar si es accesible mediante HTTP.", + "diagnosis_http_partially_unreachable": "El dominio {domain} parece que no es accesible mediante HTTP desde fuera de la red local mediante IPv{failed}, aunque si que funciona mediante IPv{passed}.", + "diagnosis_http_hairpinning_issue_details": "Esto quizás es debido a tu router o máquina en el ISP. Como resultado, la gente fuera de tu red local podrá acceder a tu servidor como es de esperar, pero no así las persona que estén dentro de la red local (como tu probablemente) o cuando usen el nombre de dominio o la IP global. Quizás puedes mejorar o arreglar esta situación leyendo https://yunohost.org/dns_local_network", + "diagnosis_http_hairpinning_issue": "Parece que tu red local no tiene la opción hairpinning activada.", + "diagnosis_ports_partially_unreachable": "El port {port} no es accesible desde el exterior mediante IPv{failed}.", + "diagnosis_mail_queue_too_big": "Demasiados correos electrónicos pendientes en la cola ({nb_pending} correos electrónicos)", + "diagnosis_mail_queue_unavailable_details": "Error: {error}", + "diagnosis_mail_queue_unavailable": "No se ha podido consultar el número de correos electrónicos pendientes en la cola", + "diagnosis_mail_queue_ok": "{nb_pending} correos esperando e la cola de correos electrónicos", + "diagnosis_mail_blacklist_website": "Cuando averigües y arregles el motivo por el que aprareces en la lista maligna, no dudes en solicitar que tu IP o dominio sea retirado de la {blacklist_website}", + "diagnosis_mail_blacklist_reason": "El motivo de estar en la lista maligna es: {reason}", + "diagnosis_mail_blacklist_listed_by": "Tu IP o dominio {item} está marcado como maligno en {blacklist_name}", + "diagnosis_mail_blacklist_ok": "Las IP y los dominios utilizados en este servidor no parece que estén en ningún listado maligno (blacklist)", + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "El DNS inverso actual es: {rdns_domain}
Valor esperado: {ehlo_domain}", + "diagnosis_mail_fcrdns_different_from_ehlo_domain": "La resolución de DNS inverso no está correctamente configurada mediante IPv{ipversion}. Algunos correos pueden fallar al ser enviados o pueden ser marcados como basura.", + "diagnosis_mail_fcrdns_nok_alternatives_6": "Algunos proveedores no permiten configurar el DNS inverso (o su funcionalidad puede estar rota...). Si tu DNS inverso está configurado correctamente para IPv4, puedes intentar deshabilitarlo para IPv6 cuando envies correos mediante el comando yunohost settings set smtp.allow_ipv6 -v off. Nota: esta solución quiere decir que no podrás enviar ni recibir correos con los pocos servidores que utilizan exclusivamente IPv6.", + "diagnosis_mail_fcrdns_nok_alternatives_4": "Algunos proveedores no te permitirán que configures un DNS inverso (o puede que esta opción esté rota...). Si estás sufriendo problemas por este asunto, quizás te sirvan las siguientes soluciones:
- Algunos ISP proporcionan una alternativa mediante el uso de un relay de servidor de correo aunque esto implica que el relay podrá espiar tu tráfico de correo electrónico.
- Una solución amigable con la privacidad es utilizar una VPN con una *IP pública dedicada* para evitar este tipo de limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Quizás tu solución sea cambiar de proveedor de internet", + "diagnosis_mail_fcrdns_nok_details": "Primero deberías intentar configurar el DNS inverso mediante {ehlo_domain} en la interfaz de internet de tu router o en la de tu proveedor de internet. (Algunos proveedores de internet en ocasiones necesitan que les solicites un ticket de soporte para ello).", + "diagnosis_mail_fcrdns_dns_missing": "No hay definida ninguna DNS inversa mediante IPv{ipversion}. Algunos correos puede que fallen al enviarse o puede que se marquen como basura.", + "diagnosis_mail_fcrdns_ok": "¡Las DNS inversas están bien configuradas!", + "diagnosis_mail_ehlo_could_not_diagnose_details": "Error: {error}", + "diagnosis_mail_ehlo_could_not_diagnose": "No pudimos diagnosticar si el servidor de correo postfix es accesible desde el exterior utilizando IPv{ipversion}.", + "diagnosis_mail_ehlo_wrong_details": "El EHLO recibido por el diagnosticador remoto de IPv{ipversion} es diferente del dominio de tu servidor.
EHLO recibido: {wrong_ehlo}
EHLO esperado: {right_ehlo}
La causa más común de este error suele ser que el puerto 25 no está correctamente enrutado hacia tu servidor. Así mismo asegurate que ningún firewall ni reverse-proxy está interfiriendo.", + "diagnosis_mail_ehlo_wrong": "Un servidor diferente de SMTP está respondiendo mediante IPv{ipversion}. Es probable que tu servidor no pueda recibir correos.", + "diagnosis_mail_ehlo_bad_answer_details": "Podría ser debido a otra máquina en lugar de tu servidor.", + "diagnosis_mail_ehlo_bad_answer": "Un servicio que no es SMTP respondió en el puerto 25 mediante IPv{ipversion}", + "diagnosis_mail_ehlo_unreachable_details": "No pudo abrirse la conexión en el puerto 25 de tu servidor mediante IPv{ipversion}. Parece que no se puede contactar.
1. La causa más común en estos casos suele ser que el puerto 25 no está correctamente redireccionado a tu servidor.
2. También deberías asegurarte que el servicio postfix está en marcha.
3. En casos más complejos: asegurate que no estén interfiriendo ni el firewall ni el reverse-proxy.", + "diagnosis_mail_ehlo_unreachable": "El servidor de correo SMTP no puede contactarse desde el exterior mediante IPv{ipversion}. No puede recibir correos", + "diagnosis_mail_ehlo_ok": "¡El servidor de correo SMTP puede contactarse desde el exterior por lo que puede recibir correos!", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Algunos proveedores de internet no le permitirán desbloquear el puerto 25 porque no les importa la Neutralidad de la Red.
- Algunos proporcionan una alternativa usando un relay como servidor de correo lo que implica que el relay podrá espiar tu trafico de correo.
- Una alternativa buena para la privacidad es utilizar una VPN *con una IP pública dedicada* para evitar estas limitaciones. Mira en https://yunohost.org/#/vpn_advantage
- Otra alternativa es cambiar de proveedor de inteernet a uno más amable con la Neutralidad de la Red", + "diagnosis_backports_in_sources_list": "Parece que apt (el gestor de paquetes) está configurado para usar el repositorio backports. A menos que realmente sepas lo que estás haciendo, desaconsejamos absolutamente instalar paquetes desde backports, ya que pueden provocar comportamientos intestables o conflictos en el sistema.", + "diagnosis_basesystem_hardware_model": "El modelo de servidor es {model}", + "additional_urls_already_removed": "La URL adicional «{url:s}» ya se ha eliminado para el permiso «{permission:s}»", + "additional_urls_already_added": "La URL adicional «{url:s}» ya se ha añadido para el permiso «{permission:s}»" } From d71792e43950f01c122768c7bb09e1944f5c4de8 Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Fri, 8 Jan 2021 13:18:18 +0000 Subject: [PATCH 141/363] Translated using Weblate (Italian) Currently translated at 100.0% (631 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/it.json b/locales/it.json index 0a30b5790..9b5d18f1a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -69,7 +69,7 @@ "domain_dyndns_already_subscribed": "Hai già sottoscritto un dominio DynDNS", "domain_dyndns_root_unknown": "Dominio radice DynDNS sconosciuto", "domain_hostname_failed": "Impossibile impostare il nuovo hostname. Potrebbe causare problemi in futuro (o anche no).", - "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio: {apps}. Disinstallale prima di procedere alla cancellazione di un dominio", + "domain_uninstall_app_first": "Queste applicazioni sono già installate su questo dominio:\n{apps}\n\nDisinstallale eseguendo 'yunohost app remove app_id' o spostale in un altro dominio eseguendo 'yunohost app change-url app_id' prima di procedere alla cancellazione del dominio", "domain_unknown": "Dominio sconosciuto", "done": "Terminato", "domains_available": "Domini disponibili:", @@ -672,5 +672,7 @@ "diagnosis_mail_queue_ok": "{nb_pending} emails in attesa nelle code", "diagnosis_mail_blacklist_website": "Dopo aver identificato il motivo e averlo risolto, sentiti libero di chiedere di rimuovere il tuo IP o dominio da {blacklist_website}", "diagnosis_mail_blacklist_reason": "Il motivo della blacklist è: {reason}", - "diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio {item} è nella blacklist {blacklist_name}" + "diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio {item} è nella blacklist {blacklist_name}", + "diagnosis_backports_in_sources_list": "Sembra che apt (il package manager) sia configurato per utilizzare le backport del repository. A meno che tu non sappia quello che stai facendo, scoraggiamo fortemente di installare pacchetti tramite esse, perché ci sono alte probabilità di creare conflitti con il tuo sistema.", + "diagnosis_basesystem_hardware_model": "Modello server: {model}" } From 2397820e3157fc36fc81e7ab49889a0d232026b6 Mon Sep 17 00:00:00 2001 From: Yobar Date: Thu, 14 Jan 2021 14:11:44 +0000 Subject: [PATCH 142/363] Translated using Weblate (French) Currently translated at 99.8% (630 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 83ad72612..31c65d1cd 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -687,10 +687,10 @@ "invalid_regex": "Regex non valide : '{regex:s}'", "domain_name_unknown": "Domaine '{domain}' inconnu", "app_label_deprecated": "Cette commande est obsolète ! Veuillez utiliser la nouvelle commande 'yunohost user permission update' pour gérer l'étiquette de l'application.", - "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimée pour la permission '{permission:s}'", + "additional_urls_already_removed": "URL supplémentaire '{url:s}' déjà supprimées pour la permission '{permission:s}'", "migration_0019_rollback_success": "Retour à l'état antérieur du système.", "invalid_number": "Doit être un nombre", "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", - "diagnosis_basesystem_hardware_model": "Le modèle du serveur est '{model}'.", + "diagnosis_basesystem_hardware_model": "Le modèle du serveur est {model}", "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système." } From c4004b701e7e1eae842ee9d0592fa31cbfc874c5 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 14 Jan 2021 21:40:59 +0100 Subject: [PATCH 143/363] Update changelog for 4.1.5 --- debian/changelog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/debian/changelog b/debian/changelog index df242d76f..a6ed39311 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +yunohost (4.1.5) stable; urgency=low + + - [fix] Update helpers ([#1136](https://github.com/yunohost/yunohost/pull/11346)) + - [fix] Certificate during regen conf on some setup (1d2b1d9) + - [fix] Empty password is not an error if it's optional ([#1135](https://github.com/yunohost/yunohost/pull/11345)) + - [fix] Remove useless warnings during system backup ([#1138](https://github.com/yunohost/yunohost/pull/11348)) + - [fix] We can now use "true" or "false" for a boolean ([#1134](https://github.com/yunohost/yunohost/pull/1134)) + - [i18n] Translations updated for Catalan, French, Italian, Spanish + + Thanks to all contributors <3 ! (Aleks, Kay0u, Omnia89, jorge-vitrubio, YohannEpitech, xaloc33) + + + -- Kayou Thu, 14 Jan 2021 21:23:39 +0100 + yunohost (4.1.4.4) stable; urgency=low - [fix] Add the -F flag to grep command for fixed string mode, prevent special chars in the password to be interpreted as regex pattern ([#1132](https://github.com/yunohost/yunohost/pull/1132)) From 7dad37ed04ec7e85b0a64ee22629528e892628e7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 18 Jan 2021 19:57:31 +0100 Subject: [PATCH 144/363] Make dyndns update more resilient to ns0.yunohost.org being down --- src/yunohost/dyndns.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index fe2a1bc9b..98d59721a 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -34,7 +34,7 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, read_file -from moulinette.utils.network import download_json +from moulinette.utils.network import download_json, dig from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError @@ -216,8 +216,30 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, 'zone %s' % host, ] - old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)) or None - old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)) or None + + def resolve_domain(domain, rdtype): + + ok, result = dig(domain, rdtype, resolvers=[dyn_host]) + if ok == "ok": + return result[0] if len(result) else None + elif result[0] == "Timeout": + logger.debug("Timed-out while trying to resolve %s record for %s using %s" % (rdtype, domain, dyn_host)) + else: + return None + + logger.debug("Falling back to external resolvers") + ok, result = dig(domain, rdtype, resolvers="force_external") + if ok == "ok": + return result[0] if len(result) else None + elif result[0] == "Timeout": + logger.debug("Timed-out while trying to resolve %s record for %s using external resolvers : %s" % (rdtype, domain, result)) + else: + return None + + raise YunohostError("Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True) + + old_ipv4 = resolve_domain(domain, "A") + old_ipv6 = resolve_domain(domain, "AAAA") # Get current IPv4 and IPv6 ipv4_ = get_public_ip() From d5403e5f5f3158fb51f741d6a49b13c3a9641ddc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 18 Jan 2021 20:25:36 +0100 Subject: [PATCH 145/363] Typo --- 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 98d59721a..eb03b056e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -34,12 +34,12 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, read_file -from moulinette.utils.network import download_json, dig +from moulinette.utils.network import download_json from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError from yunohost.domain import _get_maindomain, _build_dns_conf -from yunohost.utils.network import get_public_ip +from yunohost.utils.network import get_public_ip, dig from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.dyndns') From e7300859704c9f815d79f7b1ac63cb813d52120b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 18 Jan 2021 20:53:38 +0100 Subject: [PATCH 146/363] Linter is angry about unused import --- src/yunohost/dyndns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index eb03b056e..84b55d40e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -35,7 +35,6 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, read_file from moulinette.utils.network import download_json -from moulinette.utils.process import check_output from yunohost.utils.error import YunohostError from yunohost.domain import _get_maindomain, _build_dns_conf From 8b05248ad45bfe05ae9205e976798a4e722eaee9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 19 Jan 2021 02:34:28 +0100 Subject: [PATCH 147/363] Turns out we need to feed the IP of the resolver and can't use the doman name :/ --- src/yunohost/dyndns.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 84b55d40e..9904d3955 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -218,7 +218,13 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, def resolve_domain(domain, rdtype): - ok, result = dig(domain, rdtype, resolvers=[dyn_host]) + # FIXME make this work for IPv6-only hosts too.. + ok, result = dig(dyn_host, "A") + dyn_host_ip = result[0] if ok == "ok" and len(result) else None + if not dyn_host_ip: + raise YunohostError("Failed to resolve %s" % dyn_host) + + ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip]) if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": From 5c7e5033167cbf1f77175ec8cc73d4a15b0d60fb Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 19 Jan 2021 11:20:07 +0100 Subject: [PATCH 148/363] Typo --- 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 24121df30..5e02ca762 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -650,7 +650,7 @@ ynh_check_app_version_changed () { # # Generally you might probably use it as follow in the upgrade script # -# if ynh_compare_current_package_version --comparaison lt --version 2.3.2~ynh1 +# if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 # then # # Do something that is needed for the package version older than 2.3.2~ynh1 # fi From 754e779296595fc5bb727ae91bd2c8f19a7fd3fe Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 15 Jan 2021 19:54:39 +0000 Subject: [PATCH 149/363] Translated using Weblate (German) Currently translated at 59.2% (374 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/locales/de.json b/locales/de.json index 7811d3677..52f5041bb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -41,14 +41,14 @@ "backup_running_hooks": "Datensicherunghook wird ausgeführt...", "custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", - "domain_created": "Die Domain wurde angelegt", - "domain_creation_failed": "Konnte Domain nicht erzeugen", + "domain_created": "Domäne erstellt", + "domain_creation_failed": "Konnte Domäne nicht erzeugen", "domain_deleted": "Domain wurde gelöscht", "domain_deletion_failed": "Domain {domain}: {error} konnte nicht gelöscht werden", - "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domain angemeldet", + "domain_dyndns_already_subscribed": "Sie haben sich schon für eine DynDNS-Domäne registriert", "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_exists": "Die Domäne existiert bereits", + "domain_uninstall_app_first": "Diese Apps sind noch auf Ihrer Domäne installiert; \n{apps}\n\nBitte deinstallieren Sie sie mit dem Befehl 'yunohost app remove the_app_id' oder verschieben Sie sie mit 'yunohost app change-url the_app_id'", "domain_unknown": "Unbekannte Domain", "done": "Erledigt", "downloading": "Wird heruntergeladen…", @@ -187,7 +187,7 @@ "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file:s})", "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.", - "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen", + "domain_hostname_failed": "Sie können keinen neuen Hostnamen verwenden. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", "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_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}", @@ -475,5 +475,6 @@ "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", - "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}" + "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", + "domain_name_unknown": "Domäne '{domain}' unbekannt" } From cfd80a94785793d145cb8be1f45af5577bffcd09 Mon Sep 17 00:00:00 2001 From: penguin321 Date: Sat, 16 Jan 2021 18:37:51 +0000 Subject: [PATCH 150/363] Translated using Weblate (German) Currently translated at 61.3% (387 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 52f5041bb..eab2dc69c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -476,5 +476,18 @@ "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", - "domain_name_unknown": "Domäne '{domain}' unbekannt" + "domain_name_unknown": "Domäne '{domain}' unbekannt", + "group_user_not_in_group": "Der Benutzer {user} ist nicht in der Gruppe {group}", + "group_user_already_in_group": "Der Benutzer {user} ist bereits in der Gruppe {group}", + "group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher", + "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in Yunohost zu halten", + "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber Yunohost wird sie entfernen...", + "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", + "group_already_exist": "Die Gruppe {group} existiert bereits", + "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", + "global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account", + "global_settings_setting_smtp_relay_port": "SMTP Relay Port", + "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", + "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", + "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domains:s}\" entfernen." } From 7c837a2db3e0c93c31023541ddced01c0ca74d7b Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 18 Jan 2021 13:19:21 +0000 Subject: [PATCH 151/363] Translated using Weblate (German) Currently translated at 62.5% (395 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/de.json b/locales/de.json index eab2dc69c..bdf21ac1b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -52,13 +52,13 @@ "domain_unknown": "Unbekannte Domain", "done": "Erledigt", "downloading": "Wird heruntergeladen…", - "dyndns_cron_installed": "DynDNS Cronjob erfolgreich angelegt", - "dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte nicht entfernt werden", + "dyndns_cron_installed": "DynDNS Cronjob erfolgreich erstellt", + "dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte aufgrund dieses Fehlers nicht entfernt werden: {error}", "dyndns_cron_removed": "DynDNS-Cronjob gelöscht", "dyndns_ip_update_failed": "Konnte die IP-Adresse für DynDNS nicht aktualisieren", "dyndns_ip_updated": "Aktualisierung Ihrer IP-Adresse bei DynDNS", "dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.", - "dyndns_registered": "Deine DynDNS Domain wurde registriert", + "dyndns_registered": "DynDNS Domain 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…", @@ -160,7 +160,7 @@ "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", "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", + "dyndns_no_domain_registered": "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}'", @@ -254,7 +254,7 @@ "global_settings_setting_security_ssh_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den SSH-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", "group_deletion_failed": "Kann Gruppe '{group}' nicht löschen", - "dyndns_provider_unreachable": "Dyndns-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", + "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", "group_created": "Gruppe '{group}' angelegt", "group_creation_failed": "Kann Gruppe '{group}' nicht anlegen", "group_unknown": "Die Gruppe '{group:s}' ist unbekannt", @@ -265,7 +265,7 @@ "global_settings_setting_security_postfix_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Postfix-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "log_category_404": "Die Log-Kategorie '{category}' existiert nicht", "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting:s} scheint den Typ {unknown_type:s} zu haben, ist aber kein vom System unterstützter Typ.", - "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo dpkg --configure -a` ausführst.", + "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key:s}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "global_settings_setting_example_bool": "Beispiel einer booleschen Option", From 35a0711713b2dd69457ce862009f2959afe26eea Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 20 Jan 2021 01:28:55 +0100 Subject: [PATCH 152/363] Stupid yolopatch for not-normalized app path settings >_> (#1141) * Update app.py * Be more careful (c.f. _normalize_domain_path code ... maybe the path doesn't start with / either ..) * Annnnnd ignore case where path is '/' which is fine --- src/yunohost/app.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0349f92e7..ade39bf20 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1892,6 +1892,18 @@ def _get_app_settings(app_id): # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... settings = {k: _encode_string(v) for k, v in settings.items()} + + # Stupid fix for legacy bullshit + # In the past, some setups did not have proper normalization for app domain/path + # Meaning some setups (as of January 2021) still have path=/foobar/ (with a trailing slash) + # resulting in stupid issue unless apps using ynh_app_normalize_path_stuff + # So we yolofix the settings if such an issue is found >_> + # A simple call to `yunohost app list` (which happens quite often) should be enough + # to migrate all app settings ... so this can probably be removed once we're past Bullseye... + if settings.get("path") != "/" and (settings.get("path", "").endswith("/") or not settings.get("path", "/").startswith("/")): + settings["path"] = "/" + settings["path"].strip("/") + _set_app_settings(app_id, settings) + if app_id == settings['id']: return settings except (IOError, TypeError, KeyError): @@ -2423,7 +2435,7 @@ class YunoHostArgumentFormatParser(object): if parsed_question.ask is None: parsed_question.ask = "Enter value for '%s':" % parsed_question.name - + # Empty value is parsed as empty string if parsed_question.default == "": parsed_question.default = None From 63a0fcc75382b9842d3e48c15c456143470c32a3 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 15 Jan 2021 19:54:39 +0000 Subject: [PATCH 153/363] Translated using Weblate (German) Currently translated at 59.2% (374 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/locales/de.json b/locales/de.json index 7811d3677..52f5041bb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -41,14 +41,14 @@ "backup_running_hooks": "Datensicherunghook wird ausgeführt...", "custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", - "domain_created": "Die Domain wurde angelegt", - "domain_creation_failed": "Konnte Domain nicht erzeugen", + "domain_created": "Domäne erstellt", + "domain_creation_failed": "Konnte Domäne nicht erzeugen", "domain_deleted": "Domain wurde gelöscht", "domain_deletion_failed": "Domain {domain}: {error} konnte nicht gelöscht werden", - "domain_dyndns_already_subscribed": "Du hast dich schon für eine DynDNS-Domain angemeldet", + "domain_dyndns_already_subscribed": "Sie haben sich schon für eine DynDNS-Domäne registriert", "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_exists": "Die Domäne existiert bereits", + "domain_uninstall_app_first": "Diese Apps sind noch auf Ihrer Domäne installiert; \n{apps}\n\nBitte deinstallieren Sie sie mit dem Befehl 'yunohost app remove the_app_id' oder verschieben Sie sie mit 'yunohost app change-url the_app_id'", "domain_unknown": "Unbekannte Domain", "done": "Erledigt", "downloading": "Wird heruntergeladen…", @@ -187,7 +187,7 @@ "certmanager_unable_to_parse_self_CA_name": "Der Name der Zertifizierungsstelle für selbstsignierte Zertifikate konnte nicht aufgelöst werden (Datei: {file:s})", "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.", - "domain_hostname_failed": "Erstellen des neuen Hostnamens fehlgeschlagen", + "domain_hostname_failed": "Sie können keinen neuen Hostnamen verwenden. Das kann zukünftige Probleme verursachen (es kann auch sein, dass es funktioniert).", "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_url_failed_nginx_reload": "NGINX konnte nicht neu gestartet werden. Hier ist der Output von 'nginx -t':\n{nginx_errors:s}", @@ -475,5 +475,6 @@ "diagnosis_never_ran_yet": "Sie haben kürzlich einen neuen Yunohost-Server installiert aber es gibt davon noch keinen Diagnosereport. Sie sollten eine Diagnose anstossen. Sie können das entweder vom Webadmin aus oder in der Kommandozeile machen. In der Kommandozeile verwenden Sie dafür den Befehl 'yunohost diagnosis run'.", "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", - "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}" + "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", + "domain_name_unknown": "Domäne '{domain}' unbekannt" } From f617b2fe72623e3f8f9ba8879deaa1c966b3271f Mon Sep 17 00:00:00 2001 From: penguin321 Date: Sat, 16 Jan 2021 18:37:51 +0000 Subject: [PATCH 154/363] Translated using Weblate (German) Currently translated at 61.3% (387 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 52f5041bb..eab2dc69c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -476,5 +476,18 @@ "diagnosis_http_nginx_conf_not_up_to_date_details": "Um dieses Problem zu beheben, geben Sie in der Kommandozeile yunohost tools regen-conf nginx --dry-run --with-diff ein. Dieses Tool zeigt ihnen den Unterschied an. Wenn Sie damit einverstanden sind, können Sie mit yunohost tools regen-conf nginx --force die Änderungen übernehmen.", "diagnosis_backports_in_sources_list": "Sie haben anscheinend apt (den Paketmanager) für das Backports-Repository konfiguriert. Wir raten strikte davon ab, Pakete aus dem Backports-Repository zu installieren. Diese würden wahrscheinlich zu Instabilitäten und Konflikten führen. Es sei denn, Sie wissen was Sie tun.", "diagnosis_basesystem_hardware_model": "Das Servermodell ist {model}", - "domain_name_unknown": "Domäne '{domain}' unbekannt" + "domain_name_unknown": "Domäne '{domain}' unbekannt", + "group_user_not_in_group": "Der Benutzer {user} ist nicht in der Gruppe {group}", + "group_user_already_in_group": "Der Benutzer {user} ist bereits in der Gruppe {group}", + "group_cannot_edit_visitors": "Die Gruppe \"Besucher\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe und repräsentiert anonyme Besucher", + "group_cannot_edit_all_users": "Die Gruppe \"all_users\" kann nicht manuell editiert werden. Sie ist eine Sondergruppe die dafür gedacht ist alle Benutzer in Yunohost zu halten", + "group_already_exist_on_system_but_removing_it": "Die Gruppe {group} existiert bereits in den Systemgruppen, aber Yunohost wird sie entfernen...", + "group_already_exist_on_system": "Die Gruppe {group} existiert bereits in den Systemgruppen", + "group_already_exist": "Die Gruppe {group} existiert bereits", + "global_settings_setting_smtp_relay_password": "SMTP Relay Host Passwort", + "global_settings_setting_smtp_relay_user": "SMTP Relay Benutzer Account", + "global_settings_setting_smtp_relay_port": "SMTP Relay Port", + "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", + "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", + "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domains:s}\" entfernen." } From a61dece925b5ba3d8a9fa0673d4802e22a66ef7c Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 18 Jan 2021 13:19:21 +0000 Subject: [PATCH 155/363] Translated using Weblate (German) Currently translated at 62.5% (395 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/de.json b/locales/de.json index eab2dc69c..bdf21ac1b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -52,13 +52,13 @@ "domain_unknown": "Unbekannte Domain", "done": "Erledigt", "downloading": "Wird heruntergeladen…", - "dyndns_cron_installed": "DynDNS Cronjob erfolgreich angelegt", - "dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte nicht entfernt werden", + "dyndns_cron_installed": "DynDNS Cronjob erfolgreich erstellt", + "dyndns_cron_remove_failed": "Der DynDNS Cronjob konnte aufgrund dieses Fehlers nicht entfernt werden: {error}", "dyndns_cron_removed": "DynDNS-Cronjob gelöscht", "dyndns_ip_update_failed": "Konnte die IP-Adresse für DynDNS nicht aktualisieren", "dyndns_ip_updated": "Aktualisierung Ihrer IP-Adresse bei DynDNS", "dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.", - "dyndns_registered": "Deine DynDNS Domain wurde registriert", + "dyndns_registered": "DynDNS Domain 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…", @@ -160,7 +160,7 @@ "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", "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", + "dyndns_no_domain_registered": "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}'", @@ -254,7 +254,7 @@ "global_settings_setting_security_ssh_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den SSH-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", "group_deletion_failed": "Kann Gruppe '{group}' nicht löschen", - "dyndns_provider_unreachable": "Dyndns-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", + "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", "group_created": "Gruppe '{group}' angelegt", "group_creation_failed": "Kann Gruppe '{group}' nicht anlegen", "group_unknown": "Die Gruppe '{group:s}' ist unbekannt", @@ -265,7 +265,7 @@ "global_settings_setting_security_postfix_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Postfix-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "log_category_404": "Die Log-Kategorie '{category}' existiert nicht", "global_settings_unknown_type": "Unerwartete Situation, die Einstellung {setting:s} scheint den Typ {unknown_type:s} zu haben, ist aber kein vom System unterstützter Typ.", - "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo dpkg --configure -a` ausführst.", + "dpkg_is_broken": "Du kannst das gerade nicht tun, weil dpkg/APT (der Systempaketmanager) in einem defekten Zustand zu sein scheint.... Du kannst versuchen, dieses Problem zu lösen, indem du dich über SSH verbindest und `sudo apt install --fix-broken` sowie/oder `sudo dpkg --configure -a` ausführst.", "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key:s}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "global_settings_setting_example_bool": "Beispiel einer booleschen Option", From 99ab7c642ce7215dea2373debcd07753b1bba3e1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 20 Jan 2021 01:53:04 +0100 Subject: [PATCH 156/363] Update changelog for 4.1.6 --- debian/changelog | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index a6ed39311..6a3691b4e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (4.1.6) stable; urgency=low + + - [fix] Make dyndns update more resilient to ns0.yunohost.org being down ([#1140](https://github.com/yunohost/yunohost/pull/1140)) + - [fix] Stupid yolopatch for not-normalized app path settings ([#1141](https://github.com/yunohost/yunohost/pull/1141)) + - [i18n] Update translations for German + + Thanks to all contributors <3 ! (Christian W., Daniel, penguin321) + + -- Alexandre Aubin Wed, 20 Jan 2021 01:46:02 +0100 + yunohost (4.1.5) stable; urgency=low - [fix] Update helpers ([#1136](https://github.com/yunohost/yunohost/pull/11346)) @@ -9,7 +19,6 @@ yunohost (4.1.5) stable; urgency=low Thanks to all contributors <3 ! (Aleks, Kay0u, Omnia89, jorge-vitrubio, YohannEpitech, xaloc33) - -- Kayou Thu, 14 Jan 2021 21:23:39 +0100 yunohost (4.1.4.4) stable; urgency=low From a97a9df31614c98361e128dad12cdec413a8419e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 20 Jan 2021 05:06:43 +0100 Subject: [PATCH 157/363] Fix more implicit references to python2, adapt generate_manpages for python3 --- debian/rules | 4 ++-- doc/generate_manpages.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/rules b/debian/rules index 54ebc5781..3790c0ef2 100755 --- a/debian/rules +++ b/debian/rules @@ -9,8 +9,8 @@ override_dh_auto_build: # Generate bash completion file - python data/actionsmap/yunohost_completion.py - python doc/generate_manpages.py --gzip --output doc/yunohost.8.gz + python3 data/actionsmap/yunohost_completion.py + python3 doc/generate_manpages.py --gzip --output doc/yunohost.8.gz override_dh_installinit: dh_installinit -pyunohost --name=yunohost-api --restart-after-upgrade diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py index 0b1251c28..67d51e024 100644 --- a/doc/generate_manpages.py +++ b/doc/generate_manpages.py @@ -60,7 +60,7 @@ def main(): # Getting the dictionary containning what actions are possible per domain actionsmap = ordered_yaml_load(actionsmap) - for i in actionsmap.keys(): + for i in list(actionsmap.keys()): if i.startswith("_"): del actionsmap[i] @@ -78,7 +78,7 @@ def main(): output.write(result) else: with gzip.open(output_path, mode="w", compresslevel=9) as output: - output.write(result) + output.write(result.encode()) if __name__ == '__main__': From 9185d044a54905548f3f0711ce4c9acbbbc90742 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 20 Jan 2021 05:24:25 +0100 Subject: [PATCH 158/363] Require moulinette >= 4.2 + placeholder / bump version to 4.2 for unstable deb builds / CI stuff --- debian/changelog | 6 ++++++ debian/control | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 6a3691b4e..fc14dfc9a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.2) unstable; urgency=low + + - Placeholder for 4.2 to satisfy CI / debian build during dev + + -- Alexandre Aubin Wed, 20 Jan 2021 05:19:58 +0100 + yunohost (4.1.6) stable; urgency=low - [fix] Make dyndns update more resilient to ns0.yunohost.org being down ([#1140](https://github.com/yunohost/yunohost/pull/1140)) diff --git a/debian/control b/debian/control index 4e57c7a84..d95b17f4e 100644 --- a/debian/control +++ b/debian/control @@ -10,7 +10,7 @@ Package: yunohost Essential: yes Architecture: all Depends: ${python3:Depends}, ${misc:Depends} - , moulinette (>= 4.1.0.1), ssowat (>= 4.0) + , moulinette (>= 4.2), ssowat (>= 4.0) , python3-psutil, python3-requests, python3-dnspython, python3-openssl , python3-miniupnpc, python3-dbus, python3-jinja2 , python3-toml, python3-packaging, python3-publicsuffix From 1387dff47c04bcd2e4283d469781419a149f62ea Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 05:04:39 +0100 Subject: [PATCH 159/363] Not sure what i'm doing but tests failing because of this package for some reasons... we had a custom build for python-miniupnpc (python2) because Debian vanilla doesn't provide it ... but not we're in python3 and turns out the custom build also built the python3 version .. but ideally we want to fall back to debian vanilla version ? --- debian/control | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/control b/debian/control index d95b17f4e..d48911df3 100644 --- a/debian/control +++ b/debian/control @@ -46,6 +46,7 @@ Conflicts: iptables-persistent , redis-server (>= 5:5.0.7) , fail2ban (>= 0.11) , iptables (>= 1.8.3) + , python3-miniupnpc (= 2.1-1+ynh10) Description: manageable and configured self-hosting server YunoHost aims to make self-hosting accessible to everyone. It configures an email, Web and IM server alongside a LDAP base. It also provides From 02ed684f9a625390a9409214fcbd3bdb1bbc8206 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 21 Jan 2021 12:00:27 +0100 Subject: [PATCH 160/363] shell=True, because Aleks says so --- 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 b11926f70..a243192e9 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -400,7 +400,7 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): _env.update(env) returncode = call_async_output( - cmd, loggers, shell=False, cwd=chdir, + cmd, loggers, shell=True, cwd=chdir, env=_env ) From c8a23f26bba37d0ab1fd6924b068197a3386c1cf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 16:25:58 +0100 Subject: [PATCH 161/363] Handle case where DKIM record is split into several pieces --- data/hooks/diagnosis/12-dnsrecords.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 941911147..cb8193dd5 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -130,8 +130,11 @@ class DNSRecordsDiagnoser(Diagnoser): # Split expected/current # from "v=DKIM1; k=rsa; p=hugekey;" # to a set like {'v=DKIM1', 'k=rsa', 'p=...'} + # Additionally, for DKIM, because the key is pretty long, + # some DNS registrar sometime split it into several pieces like this: + # "p=foo" "bar" (with a space and quotes in the middle)... expected = set(r["value"].strip(';" ').replace(";", " ").split()) - current = set(r["current"].strip(';" ').replace(";", " ").split()) + current = set(r["current"].replace('" "', '').strip(';" ').replace(";", " ").split()) # For SPF, ignore parts starting by ip4: or ip6: if r["name"] == "@": From 8d027660989662dc300a581e4c099d5d5d3542d0 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 21 Jan 2021 19:06:15 +0100 Subject: [PATCH 162/363] fix de locale --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index bdf21ac1b..764ca2c1e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -489,5 +489,5 @@ "global_settings_setting_smtp_relay_port": "SMTP Relay Port", "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", - "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domains:s}\" entfernen." + "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen." } From 21a0c65ed623d5ea566789166a95cbe267e61a76 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 19:36:21 +0100 Subject: [PATCH 163/363] diagnosis: Ignore /dev/loop devices in systemresources --- data/hooks/diagnosis/50-systemresources.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index 64517f764..30a2ad1f8 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -69,6 +69,9 @@ class SystemResourcesDiagnoser(Diagnoser): disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + # Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/) + disk_partitions = [d for d in disk_partitions if not d.device.startswith("/dev/loop")] + for disk_partition in disk_partitions: device = disk_partition.device mountpoint = disk_partition.mountpoint From 8b3ec8a1372d24404caf0067228db3d99242ebf1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 20:34:43 +0100 Subject: [PATCH 164/363] Diagnosis: report low total space for rootfs --- data/hooks/diagnosis/50-systemresources.py | 20 ++++++++++++++++++++ locales/en.json | 2 ++ 2 files changed, 22 insertions(+) diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index 30a2ad1f8..fdda2c2f0 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -103,6 +103,26 @@ class SystemResourcesDiagnoser(Diagnoser): yield item + # + # Check for minimal space on / + /var + # because some stupid VPS provider only configure a stupidly + # low amount of disk space for the root partition + # which later causes issue when it gets full... + # + + main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ['/', '/var']] + main_space = sum([psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions]) + if main_space < 10 * GB: + yield dict(meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="ERROR", + summary="diagnosis_rootfstotalspace_critical") + if main_space < 14 * GB: + yield dict(meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="WARNING", + summary="diagnosis_rootfstotalspace_warning") + # # Recent kills by oom_reaper # diff --git a/locales/en.json b/locales/en.json index dbaee0bdf..33efcda14 100644 --- a/locales/en.json +++ b/locales/en.json @@ -232,6 +232,8 @@ "diagnosis_regenconf_allgood": "All configurations files are in line with the recommended configuration!", "diagnosis_regenconf_manually_modified": "Configuration file {file} appears to have been manually modified.", "diagnosis_regenconf_manually_modified_details": "This is probably OK if you know what you're doing! YunoHost will stop updating this file automatically... But beware that YunoHost upgrades could contain important recommended changes. If you want to, you can inspect the differences with yunohost tools regen-conf {category} --dry-run --with-diff and force the reset to the recommended configuration with yunohost tools regen-conf {category} --force", + "diagnosis_rootfstotalspace_warning": "The root filesystem only has a total of {space}. This may be okay, but be careful because ultimately you may run out of disk space quickly... It's recommended to have at least 16 GB for the root filesystem.", + "diagnosis_rootfstotalspace_critical": "The root filesystem only has a total of {space} which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16 GB for the root filesystem.", "diagnosis_security_vulnerable_to_meltdown": "You appear vulnerable to the Meltdown criticial security vulnerability", "diagnosis_security_vulnerable_to_meltdown_details": "To fix this, you should upgrade your system and reboot to load the new linux kernel (or contact your server provider if this doesn't work). See https://meltdownattack.com/ for more infos.", "diagnosis_description_basesystem": "Base system", From fd61900352472af763b5511f65a0cbd4742bfe01 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 20:53:07 +0100 Subject: [PATCH 165/363] Also complain about low rootfs total disk space during postinstall --- .gitlab/ci/install.gitlab-ci.yml | 2 +- .gitlab/ci/test.gitlab-ci.yml | 2 +- data/actionsmap/yunohost.yml | 4 ++++ locales/en.json | 1 + src/yunohost/tools.py | 11 ++++++++++- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.gitlab/ci/install.gitlab-ci.yml b/.gitlab/ci/install.gitlab-ci.yml index 1df4fc4b9..e2662e9e2 100644 --- a/.gitlab/ci/install.gitlab-ci.yml +++ b/.gitlab/ci/install.gitlab-ci.yml @@ -26,4 +26,4 @@ install-postinstall: script: - apt-get update -o Acquire::Retries=3 - DEBIAN_FRONTEND=noninteractive SUDO_FORCE_REMOVE=yes apt --assume-yes -o Dpkg::Options::="--force-confold" --allow-downgrades install ./$YNH_BUILD_DIR/*.deb - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns + - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace diff --git a/.gitlab/ci/test.gitlab-ci.yml b/.gitlab/ci/test.gitlab-ci.yml index 6cbb89d0c..a4ec77ee8 100644 --- a/.gitlab/ci/test.gitlab-ci.yml +++ b/.gitlab/ci/test.gitlab-ci.yml @@ -34,7 +34,7 @@ full-tests: PYTEST_ADDOPTS: "--color=yes" before_script: - *install_debs - - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns + - yunohost tools postinstall -d domain.tld -p the_password --ignore-dyndns --force-diskspace script: - python3 -m pytest --cov=yunohost tests/ src/yunohost/tests/ --junitxml=report.xml needs: diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8eee048f2..fcc2c5e72 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1416,6 +1416,10 @@ tools: --force-password: help: Use this if you really want to set a weak password action: store_true + --force-diskspace: + help: Use this if you really want to install Yunohost on a setup with less than 10 GB on the root filesystem + action: store_true + ### tools_update() update: diff --git a/locales/en.json b/locales/en.json index 33efcda14..931b1476b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -508,6 +508,7 @@ "permission_require_account": "Permission {permission} only makes sense for users having an account, and therefore cannot be enabled for visitors.", "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", + "postinstall_low_rootfsspace": "The root filesystem has a total space less than 10 GB, which is quite worrisome! You will likely run out of disk space very quickly! It's recommended to have at least 16GB for the root filesystem. If you want to install YunoHost despite this warning, re-run the postinstall with --force-diskspace", "regenconf_file_backed_up": "Configuration file '{conf}' backed up to '{backup}'", "regenconf_file_copy_failed": "Could not copy the new configuration file '{new}' to '{conf}'", "regenconf_file_kept_back": "The configuration file '{conf}' is expected to be deleted by regen-conf (category {category}) but was kept back.", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..f7b2b91cb 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -229,7 +229,7 @@ def _detect_virt(): @is_unit_operation() def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, - force_password=False): + force_password=False, force_diskspace=False): """ YunoHost post-install @@ -242,6 +242,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, """ from yunohost.utils.password import assert_password_is_strong_enough from yunohost.domain import domain_main_domain + import psutil dyndns_provider = "dyndns.yunohost.org" @@ -249,6 +250,14 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, if os.path.isfile('/etc/yunohost/installed'): raise YunohostError('yunohost_already_installed') + # Check there's at least 10 GB on the rootfs... + disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ['/', '/var']] + main_space = sum([psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions]) + GB = 1024**3 + if not force_diskspace and main_space < 10 * GB: + raise YunohostError("postinstall_low_rootfsspace") + # Check password if not force_password: assert_password_is_strong_enough("admin", password) From b53859db50fe8bdcc6a24cc91c35d16bee8b9eb1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 21:23:49 +0100 Subject: [PATCH 166/363] [fix] python3: fix yunopaste encoding --- src/yunohost/utils/yunopaste.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 9a406a580..5afb865cd 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -20,6 +20,8 @@ def yunopaste(data): except Exception as e: logger.warning("For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s" % e) + data = data.encode() + try: r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: From 15e47b2a2bfc2f24fa2a23b79cf799f1b6348cba Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 21:24:42 +0100 Subject: [PATCH 167/363] yunohost log share --- data/actionsmap/yunohost.yml | 10 +++++++++- locales/ca.json | 2 +- locales/de.json | 2 +- locales/en.json | 2 +- locales/eo.json | 2 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/it.json | 2 +- locales/oc.json | 2 +- src/yunohost/log.py | 3 +++ 10 files changed, 20 insertions(+), 9 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8eee048f2..549ac7111 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1681,7 +1681,7 @@ log: default: 50 type: int --share: - help: Share the full log using yunopaste + help: (Deprecated, see yunohost log share) Share the full log using yunopaste action: store_true -i: full: --filter-irrelevant @@ -1692,6 +1692,14 @@ log: help: Include metadata about sub-operations of this operation... (e.g. initializing groups/permissions when installing an app) action: store_true + ### log_share() + share: + action_help: Share the full log on yunopaste (alias to display --share) + api: GET /logs/share + arguments: + path: + help: Log file to share + ############################# # Diagnosis # diff --git a/locales/ca.json b/locales/ca.json index 716ffce5f..d3188a9c0 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -201,7 +201,7 @@ "log_link_to_log": "El registre complet d'aquesta operació: «{desc}»", "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log display {name} »", "log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", - "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log display {name} --share »", + "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log share {name} »", "log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles", "log_operation_unit_unclosed_properly": "L'operació no s'ha tancat de forma correcta", "log_app_change_url": "Canvia l'URL de l'aplicació « {} »", diff --git a/locales/de.json b/locales/de.json index 764ca2c1e..2b413a241 100644 --- a/locales/de.json +++ b/locales/de.json @@ -284,7 +284,7 @@ "good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", "global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason:s}", - "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log display {name} --share', um Hilfe zu erhalten", + "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", "log_app_change_url": "Ändere die URL der Anwendung '{}'", "global_settings_setting_security_password_user_strength": "Stärke des Benutzerpassworts", diff --git a/locales/en.json b/locales/en.json index dbaee0bdf..28dabaacf 100644 --- a/locales/en.json +++ b/locales/en.json @@ -362,7 +362,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": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help", - "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log display {name} --share' to get help", + "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", "log_does_exists": "There is no 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_change_url": "Change the URL of the '{}' app", diff --git a/locales/eo.json b/locales/eo.json index f093633a5..a89cb313b 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -295,7 +295,7 @@ "restore_extracting": "Eltirante bezonatajn dosierojn el la ar theivo…", "upnp_port_open_failed": "Ne povis malfermi havenon per UPnP", "log_app_upgrade": "Ĝisdatigu la aplikon '{}'", - "log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log display {name} --share' por akiri helpon", + "log_help_to_get_failed_log": "La operacio '{desc}' ne povis finiĝi. Bonvolu dividi la plenan ŝtipon de ĉi tiu operacio per la komando 'yunohost log share {name}' por akiri helpon", "migration_description_0002_migrate_to_tsig_sha256": "Plibonigu sekurecon de DynDNS TSIG-ĝisdatigoj per SHA-512 anstataŭ MD5", "port_already_closed": "Haveno {port:d} estas jam fermita por {ip_version:s} rilatoj", "hook_name_unknown": "Nekonata hoko-nomo '{name:s}'", diff --git a/locales/es.json b/locales/es.json index 21944c372..e9ccad7ae 100644 --- a/locales/es.json +++ b/locales/es.json @@ -408,7 +408,7 @@ "log_app_change_url": "Cambiar el URL de la aplicación «{}»", "log_operation_unit_unclosed_properly": "La unidad de operación no se ha cerrado correctamente", "log_does_exists": "No existe ningún registro de actividades con el nombre '{log}', ejecute 'yunohost log list' para ver todos los registros de actividades disponibles", - "log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log display {name} --share»", + "log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log share {name}»", "log_link_to_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, proporcione el registro completo de esta operación pulsando aquí", "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log display {name}»", "log_link_to_log": "Registro completo de esta operación: «{desc}»", diff --git a/locales/fr.json b/locales/fr.json index 31c65d1cd..3d84914c3 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -280,7 +280,7 @@ "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log display {name}'", "log_link_to_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en cliquant ici", "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge PHP 7, vous pourriez ne plus pouvoir restaurer vos applications PHP (cause : {error:s})", - "log_help_to_get_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en utilisant la commande 'yunohost log display {name} --share'", + "log_help_to_get_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en utilisant la commande 'yunohost log share {name}'", "log_does_exists": "Il n’y a pas de journal des opérations avec le nom '{log}', utilisez 'yunohost log list' pour voir tous les journaux d’opérations disponibles", "log_operation_unit_unclosed_properly": "L’opération ne s’est pas terminée correctement", "log_app_change_url": "Changer l’URL de l’application '{}'", diff --git a/locales/it.json b/locales/it.json index 9b5d18f1a..5615ac1b7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -281,7 +281,7 @@ "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log display {name}'", "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", - "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log display {name} --share'", + "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", "log_does_exists": "Non esiste nessun registro delle operazioni chiamato '{log}', usa 'yunohost log list' per vedere tutti i registri delle operazioni disponibili", "log_app_change_url": "Cambia l'URL dell'app '{}'", "log_app_install": "Installa l'app '{}'", diff --git a/locales/oc.json b/locales/oc.json index 17201fefe..5a2a9401e 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -303,7 +303,7 @@ "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log display {name} »", "backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas a restaurar vòstras aplicacions PHP (rason : {error:s})", "log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion", - "log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log display {name} --share »", + "log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log share {name} »", "log_does_exists": "I a pas cap de jornal d’audit per l’operacion amb lo nom « {log} », utilizatz « yunohost log list » per veire totes los jornals d’operacion disponibles", "log_operation_unit_unclosed_properly": "L’operacion a pas acabat corrèctament", "log_app_change_url": "Cambiar l’URL de l’aplicacion « {} »", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 850680237..235084c19 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -282,6 +282,9 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su return infos +def log_share(path): + return log_display(path, share=True) + def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], exclude=['password'], operation_key=None): From e22168a98d301701e85f53c3d944dd966760ee9d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 21:35:37 +0100 Subject: [PATCH 168/363] log display -> log show (+ fix API route) --- data/actionsmap/yunohost.yml | 10 ++++++---- locales/ca.json | 2 +- locales/de.json | 2 +- locales/en.json | 2 +- locales/eo.json | 2 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/it.json | 2 +- locales/nb_NO.json | 2 +- locales/oc.json | 2 +- src/yunohost/log.py | 4 ++-- 11 files changed, 17 insertions(+), 15 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 549ac7111..a20e04ba4 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1668,10 +1668,12 @@ log: help: Include metadata about operations that are not the main operation but are sub-operations triggered by another ongoing operation... (e.g. initializing groups/permissions when installing an app) action: store_true - ### log_display() - display: + ### log_show() + show: action_help: Display a log content - api: GET /logs/display + api: GET /logs/ + deprecated_alias: + - display arguments: path: help: Log file which to display the content @@ -1694,7 +1696,7 @@ log: ### log_share() share: - action_help: Share the full log on yunopaste (alias to display --share) + action_help: Share the full log on yunopaste (alias to show --share) api: GET /logs/share arguments: path: diff --git a/locales/ca.json b/locales/ca.json index d3188a9c0..ce23d7212 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -199,7 +199,7 @@ "log_corrupted_md_file": "El fitxer de metadades YAML associat amb els registres està malmès: « {md_file} »\nError: {error}", "log_category_404": "La categoria de registres « {category} » no existeix", "log_link_to_log": "El registre complet d'aquesta operació: «{desc}»", - "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log display {name} »", + "log_help_to_get_log": "Per veure el registre de l'operació « {desc} », utilitzeu l'ordre « yunohost log show {name}{name} »", "log_link_to_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, proveïu el registre complete de l'operació clicant aquí", "log_help_to_get_failed_log": "No s'ha pogut completar l'operació « {desc} ». Per obtenir ajuda, compartiu el registre complete de l'operació utilitzant l'ordre « yunohost log share {name} »", "log_does_exists": "No hi ha cap registre per l'operació amb el nom« {log} », utilitzeu « yunohost log list » per veure tots els registre d'operació disponibles", diff --git a/locales/de.json b/locales/de.json index 2b413a241..efc25f7c5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -269,7 +269,7 @@ "global_settings_unknown_setting_from_settings_file": "Unbekannter Schlüssel in den Einstellungen: '{setting_key:s}', verwerfen und speichern in /etc/yunohost/settings-unknown.json", "log_link_to_log": "Vollständiges Log dieser Operation: '{desc}'", "global_settings_setting_example_bool": "Beispiel einer booleschen Option", - "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log display {name}'", + "log_help_to_get_log": "Um das Protokoll der Operation '{desc}' anzuzeigen, verwende den Befehl 'yunohost log show {name}{name}'", "global_settings_setting_security_nginx_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Webserver NGINX. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "backup_php5_to_php7_migration_may_fail": "Dein Archiv konnte nicht für PHP 7 konvertiert werden, Du kannst deine PHP-Anwendungen möglicherweise nicht wiederherstellen (Grund: {error:s})", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration", diff --git a/locales/en.json b/locales/en.json index 28dabaacf..9c96261f4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -360,7 +360,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 damaged: '{md_file}\nError: {error}'", "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_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log show {name}{name}'", "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by clicking here to get help", "log_help_to_get_failed_log": "The operation '{desc}' could not be completed. Please share the full log of this operation using the command 'yunohost log share {name}' to get help", "log_does_exists": "There is no operation log with the name '{log}', use 'yunohost log list' to see all available operation logs", diff --git a/locales/eo.json b/locales/eo.json index a89cb313b..de301845d 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -397,7 +397,7 @@ "password_too_simple_4": "La pasvorto bezonas almenaŭ 12 signojn kaj enhavas ciferon, majuskle, pli malaltan kaj specialajn signojn", "migration_0003_main_upgrade": "Komencanta ĉefa ĝisdatigo …", "regenconf_file_updated": "Agordodosiero '{conf}' ĝisdatigita", - "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log display {name}'", + "log_help_to_get_log": "Por vidi la protokolon de la operacio '{desc}', uzu la komandon 'yunohost log show {name}{name}'", "global_settings_setting_security_nginx_compatibility": "Kongruo vs sekureca kompromiso por la TTT-servilo NGINX. Afektas la ĉifradojn (kaj aliajn aspektojn pri sekureco)", "no_internet_connection": "La servilo ne estas konektita al la interreto", "migration_0008_dsa": "• La DSA-ŝlosilo estos malŝaltita. Tial vi eble bezonos nuligi spuran averton de via SSH-kliento kaj revizii la fingrospuron de via servilo;", diff --git a/locales/es.json b/locales/es.json index e9ccad7ae..ed02e9802 100644 --- a/locales/es.json +++ b/locales/es.json @@ -410,7 +410,7 @@ "log_does_exists": "No existe ningún registro de actividades con el nombre '{log}', ejecute 'yunohost log list' para ver todos los registros de actividades disponibles", "log_help_to_get_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, comparta el registro completo de esta operación ejecutando la orden «yunohost log share {name}»", "log_link_to_failed_log": "No se pudo completar la operación «{desc}». Para obtener ayuda, proporcione el registro completo de esta operación pulsando aquí", - "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log display {name}»", + "log_help_to_get_log": "Para ver el registro de la operación «{desc}», ejecute la orden «yunohost log show {name}{name}»", "log_link_to_log": "Registro completo de esta operación: «{desc}»", "log_category_404": "La categoría de registro «{category}» no existe", "log_corrupted_md_file": "El archivo de metadatos YAML asociado con el registro está dañado: «{md_file}\nError: {error}»", diff --git a/locales/fr.json b/locales/fr.json index 3d84914c3..510528875 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -277,7 +277,7 @@ "log_corrupted_md_file": "Le fichier YAML de métadonnées associé aux logs est corrompu : '{md_file}'\nErreur : {error}", "log_category_404": "Le journal de la catégorie '{category}' n’existe pas", "log_link_to_log": "Journal complet de cette opération : ' {desc} '", - "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log display {name}'", + "log_help_to_get_log": "Pour voir le journal de cette opération '{desc}', utilisez la commande 'yunohost log show {name}{name}'", "log_link_to_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en cliquant ici", "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge PHP 7, vous pourriez ne plus pouvoir restaurer vos applications PHP (cause : {error:s})", "log_help_to_get_failed_log": "L’opération '{desc}' a échoué ! Pour obtenir de l’aide, merci de partager le journal de l’opération en utilisant la commande 'yunohost log share {name}'", diff --git a/locales/it.json b/locales/it.json index 5615ac1b7..ab974428f 100644 --- a/locales/it.json +++ b/locales/it.json @@ -278,7 +278,7 @@ "log_corrupted_md_file": "Il file dei metadati YAML associato con i registri è danneggiato: '{md_file}'\nErrore: {error}", "log_category_404": "La categoria di registrazione '{category}' non esiste", "log_link_to_log": "Registro completo di questa operazione: '{desc}'", - "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log display {name}'", + "log_help_to_get_log": "Per vedere il registro dell'operazione '{desc}', usa il comando 'yunohost log show {name}{name}'", "global_settings_setting_security_postfix_compatibility": "Bilanciamento tra compatibilità e sicurezza per il server Postfix. Riguarda gli algoritmi di cifratura (e altri aspetti legati alla sicurezza)", "log_link_to_failed_log": "Impossibile completare l'operazione '{desc}'! Per ricevere aiuto, per favore fornisci il registro completo dell'operazione cliccando qui", "log_help_to_get_failed_log": "L'operazione '{desc}' non può essere completata. Per ottenere aiuto, per favore condividi il registro completo dell'operazione utilizzando il comando 'yunohost log share {name}'", diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 07695ec3d..66cefad04 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -132,7 +132,7 @@ "domain_dyndns_already_subscribed": "Du har allerede abonnement på et DynDNS-domene", "log_category_404": "Loggkategorien '{category}' finnes ikke", "log_link_to_log": "Full logg for denne operasjonen: '{desc}'", - "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log display {name}'", + "log_help_to_get_log": "For å vise loggen for operasjonen '{desc}', bruk kommandoen 'yunohost log show {name}{name}'", "log_user_create": "Legg til '{}' bruker", "app_change_url_success": "{app:s} nettadressen er nå {domain:s}{path:s}", "app_install_failed": "Kunne ikke installere {app}: {error}" diff --git a/locales/oc.json b/locales/oc.json index 5a2a9401e..68d142f2b 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -300,7 +300,7 @@ "log_corrupted_md_file": "Lo fichièr YAML de metadonadas ligat als jornals d’audit es damatjat : « {md_file} »\nError : {error:s}", "log_category_404": "La categoria de jornals d’audit « {category} » existís pas", "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}", - "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log display {name} »", + "log_help_to_get_log": "Per veire lo jornal d’aquesta operacion « {desc} », utilizatz la comanda « yunohost log show {name}{name} »", "backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas a restaurar vòstras aplicacions PHP (rason : {error:s})", "log_link_to_failed_log": "L’operacion « {desc} » a pas capitat ! Per obténer d’ajuda, mercés de fornir lo jornal complèt de l’operacion", "log_help_to_get_failed_log": "L’operacion « {desc} » a pas reüssit ! Per obténer d’ajuda, mercés de partejar lo jornal d’audit complèt d’aquesta operacion en utilizant la comanda « yunohost log share {name} »", diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 235084c19..9159d4117 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -130,7 +130,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False): return {"operation": operations} -def log_display(path, number=None, share=False, filter_irrelevant=False, with_suboperations=False): +def log_show(path, number=None, share=False, filter_irrelevant=False, with_suboperations=False): """ Display a log file enriched with metadata if any. @@ -283,7 +283,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False, with_su return infos def log_share(path): - return log_display(path, share=True) + return log_show(path, share=True) def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], From c7b55cdfad8dbb89b7e152aeae269f4845741bfd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 22:30:31 +0100 Subject: [PATCH 169/363] migrations migrate -> migrations run --- data/actionsmap/yunohost.yml | 6 ++++-- debian/postinst | 2 +- locales/ca.json | 2 +- locales/en.json | 2 +- locales/eo.json | 2 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/it.json | 2 +- locales/oc.json | 2 +- src/yunohost/tools.py | 2 +- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index a20e04ba4..1f1109dde 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1519,9 +1519,11 @@ tools: action: store_true ### tools_migrations_migrate() - migrate: + run: action_help: Run migrations - api: POST /migrations/migrate + api: POST /migrations/run + deprecated_alias: + - migrate arguments: targets: help: Migrations to run (all pendings by default) diff --git a/debian/postinst b/debian/postinst index 4b43b2506..7a0371dc2 100644 --- a/debian/postinst +++ b/debian/postinst @@ -15,7 +15,7 @@ do_configure() { yunohost tools regen-conf --output-as none echo "Launching migrations..." - yunohost tools migrations migrate --auto + yunohost tools migrations run --auto echo "Re-diagnosing server health..." yunohost diagnosis run --force diff --git a/locales/ca.json b/locales/ca.json index ce23d7212..7924193d0 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -292,7 +292,7 @@ "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", "migrations_no_migrations_to_run": "No hi ha cap migració a fer", "migrations_skip_migration": "Saltant migració {id}...", - "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».", + "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations run».", "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».", "no_internet_connection": "El servidor no està connectat a Internet", "not_enough_disk_space": "No hi ha prou espai en «{path:s}»", diff --git a/locales/en.json b/locales/en.json index 9c96261f4..629907bb7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -467,7 +467,7 @@ "migrations_running_forward": "Running migration {id}...", "migrations_skip_migration": "Skipping migration {id}...", "migrations_success_forward": "Migration {id} completed", - "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations migrate`.", + "migrations_to_be_ran_manually": "Migration {id} has to be run manually. Please go to Tools → Migrations on the webadmin page, or run `yunohost tools migrations run`.", "not_enough_disk_space": "Not enough free space on '{path:s}'", "invalid_number": "Must be a number", "operation_interrupted": "The operation was manually interrupted?", diff --git a/locales/eo.json b/locales/eo.json index de301845d..1a27831f2 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -358,7 +358,7 @@ "dyndns_registration_failed": "Ne povis registri DynDNS-domajnon: {error:s}", "migration_0003_not_jessie": "La nuna Debian-distribuo ne estas Jessie!", "user_unknown": "Nekonata uzanto: {user:s}", - "migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations migrate`.", + "migrations_to_be_ran_manually": "Migrado {id} devas funkcii permane. Bonvolu iri al Iloj → Migradoj en la retpaĝa paĝo, aŭ kuri `yunohost tools migrations run`.", "migration_0008_warning": "Se vi komprenas tiujn avertojn kaj volas ke YunoHost preterlasu vian nunan agordon, faru la migradon. Alie, vi ankaŭ povas salti la migradon, kvankam ĝi ne rekomendas.", "certmanager_cert_renew_success": "Ni Ĉifru atestilon renovigitan por la domajno '{domain:s}'", "global_settings_reset_success": "Antaŭaj agordoj nun estas rezervitaj al {path:s}", diff --git a/locales/es.json b/locales/es.json index ed02e9802..cfcca071f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -303,7 +303,7 @@ "permission_created": "Creado el permiso «{permission:s}»", "permission_already_exist": "El permiso «{permission}» ya existe", "pattern_password_app": "Las contraseñas no pueden incluir los siguientes caracteres: {forbidden_chars}", - "migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations migrate`.", + "migrations_to_be_ran_manually": "La migración {id} hay que ejecutarla manualmente. Vaya a Herramientas → Migraciones en la página web de administración o ejecute `yunohost tools migrations run`.", "migrations_success_forward": "Migración {id} completada", "migrations_skip_migration": "Omitiendo migración {id}…", "migrations_running_forward": "Ejecutando migración {id}…", diff --git a/locales/fr.json b/locales/fr.json index 510528875..b65268fb7 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -256,7 +256,7 @@ "app_upgrade_app_name": "Mise à jour de {app}...", "backup_output_symlink_dir_broken": "Votre répertoire d’archivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", - "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations migrate`.", + "migrations_to_be_ran_manually": "La migration {id} doit être lancée manuellement. Veuillez aller dans Outils > Migrations dans l’interface admin, ou lancer `yunohost tools migrations run`.", "migrations_need_to_accept_disclaimer": "Pour lancer la migration {id}, vous devez accepter cet avertissement :\n---\n{disclaimer}\n---\nSi vous acceptez de lancer la migration, veuillez relancer la commande avec l’option --accept-disclaimer.", "service_description_avahi-daemon": "Vous permet d’atteindre votre serveur en utilisant « yunohost.local » sur votre réseau local", "service_description_dnsmasq": "Gère la résolution des noms de domaine (DNS)", diff --git a/locales/it.json b/locales/it.json index ab974428f..29f1db1e9 100644 --- a/locales/it.json +++ b/locales/it.json @@ -531,7 +531,7 @@ "pattern_email_forward": "Dev'essere un indirizzo mail valido, simbolo '+' accettato (es: tizio+tag@example.com)", "operation_interrupted": "L'operazione è stata interrotta manualmente?", "invalid_number": "Dev'essere un numero", - "migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations migrate`.", + "migrations_to_be_ran_manually": "Migrazione {id} dev'essere eseguita manualmente. Vai in Strumenti → Migrazioni nella pagina webadmin, o esegui `yunohost tools migrations run`.", "migrations_success_forward": "Migrazione {id} completata", "migrations_skip_migration": "Salto migrazione {id}...", "migrations_running_forward": "Eseguo migrazione {id}...", diff --git a/locales/oc.json b/locales/oc.json index 68d142f2b..07d841579 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -281,7 +281,7 @@ "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_migration_has_failed": "La migracion {id} a pas capitat, abandon. Error : {exception}", "migrations_skip_migration": "Passatge de la migracion {id}…", - "migrations_to_be_ran_manually": "La migracion {id} 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_to_be_ran_manually": "La migracion {id} deu èsser lançada manualament. Mercés d’anar a Aisinas > Migracion dins l’interfàcia admin, o lançar « yunohost tools migrations run ».", "migrations_need_to_accept_disclaimer": "Per lançar la migracion {id} , 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.", "pattern_backup_archive_name": "Deu èsser un nom de fichièr valid compausat de 30 caractèrs alfanumerics al maximum e « -_. »", "service_description_dovecot": "permet als clients de messatjariá d’accedir/recuperar los corrièls (via IMAP e POP3)", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..f5d79a667 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -641,7 +641,7 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr # # Here we use a dirty hack to run a command after the current # "yunohost tools upgrade", because the upgrade of yunohost - # will also trigger other yunohost commands (e.g. "yunohost tools migrations migrate") + # will also trigger other yunohost commands (e.g. "yunohost tools migrations run") # (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...) From 9971c1751b8340dfb3c2be957cc6089cb873d524 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 22:49:40 +0100 Subject: [PATCH 170/363] Abort postinstall if /etc/yunohost/apps ain't empty --- src/yunohost/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..da88560c3 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -249,6 +249,9 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, if os.path.isfile('/etc/yunohost/installed'): raise YunohostError('yunohost_already_installed') + if os.path.isdir("/etc/yunohost/apps") and os.listdir("/etc/yunohost/apps") != []: + raise YunohostError("It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...", raw_msg=True) + # Check password if not force_password: assert_password_is_strong_enough("admin", password) From 1fdccb7c7a0d2a603ca15b7ae445f5125d3dede4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 23:30:15 +0100 Subject: [PATCH 171/363] Forgot to rename function #oopsies --- data/actionsmap/yunohost.yml | 2 +- src/yunohost/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 1f1109dde..a4f27ee18 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1518,7 +1518,7 @@ tools: help: list only migrations already performed action: store_true - ### tools_migrations_migrate() + ### tools_migrations_run() run: action_help: Run migrations api: POST /migrations/run diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f5d79a667..bdef71cee 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -792,7 +792,7 @@ def tools_migrations_list(pending=False, done=False): return {"migrations": migrations} -def tools_migrations_migrate(targets=[], skip=False, auto=False, force_rerun=False, accept_disclaimer=False): +def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, accept_disclaimer=False): """ Perform migrations From 3e290d5c37163f09969f0dd38df144bb12824086 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 23:55:37 +0100 Subject: [PATCH 172/363] Prevent the installation of apache2 ... --- data/hooks/conf_regen/10-apt | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/data/hooks/conf_regen/10-apt b/data/hooks/conf_regen/10-apt index 09789470b..10406708d 100755 --- a/data/hooks/conf_regen/10-apt +++ b/data/hooks/conf_regen/10-apt @@ -15,6 +15,31 @@ Package: $package Pin: origin \"packages.sury.org\" Pin-Priority: -1" >> "${pending_dir}/etc/apt/preferences.d/extra_php_version" done + + echo " +# Yes ! +# This is what's preventing you from installing apache2 ! +# +# Maybe take two fucking minutes to realize that if you try to install +# apache2, this will break nginx and break the entire YunoHost ecosystem. +# on your server. +# +# So, *NO* +# DO NOT do this. +# DO NOT remove these lines. +# +# I warned you. I WARNED YOU! But did you listen to me? +# Oooooh, noooo. You knew it all, didn't you? + +Package: apache2 +Pin: release * +Pin-Priority: -1 + +Package: apache2-bin +Pin: release * +Pin-Priority: -1 +" >> "${pending_dir}/etc/apt/preferences.d/forbid_apache2" + } do_post_regen() { From 148bfdac0d2a8deb1d3f4447aa06cb118df42ca8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 23:59:22 +0100 Subject: [PATCH 173/363] Also ban bind9 --- data/hooks/conf_regen/10-apt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/10-apt b/data/hooks/conf_regen/10-apt index 10406708d..bb5caf67f 100755 --- a/data/hooks/conf_regen/10-apt +++ b/data/hooks/conf_regen/10-apt @@ -38,7 +38,15 @@ Pin-Priority: -1 Package: apache2-bin Pin: release * Pin-Priority: -1 -" >> "${pending_dir}/etc/apt/preferences.d/forbid_apache2" + +# Also yes, bind9 will conflict with dnsmasq. +# Same story than for apache2. +# Don't fucking install it. + +Package: bind9 +Pin: release * +Pin-Priority: -1 +" >> "${pending_dir}/etc/apt/preferences.d/ban_packages" } From c659216e23f2f8cf9944e5d779ecfaf0b79d2e92 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:32:27 +0100 Subject: [PATCH 174/363] Make sure tmp_script exists .. --- src/yunohost/backup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index e7904bf8d..870e48476 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -674,6 +674,7 @@ class BackupManager(): settings_dir = os.path.join(self.work_dir, 'apps', app, 'settings') logger.info(m18n.n("app_start_backup", app=app)) + tmp_script = None # This is to make sure the var exists later in the 'finally' ... try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid='admin') @@ -716,7 +717,8 @@ class BackupManager(): # Remove tmp files in all situations finally: - filesystem.rm(tmp_script, force=True) + if tmp_script: + filesystem.rm(tmp_script, force=True) filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) # From 9c11fd58e25c9ecb8cedcd140ed6ee46b210c4ee Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:37:29 +0100 Subject: [PATCH 175/363] service_regen_conf is deprecated in factor of regen_conf --- src/yunohost/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 060aca6e4..17ab93cd4 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -8,7 +8,7 @@ from collections import OrderedDict from moulinette import m18n from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf +from yunohost.regenconf import regen_conf logger = getActionLogger('yunohost.settings') @@ -325,13 +325,13 @@ def trigger_post_change_hook(setting_name, old_value, new_value): @post_change_hook("security.nginx.compatibility") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['nginx']) + regen_conf(names=['nginx']) @post_change_hook("security.ssh.compatibility") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['ssh']) + regen_conf(names=['ssh']) @post_change_hook("smtp.allow_ipv6") @@ -342,7 +342,7 @@ def reconfigure_ssh(setting_name, old_value, new_value): @post_change_hook("security.postfix.compatibility") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['postfix']) + regen_conf(names=['postfix']) @post_change_hook("pop3.enabled") @@ -364,9 +364,9 @@ def reconfigure_dovecot(setting_name, old_value, new_value): ] subprocess.call(command, env=environment) if old_value != new_value: - service_regen_conf(names=['dovecot']) + regen_conf(names=['dovecot']) else: if old_value != new_value: - service_regen_conf(names=['dovecot']) + regen_conf(names=['dovecot']) command = ['apt-get', '-y', 'remove', dovecot_package] subprocess.call(command, env=environment) From f5ab4443ff039f6fc15282ddf2be95f821832f78 Mon Sep 17 00:00:00 2001 From: Kayou Date: Fri, 22 Jan 2021 14:13:58 +0100 Subject: [PATCH 176/363] We don't need this conflict anymore because the python3-miniupnpc package is no longer in our repositorie. --- debian/control | 1 - 1 file changed, 1 deletion(-) diff --git a/debian/control b/debian/control index d48911df3..d95b17f4e 100644 --- a/debian/control +++ b/debian/control @@ -46,7 +46,6 @@ Conflicts: iptables-persistent , redis-server (>= 5:5.0.7) , fail2ban (>= 0.11) , iptables (>= 1.8.3) - , python3-miniupnpc (= 2.1-1+ynh10) Description: manageable and configured self-hosting server YunoHost aims to make self-hosting accessible to everyone. It configures an email, Web and IM server alongside a LDAP base. It also provides From 0efd8307430dfd9b62aca60be00528202cdd7967 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Fri, 22 Jan 2021 14:49:47 +0100 Subject: [PATCH 177/363] fix can ynh-admin vuejs --- 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 8b81ab932..ab6d270c7 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf.inc +++ b/data/templates/nginx/plain/yunohost_admin.conf.inc @@ -10,7 +10,7 @@ location /yunohost/admin/ { more_set_headers "Content-Security-Policy-Report-Only:"; # Short cache on handlebars templates - location ~* \.(?:ms)$ { + location ~* \.(js|css|png|jpg|jpeg|gif|ico|json|woff|woff2|ttf|eot)$ { expires 5m; add_header Cache-Control "public"; } From 6e9ab553b8006a14723db673c62d595364dc3be6 Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Fri, 22 Jan 2021 17:08:28 +0100 Subject: [PATCH 178/363] Add ynh_exec_as to official --- data/helpers.d/logging | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index dc32ecba9..e5d208a0f 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -100,6 +100,30 @@ ynh_print_err () { ynh_print_log "[Error] ${message}" >&2 } +# Execute a command as another user +# +# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] +# | arg: -u, --user= - the user that will execute the command +# | arg: -n, --command= - the command to be executed +# +# Requires YunoHost version 4.1.7 or higher. +ynh_exec_as() +{ + # Declare an array to define the options of this helper. + local legacy_args=uc + local -A args_array=( [u]=user= [c]=command= ) + local user + local command + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [[ $user = $(whoami) ]]; then + eval "$command" + else + sudo -u "$user" "$command" + fi +} + # Execute a command and print the result as an error # # usage: ynh_exec_err your_command From 25f2bdf83ac91fe699024dc8bbab8632c184eda7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 9 Oct 2020 18:11:24 +0200 Subject: [PATCH 179/363] Refactor local CA management for more consistency and simpler postinstall --- data/hooks/conf_regen/02-ssl | 122 ++++++++++++++++++----------------- src/yunohost/domain.py | 30 ++------- src/yunohost/tools.py | 30 --------- 3 files changed, 68 insertions(+), 114 deletions(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index 310a5d526..ac740f6ea 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -3,71 +3,81 @@ set -e ssl_dir="/usr/share/yunohost/yunohost-config/ssl/yunoCA" +ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem" +ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem" +ynh_key="/etc/yunohost/certs/yunohost.org/key.pem" +openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf" + +regen_local_ca() { + + domain="$1" + + echo -e "\n# Creating local certification authority with domain=$domain\n" + + # create certs and SSL directories + mkdir -p "/etc/yunohost/certs/yunohost.org" + mkdir -p "${ssl_dir}/"{ca,certs,crl,newcerts} + + pushd ${ssl_dir} + + # (Update the serial so that it's specific to this very instance) + # N.B. : the weird RANDFILE thing comes from: + # https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean + RANDFILE=.rnd openssl rand -hex 19 > serial + rm -f index.txt + touch index.txt + cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf + sed -i s/yunohost.org/${domain}/g openssl.ca.cnf + openssl req -x509 \ + -new \ + -config openssl.ca.cnf \ + -days 3650 \ + -out ca/cacert.pem \ + -keyout ca/cakey.pem \ + -nodes \ + -batch \ + -subj /CN=${domain}/O=${domain%.*} 2>&1 + + chmod 640 ca/cacert.pem + chmod 640 ca/cakey.pem + + cp ca/cacert.pem $ynh_ca + ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem + update-ca-certificates + + popd +} + + do_init_regen() { - if [[ $EUID -ne 0 ]]; then - echo "You must be root to run this script" 1>&2 - 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} - - # initialize some files - # N.B. : the weird RANDFILE thing comes from: - # https://stackoverflow.com/questions/94445/using-openssl-what-does-unable-to-write-random-state-mean - [[ -f "${ssl_dir}/serial" ]] \ - || RANDFILE=.rnd openssl rand -hex 19 > "${ssl_dir}/serial" - [[ -f "${ssl_dir}/index.txt" ]] \ - || touch "${ssl_dir}/index.txt" - - openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf" - ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem" - ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem" - ynh_key="/etc/yunohost/certs/yunohost.org/key.pem" + LOGFILE=/tmp/yunohost-ssl-init + echo "" > $LOGFILE + chown root:root $LOGFILE + chmod 640 $LOGFILE # create default certificates if [[ ! -f "$ynh_ca" ]]; 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 >>$LOGFILE 2>&1 - - cp "${ssl_dir}/ca/cacert.pem" "$ynh_ca" - ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem - update-ca-certificates + regen_local_ca yunohost.org >>$LOGFILE fi if [[ ! -f "$ynh_crt" ]]; then - echo -e "\n# Creating initial key and certificate (?)\n" >>$LOGFILE + 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 >>$LOGFILE 2>&1 + -nodes -batch &>>$LOGFILE openssl ca \ -config "$openssl_conf" \ -days 730 \ -in "${ssl_dir}/certs/yunohost_csr.pem" \ -out "${ssl_dir}/certs/yunohost_crt.pem" \ - -batch >>$LOGFILE 2>&1 + -batch &>>$LOGFILE chmod 640 "${ssl_dir}/certs/yunohost_key.pem" chmod 640 "${ssl_dir}/certs/yunohost_crt.pem" @@ -93,22 +103,16 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 - # Ensure that index.txt exists - index_txt=/usr/share/yunohost/yunohost-config/ssl/yunoCA/index.txt - [[ -f "${index_txt}" ]] || { - if [[ -f "${index_txt}.saved" ]]; then - # use saved database from 2.2 - cp "${index_txt}.saved" "${index_txt}" - elif [[ -f "${index_txt}.old" ]]; then - # ... or use the state-1 database - cp "${index_txt}.old" "${index_txt}" - else - # ... or create an empty one - touch "${index_txt}" - fi - } + current_local_ca_domain=$(openssl x509 -in $ynh_ca -text | tr ',' '\n' | grep Issuer | awk '{print $4}') + main_domain=$(cat /etc/yunohost/current_host) - # TODO: regenerate certificates if conf changed? + if [[ "$current_local_ca_domain" != "$main_domain" ]] + then + regen_local_ca $main_domain + # Idk how useful this is, but this was in the previous python code (domain.main_domain()) + ln -sf /etc/yunohost/certs/$domain/crt.pem /etc/ssl/certs/yunohost_crt.pem + ln -sf /etc/yunohost/certs/$domain/key.pem /etc/ssl/private/yunohost_key.pem + fi } FORCE=${2:-0} diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index af45d8757..6477b3943 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -30,6 +30,7 @@ from moulinette import m18n, msettings from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import write_to_file from yunohost.app import app_ssowatconf, _installed_apps, _get_app_settings, _get_conflicting_apps from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf @@ -318,36 +319,20 @@ def domain_main_domain(operation_logger, new_main_domain=None): operation_logger.start() # 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_main_domain - new_ssl_crt = "/etc/yunohost/certs/%s/crt.pem" % new_main_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): - os.remove(ssl_crt) + write_to_file('/etc/yunohost/current_host', new_main_domain) - os.symlink(new_ssl_key, ssl_key) - os.symlink(new_ssl_crt, ssl_crt) - - _set_maindomain(new_main_domain) + _set_hostname(new_main_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) raise YunohostError('main_domain_change_failed') - _set_hostname(new_main_domain) - # Generate SSOwat configuration file app_ssowatconf() # Regen configurations - try: - with open('/etc/yunohost/installed', 'r'): - regen_conf() - except IOError: - pass + if os.path.exists('/etc/yunohost/installed'): + regen_conf() logger.success(m18n.n('main_domain_changed')) @@ -385,11 +370,6 @@ def _get_maindomain(): return maindomain -def _set_maindomain(domain): - with open('/etc/yunohost/current_host', 'w') as f: - f.write(domain) - - def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): """ Internal function that will returns a data structure containing the needed diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..126158606 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -315,37 +315,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, os.system('chmod 644 /etc/ssowat/conf.json.persistent') - # Create SSL CA - regen_conf(['ssl'], force=True) - ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - # (Update the serial so that it's specific to this very instance) - os.system("openssl rand -hex 19 > %s/serial" % ssl_dir) - commands = [ - '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 -subj /CN=%s/O=%s' % (ssl_dir, ssl_dir, ssl_dir, domain, os.path.splitext(domain)[0]), - 'cp %s/ca/cacert.pem /etc/ssl/certs/ca-yunohost_crt.pem' % ssl_dir, - 'update-ca-certificates' - ] - - 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 YunohostError('yunohost_ca_creation_failed') - else: - logger.debug(out) - - logger.success(m18n.n('yunohost_ca_creation_success')) - # New domain config - regen_conf(['nsswitch'], force=True) domain_add(domain, dyndns) domain_main_domain(domain) From f755259790d878805a8536bb3bc9c2e17c0a0f1e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 9 Oct 2020 18:18:44 +0200 Subject: [PATCH 180/363] Initialize folders during .deb install instead of regen conf --- data/hooks/conf_regen/01-yunohost | 29 +++++++++++++++++++++++++++-- src/yunohost/tools.py | 27 --------------------------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 6ac61d07a..9da2d91ca 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -27,6 +27,29 @@ do_init_regen() { # allow users to access /media directory [[ -d /etc/skel/media ]] \ || (mkdir -p /media && ln -s /media /etc/skel/media) + + # Cert folders + mkdir -p /etc/yunohost/certs + chown -R root:ssl-cert /etc/yunohost/certs + chmod 750 /etc/yunohost/certs + + # App folders + mkdir -p /etc/yunohost/apps + chmod 700 /etc/yunohost/apps + mkdir -p /home/yunohost.app + chmod 755 /home/yunohost.app + + # Backup folders + mkdir -p /home/yunohost.backup/archives + chmod 750 /home/yunohost.backup/archives + chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists + + # Empty ssowat json persistent conf + echo "{}" > '/etc/ssowat/conf.json.persistent' + chmod 644 /etc/ssowat/conf.json.persistent + chown root:root /etc/ssowat/conf.json.persistent + + mkdir -p /var/cache/yunohost/repo } do_pre_regen() { @@ -67,7 +90,7 @@ EOF # (this make sure that the hash is null / file is flagged as to-delete) mkdir -p $pending_dir/etc/etckeeper touch $pending_dir/etc/etckeeper/etckeeper.conf - + # Skip ntp if inside a container (inspired from the conf of systemd-timesyncd) mkdir -p ${pending_dir}/etc/systemd/system/ntp.service.d/ echo " @@ -75,7 +98,7 @@ EOF ConditionCapability=CAP_SYS_TIME ConditionVirtualization=!container " > ${pending_dir}/etc/systemd/system/ntp.service.d/ynh-override.conf - + # Make nftable conflict with yunohost-firewall mkdir -p ${pending_dir}/etc/systemd/system/nftables.service.d/ cat > ${pending_dir}/etc/systemd/system/nftables.service.d/ynh-override.conf << EOF @@ -94,6 +117,8 @@ do_post_regen() { # Enfore permissions # ###################### + chown admin:root /home/yunohost.backup/archives + # Certs # We do this with find because there could be a lot of them... chown -R root:ssl-cert /etc/yunohost/certs diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..348266ccb 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -294,27 +294,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # TODO: Improve this part by integrate ldapinit into conf_regen hook tools_ldapinit() - # Create required folders - folders_to_create = [ - '/etc/yunohost/apps', - '/etc/yunohost/certs', - '/var/cache/yunohost/repo', - '/home/yunohost.backup', - '/home/yunohost.app' - ] - - for folder in [x for x in folders_to_create if not os.path.exists(x)]: - os.makedirs(folder) - - # Change folders permissions - os.system('chmod 755 /home/yunohost.app') - - # Init ssowat's conf.json.persistent - if not os.path.exists('/etc/ssowat/conf.json.persistent'): - write_to_json('/etc/ssowat/conf.json.persistent', {}) - - os.system('chmod 644 /etc/ssowat/conf.json.persistent') - # Create SSL CA regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' @@ -366,12 +345,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, except Exception as e: logger.warning(str(e)) - # Create the archive directory (makes it easier for people to upload backup - # archives, otherwise it's only created after running `yunohost backup - # create` once. - from yunohost.backup import _create_archive_dir - _create_archive_dir() - # Init migrations (skip them, no need to run them on a fresh system) _skip_all_migrations() From ed35915e7f800af15ccdf557c201d8f8cc20378d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 11 Oct 2020 17:05:08 +0200 Subject: [PATCH 181/363] Move ldap initialization before the postinstall --- data/hooks/conf_regen/06-slapd | 35 ++++++++++++++++++++++++++++--- data/hooks/conf_regen/09-nslcd | 10 ++++++++- data/hooks/conf_regen/46-nsswitch | 10 ++++++++- debian/postinst | 2 ++ src/yunohost/__init__.py | 1 + src/yunohost/tools.py | 8 ------- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index 9f808b58e..695a31fd6 100755 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -13,7 +13,31 @@ do_init_regen() { do_pre_regen "" systemctl daemon-reload - + + systemctl restart slapd + + # Drop current existing slapd data + + rm -rf /var/backups/*.ldapdb + rm -rf /var/backups/slapd-* + +debconf-set-selections << EOF +slapd slapd/password1 password yunohost +slapd slapd/password2 password yunohost +slapd slapd/domain string yunohost.org +slapd shared/organization string yunohost.org +slapd slapd/allow_ldap_v2 boolean false +slapd slapd/invalid_config boolean true +slapd slapd/backend select MDB +slapd slapd/move_old_database boolean true +slapd slapd/no_configuration boolean false +slapd slapd/purge_database boolean false +EOF + + DEBIAN_FRONTEND=noninteractive dpkg-reconfigure slapd -u + + # Regen conf + _regenerate_slapd_conf # Enforce permissions @@ -21,7 +45,11 @@ do_init_regen() { chown -R openldap:openldap /etc/ldap/schema/ usermod -aG ssl-cert openldap - service slapd restart + systemctl restart slapd + + # (Re-)init data according to ldap_scheme.yaml + + yunohost tools shell -c "from yunohost.tools import tools_ldapinit; tools_ldapinit()" } _regenerate_slapd_conf() { @@ -31,7 +59,8 @@ _regenerate_slapd_conf() { # so we use a temporary directory slapd_new.d rm -Rf /etc/ldap/slapd_new.d mkdir /etc/ldap/slapd_new.d - slapadd -n0 -l /etc/ldap/slapd.ldif -F /etc/ldap/slapd_new.d/ 2>&1 + slapadd -n0 -l /etc/ldap/slapd.ldif -F /etc/ldap/slapd_new.d/ 2>&1 \ + | grep -v "none elapsed\|Closing DB" || true # Actual validation (-Q is for quiet, -u is for dry-run) slaptest -Q -u -F /etc/ldap/slapd_new.d diff --git a/data/hooks/conf_regen/09-nslcd b/data/hooks/conf_regen/09-nslcd index 7090fc758..2e911b328 100755 --- a/data/hooks/conf_regen/09-nslcd +++ b/data/hooks/conf_regen/09-nslcd @@ -2,6 +2,11 @@ set -e +do_init_regen() { + do_pre_regen "" + systemctl restart nslcd +} + do_pre_regen() { pending_dir=$1 @@ -14,7 +19,7 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || service nslcd restart + || systemctl restart nslcd } FORCE=${2:-0} @@ -27,6 +32,9 @@ case "$1" in post) do_post_regen $4 ;; + init) + do_init_regen + ;; *) echo "hook called with unknown argument \`$1'" >&2 exit 1 diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch index fa9b07511..e6d998094 100755 --- a/data/hooks/conf_regen/46-nsswitch +++ b/data/hooks/conf_regen/46-nsswitch @@ -2,6 +2,11 @@ set -e +do_init_regen() { + do_pre_regen "" + systemctl restart unscd +} + do_pre_regen() { pending_dir=$1 @@ -14,7 +19,7 @@ do_post_regen() { regen_conf_files=$1 [[ -z "$regen_conf_files" ]] \ - || service unscd restart + || systemctl restart unscd } FORCE=${2:-0} @@ -27,6 +32,9 @@ case "$1" in post) do_post_regen $4 ;; + init) + do_init_regen + ;; *) echo "hook called with unknown argument \`$1'" >&2 exit 1 diff --git a/debian/postinst b/debian/postinst index 4b43b2506..e5f133fe6 100644 --- a/debian/postinst +++ b/debian/postinst @@ -8,6 +8,8 @@ do_configure() { if [ ! -f /etc/yunohost/installed ]; then bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init bash /usr/share/yunohost/hooks/conf_regen/02-ssl init + bash /usr/share/yunohost/hooks/conf_regen/09-nslcd init + bash /usr/share/yunohost/hooks/conf_regen/46-nsswitch init bash /usr/share/yunohost/hooks/conf_regen/06-slapd init bash /usr/share/yunohost/hooks/conf_regen/15-nginx init else diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index 76449a7e4..a21dad2b9 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -53,6 +53,7 @@ def check_command_is_valid_before_postinstall(args): allowed_if_not_postinstalled = ['tools postinstall', 'tools versions', + 'tools shell', 'backup list', 'backup restore', 'log display'] diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f01f6adb8..ce20e03a6 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -59,8 +59,6 @@ def tools_versions(): def tools_ldapinit(): """ YunoHost LDAP initialization - - """ with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f: @@ -288,12 +286,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, operation_logger.start() logger.info(m18n.n('yunohost_installing')) - regen_conf(['nslcd', 'nsswitch'], force=True) - - # Initialize LDAP for YunoHost - # TODO: Improve this part by integrate ldapinit into conf_regen hook - tools_ldapinit() - # Create required folders folders_to_create = [ '/etc/yunohost/apps', From 0606df529b78296d004908f22e842eb70259575e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 23 Jan 2021 00:08:50 +0100 Subject: [PATCH 182/363] Also add a check on debian/postinst because we're about to move a bunch of init step to debian/postinst instead of 'yunohost tools postintall' --- debian/postinst | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/debian/postinst b/debian/postinst index 4b43b2506..fd2a24b9e 100644 --- a/debian/postinst +++ b/debian/postinst @@ -6,10 +6,18 @@ do_configure() { rm -rf /var/cache/moulinette/* if [ ! -f /etc/yunohost/installed ]; then - bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init - bash /usr/share/yunohost/hooks/conf_regen/02-ssl init - bash /usr/share/yunohost/hooks/conf_regen/06-slapd init - bash /usr/share/yunohost/hooks/conf_regen/15-nginx init + + # If apps/ is not empty, we're probably already installed in the past and + # something funky happened ... + if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* 2>/dev/null + then + echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..." + else + bash /usr/share/yunohost/hooks/conf_regen/01-yunohost init + bash /usr/share/yunohost/hooks/conf_regen/02-ssl init + bash /usr/share/yunohost/hooks/conf_regen/06-slapd init + bash /usr/share/yunohost/hooks/conf_regen/15-nginx init + fi else echo "Regenerating configuration, this might take a while..." yunohost tools regen-conf --output-as none From 4a302dc7865fb44c6fb77d80c5054b8cc10d1871 Mon Sep 17 00:00:00 2001 From: Kayou Date: Sat, 23 Jan 2021 00:21:02 +0100 Subject: [PATCH 183/363] add auto-format-code (#1142) * add auto-format-code * add github remote * add GITHUB_TOKEN to the remove url * select Yunohost:dev * force push as it's only a code format * pull before running black * working on a clean directory * pull before push? /o\ * do not clone single branch * a last one? * only on dev branch --- .gitlab/ci/lint.gitlab-ci.yml | 22 +++++++++++++++++++++- tox.ini | 7 ++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index dabe33d62..94be52f7d 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -24,4 +24,24 @@ format-check: needs: [] allow_failure: true script: - - tox -e py37-black + - tox -e py37-black-check + +format-run: + stage: lint + image: "before-install" + needs: [] + before_script: + - apt-get update -y && apt-get install git hub -y + - git config --global user.email "yunohost@yunohost.org" + - git config --global user.name "$GITHUB_USER" + - hub clone --branch ${CI_COMMIT_REF_NAME} "https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/yunohost.git" github_repo + - cd github_repo + script: + # checkout or create and checkout the branch + - hub checkout "ci-format-${CI_COMMIT_REF_NAME}" || hub checkout -b "ci-format-${CI_COMMIT_REF_NAME}" + - tox -e py37-black-run + - hub commit -am "[CI] Format code" || true + - hub pull-request -m "[CI] Format code" -b Yunohost:dev -p || true # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd + only: + refs: + - dev \ No newline at end of file diff --git a/tox.ini b/tox.ini index 36134e85a..7607c4a41 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,13 @@ [tox] -envlist = py37-{lint,invalidcode},py37-black +envlist = py37-{lint,invalidcode},py37-black-{run,check} [testenv] skip_install=True deps = py37-{lint,invalidcode}: flake8 - py37-black: black + py37-black-{run,check}: black commands = py37-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F - py37-black: black --check --diff src doc data tests + py37-black-check: black --check --diff src doc data tests + py37-black-run: black src doc data tests From 46138e9e730f2bc201a76d669921da0cda261a19 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 23 Jan 2021 01:27:14 +0100 Subject: [PATCH 184/363] We need that file for the first domain_add during postinstall --- data/hooks/conf_regen/02-ssl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index ac740f6ea..0cb38df7b 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -90,6 +90,8 @@ do_init_regen() { chown -R root:ssl-cert /etc/yunohost/certs/yunohost.org/ chmod o-rwx /etc/yunohost/certs/yunohost.org/ + + install -D -m 644 $openssl_conf "${ssl_dir}/openssl.cnf" } do_pre_regen() { From 68cc952a895d174d2b3d964eeb0e89a287c34b33 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 23 Jan 2021 01:30:20 +0100 Subject: [PATCH 185/363] Remove stale strings --- locales/en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index dbaee0bdf..9275b87e2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -624,8 +624,6 @@ "user_update_failed": "Could not update user {user}: {error}", "user_updated": "User info changed", "yunohost_already_installed": "YunoHost is already installed", - "yunohost_ca_creation_failed": "Could not create certificate authority", - "yunohost_ca_creation_success": "Local certification authority created.", "yunohost_configured": "YunoHost is now configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'", From c023b177fefa6aa5d1b508bee6490e10b1e84b67 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 31 Dec 2020 19:16:44 +0100 Subject: [PATCH 186/363] Normalize conf template handling for nginx, php-fpm, systemd and fail2ban using ynh_add_config --- data/helpers.d/fail2ban | 65 ++++++++------------------- data/helpers.d/nginx | 59 +++++-------------------- data/helpers.d/php | 97 ++++++++++++++++++----------------------- data/helpers.d/systemd | 44 +++---------------- data/helpers.d/utils | 1 + 5 files changed, 76 insertions(+), 190 deletions(-) diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index f9bdd89b2..da090d2f9 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -16,11 +16,8 @@ # | for example : 'var_1 var_2 ...' # # This will use a template in ../conf/f2b_jail.conf and ../conf/f2b_filter.conf -# __APP__ by $app -# -# You can dynamically replace others variables by example : -# __VAR_1__ by $var_1 -# __VAR_2__ by $var_2 +# See the documentation of ynh_add_config for a description of the template +# format and how placeholders are replaced with actual variables. # # Generally your template will look like that by example (for synapse): # @@ -64,73 +61,45 @@ # Requires YunoHost version 3.5.0 or higher. ynh_add_fail2ban_config () { # Declare an array to define the options of this helper. - local legacy_args=lrmptv - local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=) + local legacy_args=lrmpt + local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template) local logpath local failregex local max_retry local ports - local others_var local use_template # Manage arguments with getopts ynh_handle_getopts_args "$@" max_retry=${max_retry:-3} ports=${ports:-http,https} - others_var=${others_var:-} use_template="${use_template:-0}" - finalfail2banjailconf="/etc/fail2ban/jail.d/$app.conf" - finalfail2banfilterconf="/etc/fail2ban/filter.d/$app.conf" - ynh_backup_if_checksum_is_different "$finalfail2banjailconf" - ynh_backup_if_checksum_is_different "$finalfail2banfilterconf" - - if [ $use_template -eq 1 ] + if [ $use_template -ne 1 ] then - # Usage 2, templates - cp ../conf/f2b_jail.conf $finalfail2banjailconf - cp ../conf/f2b_filter.conf $finalfail2banfilterconf - - if [ -n "${app:-}" ] - then - ynh_replace_string "__APP__" "$app" "$finalfail2banjailconf" - ynh_replace_string "__APP__" "$app" "$finalfail2banfilterconf" - fi - - # Replace all other variable given as arguments - for var_to_replace in $others_var - do - # ${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 --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banjailconf" - ynh_replace_string --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalfail2banfilterconf" - done - - else # Usage 1, no template. Build a config file from scratch. test -n "$logpath" || ynh_die "ynh_add_fail2ban_config expects a logfile path as first argument and received nothing." test -n "$failregex" || ynh_die "ynh_add_fail2ban_config expects a failure regex as second argument and received nothing." - tee $finalfail2banjailconf < ../conf/f2b_jail.conf - tee $finalfail2banfilterconf < ../conf/f2b_filter.conf fi - # Common to usage 1 and 2. - ynh_store_file_checksum "$finalfail2banjailconf" - ynh_store_file_checksum "$finalfail2banfilterconf" + ynh_add_config --template="../conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" + ynh_add_config --template="../conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd diff --git a/data/helpers.d/nginx b/data/helpers.d/nginx index cd4380f16..050a24117 100644 --- a/data/helpers.d/nginx +++ b/data/helpers.d/nginx @@ -2,60 +2,25 @@ # Create a dedicated nginx config # -# usage: ynh_add_nginx_config "list of others variables to replace" -# -# | arg: list - (Optional) list of others variables to replace separated by spaces. For example : 'path_2 port_2 ...' +# usage: ynh_add_nginx_config # # This will use a template in ../conf/nginx.conf -# __PATH__ by $path_url -# __DOMAIN__ by $domain -# __PORT__ by $port -# __NAME__ by $app -# __FINALPATH__ by $final_path -# __PHPVERSION__ by $YNH_PHP_VERSION ($YNH_PHP_VERSION is either the default php version or the version defined for the app) +# See the documentation of ynh_add_config for a description of the template +# format and how placeholders are replaced with actual variables. # -# And dynamic variables (from the last example) : -# __PATH_2__ by $path_2 -# __PORT_2__ by $port_2 +# Additionally, ynh_add_nginx_config will replace: +# - #sub_path_only by empty string if path_url is not '/' +# - #root_path_only by empty string if path_url *is* '/' +# +# This allows to enable/disable specific behaviors dependenging on the install +# location # # Requires YunoHost version 2.7.2 or higher. -# Requires YunoHost version 2.7.13 or higher for dynamic variables ynh_add_nginx_config () { - finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" - local others_var=${1:-} - ynh_backup_if_checksum_is_different --file="$finalnginxconf" - 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 - # path_url_slash_less is path_url, or a blank value if path_url is only '/' - local path_url_slash_less=${path_url%/} - ynh_replace_string --match_string="__PATH__/" --replace_string="$path_url_slash_less/" --target_file="$finalnginxconf" - ynh_replace_string --match_string="__PATH__" --replace_string="$path_url" --target_file="$finalnginxconf" - fi - if test -n "${domain:-}"; then - ynh_replace_string --match_string="__DOMAIN__" --replace_string="$domain" --target_file="$finalnginxconf" - fi - if test -n "${port:-}"; then - ynh_replace_string --match_string="__PORT__" --replace_string="$port" --target_file="$finalnginxconf" - fi - if test -n "${app:-}"; then - ynh_replace_string --match_string="__NAME__" --replace_string="$app" --target_file="$finalnginxconf" - fi - if test -n "${final_path:-}"; then - ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalnginxconf" - fi - ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$YNH_PHP_VERSION" --target_file="$finalnginxconf" + local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" - # Replace all other variable given as arguments - for var_to_replace in $others_var - do - # ${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 --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalnginxconf" - done + ynh_add_config --template="../conf/nginx.conf" --destination="$finalnginxconf" if [ "${path_url:-}" != "/" ] then @@ -64,8 +29,6 @@ ynh_add_nginx_config () { ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf" fi - ynh_store_file_checksum --file="$finalnginxconf" - ynh_systemd_action --service_name=nginx --action=reload } diff --git a/data/helpers.d/php b/data/helpers.d/php index 0dd589956..95151b45b 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -132,7 +132,6 @@ ynh_add_fpm_config () { ynh_app_setting_set --app=$app --key=fpm_service --value="$fpm_service" ynh_app_setting_set --app=$app --key=fpm_dedicated_service --value="$dedicated_service" ynh_app_setting_set --app=$app --key=phpversion --value=$phpversion - finalphpconf="$fpm_config_dir/pool.d/$app.conf" # Migrate from mutual PHP service to dedicated one. if [ $dedicated_service -eq 1 ] @@ -151,8 +150,6 @@ ynh_add_fpm_config () { fi fi - ynh_backup_if_checksum_is_different --file="$finalphpconf" - if [ $use_template -eq 1 ] then # Usage 1, use the template in conf/php-fpm.conf @@ -162,12 +159,6 @@ ynh_add_fpm_config () { fi # Make sure now that the template indeed exists [ -e "$phpfpm_path" ] || ynh_die --message="Unable to find template to configure PHP-FPM." - cp "$phpfpm_path" "$finalphpconf" - ynh_replace_string --match_string="__NAMETOCHANGE__" --replace_string="$app" --target_file="$finalphpconf" - ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalphpconf" - ynh_replace_string --match_string="__USER__" --replace_string="$app" --target_file="$finalphpconf" - ynh_replace_string --match_string="__PHPVERSION__" --replace_string="$phpversion" --target_file="$finalphpconf" - else # Usage 2, generate a PHP-FPM config file with ynh_get_scalable_phpfpm @@ -178,82 +169,78 @@ ynh_add_fpm_config () { # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint - # Copy the default file - cp "/etc/php/$phpversion/fpm/pool.d/www.conf" "$finalphpconf" + local phpfpm_path="../conf/php-fpm.conf" + echo " +[__APP__] - # Replace standard variables into the default file - ynh_replace_string --match_string="^\[www\]" --replace_string="[$app]" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*listen = .*" --replace_string="listen = /var/run/php/php$phpversion-fpm-$app.sock" --target_file="$finalphpconf" - ynh_replace_string --match_string="^user = .*" --replace_string="user = $app" --target_file="$finalphpconf" - ynh_replace_string --match_string="^group = .*" --replace_string="group = $app" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*chdir = .*" --replace_string="chdir = $final_path" --target_file="$finalphpconf" +user = __APP__ +group = __APP__ + +chdir = __FINALPATH__ + +listen = /var/run/php/php__PHPVERSION__-fpm-__APP__.sock +listen.owner = www-data +listen.group = www-data + +pm = __PHP_PM__ +pm.max_children = __PHP_MAX_CHILDREN__ +pm.max_requests = 500 +request_terminate_timeout = 1d +" > $phpfpm_path - # Configure FPM children - ynh_replace_string --match_string=".*pm = .*" --replace_string="pm = $php_pm" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*pm.max_children = .*" --replace_string="pm.max_children = $php_max_children" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*pm.max_requests = .*" --replace_string="pm.max_requests = 500" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*request_terminate_timeout = .*" --replace_string="request_terminate_timeout = 1d" --target_file="$finalphpconf" if [ "$php_pm" = "dynamic" ] then - ynh_replace_string --match_string=".*pm.start_servers = .*" --replace_string="pm.start_servers = $php_start_servers" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*pm.min_spare_servers = .*" --replace_string="pm.min_spare_servers = $php_min_spare_servers" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*pm.max_spare_servers = .*" --replace_string="pm.max_spare_servers = $php_max_spare_servers" --target_file="$finalphpconf" + echo " +pm.start_servers = __PHP_START_SERVERS__ +pm.min_spare_servers = __PHP_MIN_SPARE_SERVERS__ +pm.max_spare_servers = __PHP_MAX_SPARE_SERVERS__ +" >> $phpfpm_path + elif [ "$php_pm" = "ondemand" ] then - ynh_replace_string --match_string=".*pm.process_idle_timeout = .*" --replace_string="pm.process_idle_timeout = 10s" --target_file="$finalphpconf" - fi - - # Comment unused parameters - if [ "$php_pm" != "dynamic" ] - then - ynh_replace_string --match_string=".*\(pm.start_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*\(pm.min_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf" - ynh_replace_string --match_string=".*\(pm.max_spare_servers = .*\)" --replace_string=";\1" --target_file="$finalphpconf" - fi - if [ "$php_pm" != "ondemand" ] - then - ynh_replace_string --match_string=".*\(pm.process_idle_timeout = .*\)" --replace_string=";\1" --target_file="$finalphpconf" + echo " +pm.process_idle_timeout = 10s +" >> $phpfpm_path fi # Concatene the extra config. if [ -e ../conf/extra_php-fpm.conf ]; then - cat ../conf/extra_php-fpm.conf >> "$finalphpconf" + cat ../conf/extra_php-fpm.conf >> "$phpfpm_path" fi fi - chown root: "$finalphpconf" - ynh_store_file_checksum --file="$finalphpconf" + local finalphpconf="$fpm_config_dir/pool.d/$app.conf" + ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" if [ -e "../conf/php-fpm.ini" ] then ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." - finalphpini="$fpm_config_dir/conf.d/20-$app.ini" - ynh_backup_if_checksum_is_different "$finalphpini" - cp ../conf/php-fpm.ini "$finalphpini" - chown root: "$finalphpini" - ynh_store_file_checksum "$finalphpini" + ynh_add_config --template="../conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" fi if [ $dedicated_service -eq 1 ] then # Create a dedicated php-fpm.conf for the service local globalphpconf=$fpm_config_dir/php-fpm-$app.conf - cp /etc/php/${phpversion}/fpm/php-fpm.conf $globalphpconf - ynh_replace_string --match_string="^[; ]*pid *=.*" --replace_string="pid = /run/php/php${phpversion}-fpm-$app.pid" --target_file="$globalphpconf" - ynh_replace_string --match_string="^[; ]*error_log *=.*" --replace_string="error_log = /var/log/php/fpm-php.$app.log" --target_file="$globalphpconf" - ynh_replace_string --match_string="^[; ]*syslog.ident *=.*" --replace_string="syslog.ident = php-fpm-$app" --target_file="$globalphpconf" - ynh_replace_string --match_string="^[; ]*include *=.*" --replace_string="include = $finalphpconf" --target_file="$globalphpconf" +echo "[global] +pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid +error_log = /var/log/php/fpm-php.__APP__.log +syslog.ident = php-fpm-__APP__ +include = __FINALPHPCONF__ +" > ../conf/php-fpm-$app.conf + + ynh_add_config --template="../config/php-fpm-$app.conf" --destination="$globalphpconf" # Create a config for a dedicated PHP-FPM service for the app echo "[Unit] -Description=PHP $phpversion FastCGI Process Manager for $app +Description=PHP __PHPVERSION__ FastCGI Process Manager for __APP__ After=network.target -[Service] +[Service] Type=notify -PIDFile=/run/php/php${phpversion}-fpm-$app.pid -ExecStart=/usr/sbin/php-fpm$phpversion --nodaemonize --fpm-config $globalphpconf +PIDFile=/run/php/php__PHPVERSION__-fpm-__APP__.pid +ExecStart=/usr/sbin/php-fpm__PHPVERSION__ --nodaemonize --fpm-config __GLOBALPHPCONF__ ExecReload=/bin/kill -USR2 \$MAINPID [Install] diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index ff1b9587c..ad5ab95fb 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -3,61 +3,27 @@ # Create a dedicated systemd config # # usage: ynh_add_systemd_config [--service=service] [--template=template] -# usage: ynh_add_systemd_config [--service=service] [--template=template] [--others_var="list of others variables to replace"] # | arg: -s, --service= - Service name (optionnal, $app by default) # | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template) -# | arg: -v, --others_var= - List of others variables to replace separated by a space. For example: 'var_1 var_2 ...' # # 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 -# __FINALPATH__ by $final_path -# -# And dynamic variables (from the last example) : -# __VAR_1__ by $var_1 -# __VAR_2__ by $var_2 +# See the documentation of ynh_add_config for a description of the template +# format and how placeholders are replaced with actual variables. # # Requires YunoHost version 2.7.11 or higher. ynh_add_systemd_config () { # Declare an array to define the options of this helper. - local legacy_args=stv - local -A args_array=( [s]=service= [t]=template= [v]=others_var= ) + local legacy_args=st + local -A args_array=( [s]=service= [t]=template=) local service local template - local others_var # Manage arguments with getopts ynh_handle_getopts_args "$@" local service="${service:-$app}" local template="${template:-systemd.service}" - others_var="${others_var:-}" - finalsystemdconf="/etc/systemd/system/$service.service" - ynh_backup_if_checksum_is_different --file="$finalsystemdconf" - cp ../conf/$template "$finalsystemdconf" + ynh_add_config --template="../conf/$template" --destination="/etc/systemd/system/$service.service" - # 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 [ -n "${final_path:-}" ]; then - ynh_replace_string --match_string="__FINALPATH__" --replace_string="$final_path" --target_file="$finalsystemdconf" - fi - if [ -n "${app:-}" ]; then - ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$finalsystemdconf" - fi - - # Replace all other variables given as arguments - for var_to_replace in $others_var - do - # ${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 --match_string="__${var_to_replace^^}__" --replace_string="${!var_to_replace}" --target_file="$finalsystemdconf" - done - - ynh_store_file_checksum --file="$finalsystemdconf" - - chown root: "$finalsystemdconf" systemctl enable $service --quiet systemctl daemon-reload } diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 78b54f19e..13f84424e 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -322,6 +322,7 @@ ynh_add_config () { ynh_backup_if_checksum_is_different --file="$destination" cp "$template_path" "$destination" + chown root: "$destination" ynh_replace_vars --file="$destination" From 72822ce9873cf713f451a8f123e9f9ee9e6c4af0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 23 Jan 2021 00:15:35 +0100 Subject: [PATCH 187/363] Replace #sub_path_only and #root_path_only in the template *before* calling ynh_add_config, otherwise the it ain't in the checksum computation --- data/helpers.d/nginx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/nginx b/data/helpers.d/nginx index 050a24117..f7157cd8d 100644 --- a/data/helpers.d/nginx +++ b/data/helpers.d/nginx @@ -20,15 +20,16 @@ ynh_add_nginx_config () { local finalnginxconf="/etc/nginx/conf.d/$domain.d/$app.conf" - ynh_add_config --template="../conf/nginx.conf" --destination="$finalnginxconf" - if [ "${path_url:-}" != "/" ] then - ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$finalnginxconf" + ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="../conf/nginx.conf" else - ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$finalnginxconf" + ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="../conf/nginx.conf" fi + ynh_add_config --template="../conf/nginx.conf" --destination="$finalnginxconf" + + ynh_systemd_action --service_name=nginx --action=reload } From 2c73c50cce2b046cb6a62846b60d933de788750c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 24 Jan 2021 05:05:28 +0100 Subject: [PATCH 188/363] Unused import --- 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 348266ccb..ad0f9b060 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -33,7 +33,7 @@ from importlib import import_module from moulinette import msignals, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output, call_async_output -from moulinette.utils.filesystem import write_to_json, read_yaml, write_to_yaml +from moulinette.utils.filesystem import read_yaml, write_to_yaml from yunohost.app import _update_apps_catalog, app_info, app_upgrade, _initialize_apps_catalog_system from yunohost.domain import domain_add From 8430e4f1971ce9e7f22ed7c230fd637e27b68b06 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 25 Jan 2021 03:57:51 +0100 Subject: [PATCH 189/363] Misc issues with yunoprompt --- bin/yunoprompt | 15 +++++++-------- data/other/yunoprompt.service | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 252e5a1a4..645dd4ab1 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -6,7 +6,7 @@ x509_fingerprint=$(openssl x509 -in /etc/yunohost/certs/yunohost.org/crt.pem -n # Fetch SSH fingerprints i=0 -for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do +for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do output=$(ssh-keygen -l -f $key) fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)" i=$(($i + 1)) @@ -43,22 +43,21 @@ LOGO_AND_FINGERPRINTS=$(cat << EOF $LOGO - IP: ${local_ip} - X509 fingerprint: ${x509_fingerprint} + Local IP: ${local_ip:-(no ip detected?)} + Local SSL CA X509 fingerprint: + ${x509_fingerprint} SSH fingerprints: ${fingerprint[0]} ${fingerprint[1]} ${fingerprint[2]} - ${fingerprint[3]} - ${fingerprint[4]} EOF ) -if [[ -f /etc/yunohost/installed ]] +echo "$LOGO_AND_FINGERPRINTS" > /etc/issue + +if [[ ! -f /etc/yunohost/installed ]] then - echo "$LOGO_AND_FINGERPRINTS" > /etc/issue -else chvt 2 # Formatting diff --git a/data/other/yunoprompt.service b/data/other/yunoprompt.service index 3c4df50f9..effb69590 100644 --- a/data/other/yunoprompt.service +++ b/data/other/yunoprompt.service @@ -1,6 +1,7 @@ [Unit] Description=YunoHost boot prompt After=getty@tty2.service +After=network.target [Service] Type=simple From 903d4d188139b7764a9fd3f4a7b56c981c49799e Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 21 Dec 2020 05:00:45 +0100 Subject: [PATCH 190/363] [fix] If uid is less than 1000 nsswitch ignore it --- 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 9fe8c3176..d00f1f0f2 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -171,7 +171,7 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default - uid = str(random.randint(200, 65000)) + uid = str(random.randint(1000, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP From dccc3526e42f59385dfb9497cca7adb34b3cc5c6 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 21 Dec 2020 05:02:22 +0100 Subject: [PATCH 191/363] [fix] If uid is less than 1001 nsswitch ignore it --- 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 d00f1f0f2..3234ece32 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -171,7 +171,7 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default - uid = str(random.randint(1000, 65000)) + uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP From ba3705bbc66e186460c08d06618ed7a0d1831e91 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 25 Jan 2021 17:51:17 +0100 Subject: [PATCH 192/363] Recommend yunohost.local in yunoprompt --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 645dd4ab1..be46fc9ab 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -72,7 +72,7 @@ be asked for : - the administration password. You can perform this step : - - from your web browser, by accessing : ${local_ip} + - from your web browser, by accessing : https://yunohost.local/ or ${local_ip} - or in this terminal by answering 'yes' to the following question If this is your first time with YunoHost, it is strongly recommended to take From a3f3f891e48a3dbf02bca891a6ef5425ebc9040c Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 26 Jan 2021 20:05:37 +0100 Subject: [PATCH 193/363] fix upnp closing port --- data/templates/yunohost/firewall.yml | 2 ++ src/yunohost/firewall.py | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/data/templates/yunohost/firewall.yml b/data/templates/yunohost/firewall.yml index 835a82519..64c6b9326 100644 --- a/data/templates/yunohost/firewall.yml +++ b/data/templates/yunohost/firewall.yml @@ -2,6 +2,8 @@ uPnP: enabled: false TCP: [22, 25, 80, 443, 587, 993, 5222, 5269] UDP: [] + TCP_TO_CLOSE: [] + UDP_TO_CLOSE: [] ipv4: TCP: [22, 25, 53, 80, 443, 587, 993, 5222, 5269] UDP: [53, 5353] diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index dbf87a7b5..8c5272b69 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -82,6 +82,8 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, # Add port forwarding with UPnP if not no_upnp and port not in firewall['uPnP'][p]: firewall['uPnP'][p].append(port) + if firewall['uPnP'][p + "_TO_CLOSE"] and port in firewall['uPnP'][p + "_TO_CLOSE"]: + firewall['uPnP'][p + "_TO_CLOSE"].remove(port) # Update and reload firewall _update_firewall_file(firewall) @@ -139,6 +141,9 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, # Remove port forwarding with UPnP if upnp and port in firewall['uPnP'][p]: firewall['uPnP'][p].remove(port) + if not firewall['uPnP'][p + "_TO_CLOSE"]: + firewall['uPnP'][p + "_TO_CLOSE"] = [] + firewall['uPnP'][p + "_TO_CLOSE"].append(port) # Update and reload firewall _update_firewall_file(firewall) @@ -356,6 +361,16 @@ def firewall_upnp(action='status', no_refresh=False): else: # Iterate over ports for protocol in ['TCP', 'UDP']: + if firewall['uPnP'][protocol + "_TO_CLOSE"]: + for port in firewall['uPnP'][protocol + "_TO_CLOSE"]: + # Clean the mapping of this port + if upnpc.getspecificportmapping(port, protocol): + try: + upnpc.deleteportmapping(port, protocol) + except: + pass + firewall['uPnP'][protocol + "_TO_CLOSE"] = [] + for port in firewall['uPnP'][protocol]: # Clean the mapping of this port if upnpc.getspecificportmapping(port, protocol): @@ -373,15 +388,14 @@ def firewall_upnp(action='status', no_refresh=False): logger.debug('unable to add port %d using UPnP', port, exc_info=1) enabled = False + + _update_firewall_file(firewall) if enabled != firewall['uPnP']['enabled']: firewall = firewall_list(raw=True) 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: - yaml.safe_dump(firewall, f, default_flow_style=False) + _update_firewall_file(firewall) if not no_refresh: # Display success message if needed From 0c382420d377ec5532a0007b5aaf8846f81de506 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 26 Jan 2021 22:03:44 +0100 Subject: [PATCH 194/363] fix i18n test after format --- tests/test_i18n_keys.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py index b41e8cd78..f7455bc8e 100644 --- a/tests/test_i18n_keys.py +++ b/tests/test_i18n_keys.py @@ -24,9 +24,9 @@ def find_expected_string_keys(): # m18n.n( "foo" # YunohostError("foo" # # i18n: foo - p1 = re.compile(r'm18n\.n\(\s*[\"\'](\w+)[\"\']') - p2 = re.compile(r'YunohostError\([\'\"](\w+)[\'\"]') - p3 = re.compile(r'# i18n: [\'\"]?(\w+)[\'\"]?') + p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']") + p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]") + p3 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") python_files = glob.glob("src/yunohost/*.py") python_files.extend(glob.glob("src/yunohost/utils/*.py")) @@ -78,7 +78,7 @@ def find_expected_string_keys(): for funcname in subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n"): yield "log_" + funcname - p4 = re.compile(r"OperationLogger\([\"\'](\w+)[\"\']") + p4 = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']") for python_file in python_files: content = open(python_file).read() for m in ("log_" + match for match in p4.findall(content)): @@ -86,7 +86,7 @@ def find_expected_string_keys(): # Global settings descriptions # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ... - p5 = re.compile(r" \([\"\'](\w[\w\.]+)[\"\'],") + p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],") content = open("src/yunohost/settings.py").read() for m in ("global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)): yield m From 535fe5aebfe946fc2360c337101aef318a48b928 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 26 Jan 2021 22:21:58 +0100 Subject: [PATCH 195/363] remove travis --- .travis.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9a0f40674..000000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: python - -matrix: - allow_failures: - - env: TOXENV=py27-lint - - env: TOXENV=py37-lint - - env: TOXENV=py37-invalidcode - include: - - python: 2.7 - env: TOXENV=py27-lint - - python: 2.7 - env: TOXENV=py27-invalidcode - - python: 3.7 - env: TOXENV=py37-lint - - python: 3.7 - env: TOXENV=py37-invalidcode - -install: - - pip install tox - -script: - - tox From f9478b93cd507a9924782e8a3a26cdd391007971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Mennetrier?= Date: Wed, 27 Jan 2021 10:46:03 +0100 Subject: [PATCH 196/363] Fix let's encrypt certificat generation --- src/yunohost/certificate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 7c633de4f..3b941ed62 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -598,7 +598,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): subdomain = "xmpp-upload." + domain xmpp_records = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "xmpp"}).get("data") or {} if xmpp_records.get("CNAME:xmpp-upload") == "OK": - csr.add_extensions([crypto.X509Extension("subjectAltName", False, "DNS:" + subdomain)]) + csr.add_extensions([crypto.X509Extension("subjectAltName".encode('utf8'), False, ("DNS:" + subdomain).encode('utf8'))]) else: logger.warning(m18n.n('certmanager_warning_subdomain_dns_record', subdomain=subdomain, domain=domain)) @@ -615,7 +615,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): csr_file = output_folder + domain + ".csr" logger.debug("Saving to %s.", csr_file) - with open(csr_file, "w") as f: + with open(csr_file, "wb") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) @@ -726,10 +726,9 @@ def _generate_key(destination_path): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, KEY_SIZE) - with open(destination_path, "w") as f: + with open(destination_path, "wb") 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 From fea1ad474bd546231c02cb7f7503d75e7772acf5 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 28 Jan 2021 00:03:38 +0100 Subject: [PATCH 197/363] remove cache --- data/templates/nginx/plain/yunohost_admin.conf.inc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf.inc b/data/templates/nginx/plain/yunohost_admin.conf.inc index ab6d270c7..26f348dea 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf.inc +++ b/data/templates/nginx/plain/yunohost_admin.conf.inc @@ -8,10 +8,4 @@ location /yunohost/admin/ { more_set_headers "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; connect-src 'self' https://raw.githubusercontent.com https://paste.yunohost.org wss://$host; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; object-src 'none';"; more_set_headers "Content-Security-Policy-Report-Only:"; - - # Short cache on handlebars templates - location ~* \.(js|css|png|jpg|jpeg|gif|ico|json|woff|woff2|ttf|eot)$ { - expires 5m; - add_header Cache-Control "public"; - } } From b6b33d99dee501baf912e653e30438b58ecb2586 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 28 Jan 2021 00:29:38 +0100 Subject: [PATCH 198/363] We don't want any output of ls, just the return code Co-authored-by: Kayou --- debian/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/postinst b/debian/postinst index fd2a24b9e..8fdb288bc 100644 --- a/debian/postinst +++ b/debian/postinst @@ -9,7 +9,7 @@ do_configure() { # If apps/ is not empty, we're probably already installed in the past and # something funky happened ... - if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* 2>/dev/null + if [ -d /etc/yunohost/apps/ ] && ls /etc/yunohost/apps/* >/dev/null 2>&1 then echo "Sounds like /etc/yunohost/installed mysteriously disappeared ... You should probably contact the Yunohost support ..." else From 43f121baede5d56f6811bcad731ab0533a30a738 Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Thu, 28 Jan 2021 08:07:53 +0100 Subject: [PATCH 199/363] Move ynh_exec_as helper to user section --- data/helpers.d/logging | 24 ------------------------ data/helpers.d/user | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index e5d208a0f..dc32ecba9 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -100,30 +100,6 @@ ynh_print_err () { ynh_print_log "[Error] ${message}" >&2 } -# Execute a command as another user -# -# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] -# | arg: -u, --user= - the user that will execute the command -# | arg: -n, --command= - the command to be executed -# -# Requires YunoHost version 4.1.7 or higher. -ynh_exec_as() -{ - # Declare an array to define the options of this helper. - local legacy_args=uc - local -A args_array=( [u]=user= [c]=command= ) - local user - local command - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - if [[ $user = $(whoami) ]]; then - eval "$command" - else - sudo -u "$user" "$command" - fi -} - # Execute a command and print the result as an error # # usage: ynh_exec_err your_command diff --git a/data/helpers.d/user b/data/helpers.d/user index aeac3a9c5..f5d4b1680 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -163,3 +163,27 @@ ynh_system_user_delete () { delgroup $username fi } + +# Execute a command as another user +# +# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] +# | arg: -u, --user= - the user that will execute the command +# | arg: -n, --command= - the command to be executed +# +# Requires YunoHost version 4.1.7 or higher. +ynh_exec_as() +{ + # Declare an array to define the options of this helper. + local legacy_args=uc + local -A args_array=( [u]=user= [c]=command= ) + local user + local command + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [[ $user = $(whoami) ]]; then + eval "$command" + else + sudo -u "$user" "$command" + fi +} From 9d0bda548ae7e34f07ccbf2c96c04f3471f19608 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 31 Jan 2021 11:57:34 +0100 Subject: [PATCH 200/363] Do not ynh_die if systemctl action fails, because we don't want to exit in the middle of a remove script ... instead, return a non-zero code which should trigger script failure only if set -eu is enabled --- data/helpers.d/apt | 2 +- data/helpers.d/systemd | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 1998c80f0..6abaf20a2 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -32,7 +32,7 @@ ynh_wait_dpkg_free() { if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$" then # If so, that a remaining of dpkg. - ynh_print_err "E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." + ynh_print_err "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." set -o xtrace # set -x return 1 fi diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index ff1b9587c..b0e175d4d 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -149,11 +149,9 @@ ynh_systemd_action() { # If a log is specified for this service, show also the content of this log if [ -e "$log_path" ] then - ynh_print_err --message="--" ynh_exec_err tail --lines=$length "$log_path" fi - # Fail the app script, since the service failed. - ynh_die + return 1 fi # Start the timeout and try to find line_match From cf560b8f91b4221d0d4363093f6a1dc8d0e96704 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 31 Dec 2020 14:05:39 +0100 Subject: [PATCH 201/363] [mod] activate moulinette.core logging in cli --- src/yunohost/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index 76449a7e4..0c3cb8355 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -143,6 +143,11 @@ def init_logging(interface="cli", 'handlers': ['file', 'tty'] if not quiet else ['file'], 'propagate': False, }, + 'moulinette.core': { + 'level': 'DEBUG' if debug else 'ERROR', + 'handlers': ['file', 'tty'] if not quiet else ['file'], + 'propagate': False, + }, }, 'root': { 'level': 'DEBUG', From c931f8a5323a9c2e734a0764330d03f73dd87dfc Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Thu, 31 Dec 2020 14:05:58 +0100 Subject: [PATCH 202/363] [mod] activate moulinette.core and moulinette.interface.api logging in api --- src/yunohost/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index 0c3cb8355..bda2cb994 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -203,6 +203,16 @@ def init_logging(interface="cli", 'handlers': [], 'propagate': True, }, + 'moulinette.core': { + 'level': 'DEBUG' if debug else 'ERROR', + 'handlers': ['file', 'tty'] if not quiet else ['file'], + 'propagate': False, + }, + 'moulinette.interface.api': { + 'level': 'DEBUG', + 'handlers': [], + 'propagate': True, + }, }, 'root': { 'level': 'DEBUG', From fcf9e58ce4d164dc2229664964056d6283fa2ace Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 31 Jan 2021 16:01:38 +0100 Subject: [PATCH 203/363] Rework logging configuration --- src/yunohost/__init__.py | 195 +++++++++++++++------------------------ 1 file changed, 74 insertions(+), 121 deletions(-) diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index bda2cb994..29c4b7bb7 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -93,129 +93,82 @@ def init_logging(interface="cli", if not os.path.isdir(logdir): os.makedirs(logdir, 0o750) - # ####################################################################### # + logging_configuration = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'console': { + 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + }, + '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': { + 'cli': { + 'level': 'DEBUG' if debug else 'INFO', + 'class': 'moulinette.interfaces.cli.TTYHandler', + 'formatter': 'tty-debug' if debug else '', + }, + 'api': { + 'level': 'DEBUG' if debug else 'INFO', + 'class': 'moulinette.interfaces.api.APIQueueHandler', + }, + 'file': { + 'class': 'logging.FileHandler', + 'formatter': 'precise', + 'filename': logfile, + 'filters': ['action'], + }, + }, + 'loggers': { + 'yunohost': { + 'level': 'DEBUG', + 'handlers': ['file', interface] if not quiet else ['file'], + 'propagate': False, + }, + 'moulinette': { + 'level': 'DEBUG', + 'handlers': ['file', interface] if not quiet else ['file'], + 'propagate': False, + }, + }, + 'root': { + 'level': 'DEBUG', + 'handlers': ['file', interface] if debug else ['file'], + }, + } + # Logging configuration for CLI (or any other interface than api...) # - # ####################################################################### # if interface != "api": - configure_logging({ - 'version': 1, - 'main_logger': "yunohost", - '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': 'DEBUG' if debug else 'INFO', - 'class': 'moulinette.interfaces.cli.TTYHandler', - 'formatter': 'tty-debug' if debug else '', - }, - 'file': { - 'class': 'logging.FileHandler', - 'formatter': 'precise', - 'filename': logfile, - 'filters': ['action'], - }, - }, - 'loggers': { - 'yunohost': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - 'moulinette': { - 'level': 'DEBUG', - 'handlers': [], - 'propagate': True, - }, - 'moulinette.interface': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - 'moulinette.core': { - 'level': 'DEBUG' if debug else 'ERROR', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if debug else ['file'], - }, - }) - # ####################################################################### # + + logging_configuration["main_logger"] = "yunohost" + + configure_logging(logging_configuration) + # Logging configuration for API # - # ####################################################################### # else: - configure_logging({ - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'console': { - 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' - }, - 'precise': { - 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' - }, + # We use a WatchedFileHandler instead of regular FileHandler to possibly support log rotation etc + logging_configuration["handlers"]["file"]["class"] = 'logging.handlers.WatchedFileHandler' + + # This is for when launching yunohost-api in debug mode, we want to display stuff in the console + if debug: + logging_configuration["handlers"]["console"] = { + 'class': 'logging.StreamHandler', + 'formatter': 'console', + 'stream': 'ext://sys.stdout', + 'filters': ['action'], }, - 'filters': { - 'action': { - '()': 'moulinette.utils.log.ActionFilter', - }, - }, - 'handlers': { - 'api': { - 'level': 'DEBUG' if debug else 'INFO', - 'class': 'moulinette.interfaces.api.APIQueueHandler', - }, - 'file': { - 'class': 'logging.handlers.WatchedFileHandler', - 'formatter': 'precise', - 'filename': logfile, - 'filters': ['action'], - }, - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'console', - 'stream': 'ext://sys.stdout', - 'filters': ['action'], - }, - }, - 'loggers': { - 'yunohost': { - 'level': 'DEBUG', - 'handlers': ['file', 'api'] + (['console'] if debug else []), - 'propagate': False, - }, - 'moulinette': { - 'level': 'DEBUG', - 'handlers': [], - 'propagate': True, - }, - 'moulinette.core': { - 'level': 'DEBUG' if debug else 'ERROR', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - 'moulinette.interface.api': { - 'level': 'DEBUG', - 'handlers': [], - 'propagate': True, - }, - }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['file'] + (['console'] if debug else []), - }, - }) + logging_configuration["loggers"]["yunohost"]["handlers"].append("console") + logging_configuration["loggers"]["moulinette"]["handlers"].append("console") + logging_configuration["root"]["handlers"].append("console") + + configure_logging(logging_configuration) From 2ab330a0e2a1c699a6d09510b18b73505f821d11 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 31 Jan 2021 16:08:07 +0100 Subject: [PATCH 204/363] I think we don't need this hack anymore --- src/yunohost/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index 29c4b7bb7..c536ba255 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -149,9 +149,6 @@ def init_logging(interface="cli", # Logging configuration for CLI (or any other interface than api...) # if interface != "api": - - logging_configuration["main_logger"] = "yunohost" - configure_logging(logging_configuration) # Logging configuration for API # From 071d8c4cbef639cb03c53cd0a71104404aa70d04 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 31 Jan 2021 18:15:51 +0100 Subject: [PATCH 205/363] Misc fixes for cert / local CA management --- data/hooks/conf_regen/02-ssl | 5 ++++- src/yunohost/certificate.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index 0cb38df7b..e65cfca60 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -27,7 +27,7 @@ regen_local_ca() { rm -f index.txt touch index.txt cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf - sed -i s/yunohost.org/${domain}/g openssl.ca.cnf + sed -i 's/yunohost.org/${domain}/g' openssl.ca.cnf openssl req -x509 \ -new \ -config openssl.ca.cnf \ @@ -57,6 +57,9 @@ do_init_regen() { chown root:root $LOGFILE chmod 640 $LOGFILE + # Make sure this conf exists + cp /usr/share/yunohost/templates/ssl/openssl.cnf ${ssl_dir}/openssl.ca.cnf + # create default certificates if [[ ! -f "$ynh_ca" ]]; then regen_local_ca yunohost.org >>$LOGFILE diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3b941ed62..23903c92c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -641,7 +641,7 @@ def _get_status(domain): valid_up_to = datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer == _name_self_CA(): + if cert_issuer == 'yunohost.org' or cert_issuer == _name_self_CA(): CA_type = { "code": "self-signed", "verbose": "Self-signed", From 06185a2392d6c9b247ddfb43fdb1f8a876dc6055 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Feb 2021 03:45:13 +0100 Subject: [PATCH 206/363] Use jq / output-as json to get info from yunohost commands instead of scraping with grep --- data/helpers.d/backup | 15 ++++++++++++--- data/helpers.d/permission | 6 ++++-- data/helpers.d/user | 7 +++---- data/helpers.d/utils | 7 +------ 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/data/helpers.d/backup b/data/helpers.d/backup index 5557cbc42..33a6db4e2 100644 --- a/data/helpers.d/backup +++ b/data/helpers.d/backup @@ -399,6 +399,15 @@ ynh_delete_file_checksum () { ynh_app_setting_delete --app=$app --key=$checksum_setting_name } +# Checks a backup archive exists +# +# [internal] +# +ynh_backup_archive_exists () { + yunohost backup list --output-as json --quiet \ + | jq -e --arg archive "$1" '.archives | index($archive)' >/dev/null +} + # Make a backup in case of failed upgrade # # usage: @@ -423,7 +432,7 @@ ynh_backup_before_upgrade () { if [ "$NO_BACKUP_UPGRADE" -eq 0 ] then # Check if a backup already exists with the prefix 1 - if yunohost backup list | grep --quiet $app_bck-pre-upgrade1 + if ynh_backup_archive_exists "$app_bck-pre-upgrade1" then # Prefix becomes 2 to preserve the previous backup backup_number=2 @@ -435,7 +444,7 @@ ynh_backup_before_upgrade () { if [ "$?" -eq 0 ] then # If the backup succeeded, remove the previous backup - if yunohost backup list | grep --quiet $app_bck-pre-upgrade$old_backup_number + if ynh_backup_archive_exists "$app_bck-pre-upgrade$old_backup_number" then # Remove the previous backup only if it exists yunohost backup delete $app_bck-pre-upgrade$old_backup_number > /dev/null @@ -467,7 +476,7 @@ ynh_restore_upgradebackup () { if [ "$NO_BACKUP_UPGRADE" -eq 0 ] then # Check if an existing backup can be found before removing and restoring the application. - if yunohost backup list | grep --quiet $app_bck-pre-upgrade$backup_number + if ynh_backup_archive_exists "$app_bck-pre-upgrade$backup_number" then # Remove the application then restore it yunohost app remove $app diff --git a/data/helpers.d/permission b/data/helpers.d/permission index 1791425b5..f4e200019 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -185,7 +185,8 @@ ynh_permission_exists() { local permission ynh_handle_getopts_args "$@" - yunohost user permission list --short | grep --word-regexp --quiet "$app.$permission" + yunohost user permission list --output-as json --quiet \ + | jq -e --arg perm "$app.$permission" '.permissions[$perm]' >/dev/null } # Redefine the url associated to a permission @@ -366,7 +367,8 @@ ynh_permission_has_user() { return 1 fi - yunohost user permission info "$app.$permission" | grep --word-regexp --quiet "$user" + yunohost user permission info "$app.$permission" --output-as json --quiet \ + | jq -e --arg user $user '.corresponding_users | index($user)' >/dev/null } # Check if a legacy permissions exist diff --git a/data/helpers.d/user b/data/helpers.d/user index f5d4b1680..bb2af6b55 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -17,7 +17,7 @@ ynh_user_exists() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - yunohost user list --output-as json | grep --quiet "\"username\": \"${username}\"" + yunohost user list --output-as json --quiet | jq -e ".users.${username}" >/dev/null } # Retrieve a YunoHost user information @@ -39,7 +39,7 @@ ynh_user_get_info() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - yunohost user info "$username" --output-as plain | ynh_get_plain_key "$key" + yunohost user info "$username" --output-as json --quiet | jq -r ".$key" } # Get the list of YunoHost users @@ -51,8 +51,7 @@ ynh_user_get_info() { # # Requires YunoHost version 2.4.0 or higher. ynh_user_list() { - yunohost user list --output-as plain --quiet \ - | awk '/^##username$/{getline; print}' + yunohost user list --output-as json --quiet | jq -r ".users | keys[]" } # Check if a user exists on the system diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 13f84424e..d696effdc 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -504,12 +504,7 @@ ynh_secure_remove () { # # [internal] # -# example: yunohost user info tata --output-as plain | ynh_get_plain_key mail -# -# usage: ynh_get_plain_key key [subkey [subsubkey ...]] -# | ret: string - the key's value -# -# Requires YunoHost version 2.2.4 or higher. +# (Deprecated, use --output-as json and jq instead) ynh_get_plain_key() { local prefix="#" local founded=0 From 10ba00affe950f62eaef296869b5ad90c67c3d9f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Feb 2021 03:57:33 +0100 Subject: [PATCH 207/363] logger.exception -> logger.error because logger.exception displays a stacktrace and it ain't relevant in any of these cases --- src/yunohost/app.py | 8 ++++---- src/yunohost/backup.py | 6 +++--- src/yunohost/tools.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2171f2d3d..bb3e9a285 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -104,16 +104,16 @@ def app_catalog(full=False, with_categories=False): return {"apps": catalog["apps"]} else: return {"apps": catalog["apps"], "categories": catalog["categories"]} - + def app_search(string): """ Return a dict of apps whose description or name match the search string """ - + # Retrieve a simple dict listing all apps catalog_of_apps = app_catalog() - + # Selecting apps according to a match in app name or description for app in catalog_of_apps["apps"].items(): if not (re.search(string, app[0], flags=re.IGNORECASE) or @@ -2171,7 +2171,7 @@ def _get_git_last_commit_hash(repository, reference='HEAD'): .format(repository, reference) commit = check_output(cmd) except subprocess.CalledProcessError: - logger.exception("unable to get last commit from %s", repository) + logger.error("unable to get last commit from %s", repository) raise ValueError("Unable to get last commit with git") else: return commit.strip() diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 870e48476..455c39f84 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -703,7 +703,7 @@ class BackupManager(): 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)) + logger.error(m18n.n('backup_app_failed', app=app)) self.targets.set_result("apps", app, "Error") else: # Add app info @@ -945,7 +945,7 @@ class RestoreManager(): if system_part not in self.info['system'] or\ 'paths' not in self.info['system'][system_part] or\ len(self.info['system'][system_part]['paths']) == 0: - logger.exception(m18n.n('restore_hook_unavailable', part=system_part)) + logger.error(m18n.n('restore_hook_unavailable', part=system_part)) self.targets.set_result("system", system_part, "Skipped") continue @@ -1392,7 +1392,7 @@ class RestoreManager(): env=env_dict)[0] except: msg = m18n.n('restore_app_failed', app=app_instance_name) - logger.exception(msg) + logger.error(msg) operation_logger.error(msg) if msettings.get('interface') != 'api': diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ae8b3443e..405143395 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -148,7 +148,7 @@ def tools_adminpw(new_password, check_strength=True): try: ldap.update("cn=admin", {"userPassword": [new_hash], }) except: - logger.exception('unable to change admin password') + logger.error('unable to change admin password') raise YunohostError('admin_password_change_failed') else: # Write as root password From 12244036a9074b94d6883331807af8ad82bf574b Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 19 Jan 2021 11:20:07 +0100 Subject: [PATCH 208/363] Typo --- 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 24121df30..5e02ca762 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -650,7 +650,7 @@ ynh_check_app_version_changed () { # # Generally you might probably use it as follow in the upgrade script # -# if ynh_compare_current_package_version --comparaison lt --version 2.3.2~ynh1 +# if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 # then # # Do something that is needed for the package version older than 2.3.2~ynh1 # fi From 4b876ff07f773e1999c84ab73b0b2bc66fde6e38 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 16:25:58 +0100 Subject: [PATCH 209/363] Handle case where DKIM record is split into several pieces --- data/hooks/diagnosis/12-dnsrecords.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index 941911147..cb8193dd5 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -130,8 +130,11 @@ class DNSRecordsDiagnoser(Diagnoser): # Split expected/current # from "v=DKIM1; k=rsa; p=hugekey;" # to a set like {'v=DKIM1', 'k=rsa', 'p=...'} + # Additionally, for DKIM, because the key is pretty long, + # some DNS registrar sometime split it into several pieces like this: + # "p=foo" "bar" (with a space and quotes in the middle)... expected = set(r["value"].strip(';" ').replace(";", " ").split()) - current = set(r["current"].strip(';" ').replace(";", " ").split()) + current = set(r["current"].replace('" "', '').strip(';" ').replace(";", " ").split()) # For SPF, ignore parts starting by ip4: or ip6: if r["name"] == "@": From 4725e05470ef4c1fc48a13c83c5a524035259033 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 21 Jan 2021 19:06:15 +0100 Subject: [PATCH 210/363] fix de locale --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index bdf21ac1b..764ca2c1e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -489,5 +489,5 @@ "global_settings_setting_smtp_relay_port": "SMTP Relay Port", "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", - "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domains:s}\" entfernen." + "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen." } From 536fd9be9e066164b840895a0cc043241c57cba3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 21 Jan 2021 19:36:21 +0100 Subject: [PATCH 211/363] diagnosis: Ignore /dev/loop devices in systemresources --- data/hooks/diagnosis/50-systemresources.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index f0fac4974..4ae1efadf 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -68,6 +68,9 @@ class SystemResourcesDiagnoser(Diagnoser): disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) + # Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/) + disk_partitions = [d for d in disk_partitions if not d.device.startswith("/dev/loop")] + for disk_partition in disk_partitions: device = disk_partition.device mountpoint = disk_partition.mountpoint From 2fc016e3e5d74fc6fd4ed3158c6b025087f329bd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:32:27 +0100 Subject: [PATCH 212/363] Make sure tmp_script exists .. --- src/yunohost/backup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 242cd0bfd..dbad45746 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -673,6 +673,7 @@ class BackupManager(): settings_dir = os.path.join(self.work_dir, 'apps', app, 'settings') logger.info(m18n.n("app_start_backup", app=app)) + tmp_script = None # This is to make sure the var exists later in the 'finally' ... try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid='admin') @@ -713,7 +714,8 @@ class BackupManager(): # Remove tmp files in all situations finally: - filesystem.rm(tmp_script, force=True) + if tmp_script: + filesystem.rm(tmp_script, force=True) filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) # From 62e84d8b4bf6eba7d4e42f57fcc9c5297565fc64 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:37:29 +0100 Subject: [PATCH 213/363] service_regen_conf is deprecated in factor of regen_conf --- src/yunohost/settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 3c79d7945..6eaa6eaa8 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -8,7 +8,7 @@ from collections import OrderedDict from moulinette import m18n from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf +from yunohost.regenconf import regen_conf logger = getActionLogger('yunohost.settings') @@ -325,13 +325,13 @@ def trigger_post_change_hook(setting_name, old_value, new_value): @post_change_hook("security.nginx.compatibility") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['nginx']) + regen_conf(names=['nginx']) @post_change_hook("security.ssh.compatibility") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['ssh']) + regen_conf(names=['ssh']) @post_change_hook("smtp.allow_ipv6") @@ -342,7 +342,7 @@ def reconfigure_ssh(setting_name, old_value, new_value): @post_change_hook("security.postfix.compatibility") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: - service_regen_conf(names=['postfix']) + regen_conf(names=['postfix']) @post_change_hook("pop3.enabled") @@ -364,9 +364,9 @@ def reconfigure_dovecot(setting_name, old_value, new_value): ] subprocess.call(command, env=environment) if old_value != new_value: - service_regen_conf(names=['dovecot']) + regen_conf(names=['dovecot']) else: if old_value != new_value: - service_regen_conf(names=['dovecot']) + regen_conf(names=['dovecot']) command = ['apt-get', '-y', 'remove', dovecot_package] subprocess.call(command, env=environment) From 5ab5c83d549b6cedf457315b0c00c98a4da1a98f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 25 Jan 2021 03:57:51 +0100 Subject: [PATCH 214/363] Misc issues with yunoprompt --- bin/yunoprompt | 15 +++++++-------- data/other/yunoprompt.service | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 252e5a1a4..645dd4ab1 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -6,7 +6,7 @@ x509_fingerprint=$(openssl x509 -in /etc/yunohost/certs/yunohost.org/crt.pem -n # Fetch SSH fingerprints i=0 -for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do +for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do output=$(ssh-keygen -l -f $key) fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)" i=$(($i + 1)) @@ -43,22 +43,21 @@ LOGO_AND_FINGERPRINTS=$(cat << EOF $LOGO - IP: ${local_ip} - X509 fingerprint: ${x509_fingerprint} + Local IP: ${local_ip:-(no ip detected?)} + Local SSL CA X509 fingerprint: + ${x509_fingerprint} SSH fingerprints: ${fingerprint[0]} ${fingerprint[1]} ${fingerprint[2]} - ${fingerprint[3]} - ${fingerprint[4]} EOF ) -if [[ -f /etc/yunohost/installed ]] +echo "$LOGO_AND_FINGERPRINTS" > /etc/issue + +if [[ ! -f /etc/yunohost/installed ]] then - echo "$LOGO_AND_FINGERPRINTS" > /etc/issue -else chvt 2 # Formatting diff --git a/data/other/yunoprompt.service b/data/other/yunoprompt.service index 3c4df50f9..effb69590 100644 --- a/data/other/yunoprompt.service +++ b/data/other/yunoprompt.service @@ -1,6 +1,7 @@ [Unit] Description=YunoHost boot prompt After=getty@tty2.service +After=network.target [Service] Type=simple From 4e335e07823e863686f60d65992e17d362a428af Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 21 Dec 2020 05:00:45 +0100 Subject: [PATCH 215/363] [fix] If uid is less than 1000 nsswitch ignore it --- 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 edb1c7c8f..c0ca0f3fb 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -170,7 +170,7 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default - uid = str(random.randint(200, 65000)) + uid = str(random.randint(1000, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP From aef3ee1434e8ea472d73cc13de99c7d3e6fef8a7 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 21 Dec 2020 05:02:22 +0100 Subject: [PATCH 216/363] [fix] If uid is less than 1001 nsswitch ignore it --- 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 c0ca0f3fb..9e4cbc219 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -170,7 +170,7 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor uid_guid_found = False while not uid_guid_found: # LXC uid number is limited to 65536 by default - uid = str(random.randint(1000, 65000)) + uid = str(random.randint(1001, 65000)) uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP From 9fbd1a02dbd901df5eaa4338b6c001dba72a7b71 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 25 Jan 2021 17:51:17 +0100 Subject: [PATCH 217/363] Recommend yunohost.local in yunoprompt --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 645dd4ab1..be46fc9ab 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -72,7 +72,7 @@ be asked for : - the administration password. You can perform this step : - - from your web browser, by accessing : ${local_ip} + - from your web browser, by accessing : https://yunohost.local/ or ${local_ip} - or in this terminal by answering 'yes' to the following question If this is your first time with YunoHost, it is strongly recommended to take From b94ff1c20087bbe0030c6508649d9198ae399b8d Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Fri, 22 Jan 2021 17:08:28 +0100 Subject: [PATCH 218/363] Add ynh_exec_as to official --- data/helpers.d/logging | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 45b5b7e67..50772d8f4 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -100,6 +100,30 @@ ynh_print_err () { ynh_print_log "[Error] ${message}" >&2 } +# Execute a command as another user +# +# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] +# | arg: -u, --user= - the user that will execute the command +# | arg: -n, --command= - the command to be executed +# +# Requires YunoHost version 4.1.7 or higher. +ynh_exec_as() +{ + # Declare an array to define the options of this helper. + local legacy_args=uc + local -A args_array=( [u]=user= [c]=command= ) + local user + local command + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [[ $user = $(whoami) ]]; then + eval "$command" + else + sudo -u "$user" "$command" + fi +} + # Execute a command and print the result as an error # # usage: ynh_exec_err your_command From 6b2d76dddd61ef498a249e8782368137aa18c0fa Mon Sep 17 00:00:00 2001 From: ericgaspar Date: Thu, 28 Jan 2021 08:07:53 +0100 Subject: [PATCH 219/363] Move ynh_exec_as helper to user section --- data/helpers.d/logging | 24 ------------------------ data/helpers.d/user | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index 50772d8f4..45b5b7e67 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -100,30 +100,6 @@ ynh_print_err () { ynh_print_log "[Error] ${message}" >&2 } -# Execute a command as another user -# -# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] -# | arg: -u, --user= - the user that will execute the command -# | arg: -n, --command= - the command to be executed -# -# Requires YunoHost version 4.1.7 or higher. -ynh_exec_as() -{ - # Declare an array to define the options of this helper. - local legacy_args=uc - local -A args_array=( [u]=user= [c]=command= ) - local user - local command - # Manage arguments with getopts - ynh_handle_getopts_args "$@" - - if [[ $user = $(whoami) ]]; then - eval "$command" - else - sudo -u "$user" "$command" - fi -} - # Execute a command and print the result as an error # # usage: ynh_exec_err your_command diff --git a/data/helpers.d/user b/data/helpers.d/user index aeac3a9c5..f5d4b1680 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -163,3 +163,27 @@ ynh_system_user_delete () { delgroup $username fi } + +# Execute a command as another user +# +# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] +# | arg: -u, --user= - the user that will execute the command +# | arg: -n, --command= - the command to be executed +# +# Requires YunoHost version 4.1.7 or higher. +ynh_exec_as() +{ + # Declare an array to define the options of this helper. + local legacy_args=uc + local -A args_array=( [u]=user= [c]=command= ) + local user + local command + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + if [[ $user = $(whoami) ]]; then + eval "$command" + else + sudo -u "$user" "$command" + fi +} From 29fe7c31030f70a0b01dc8c9a4722ffc83458564 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 31 Jan 2021 11:57:34 +0100 Subject: [PATCH 220/363] Do not ynh_die if systemctl action fails, because we don't want to exit in the middle of a remove script ... instead, return a non-zero code which should trigger script failure only if set -eu is enabled --- data/helpers.d/apt | 2 +- data/helpers.d/systemd | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 1998c80f0..6abaf20a2 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -32,7 +32,7 @@ ynh_wait_dpkg_free() { if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$" then # If so, that a remaining of dpkg. - ynh_print_err "E: dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." + ynh_print_err "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." set -o xtrace # set -x return 1 fi diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index ff1b9587c..b0e175d4d 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -149,11 +149,9 @@ ynh_systemd_action() { # If a log is specified for this service, show also the content of this log if [ -e "$log_path" ] then - ynh_print_err --message="--" ynh_exec_err tail --lines=$length "$log_path" fi - # Fail the app script, since the service failed. - ynh_die + return 1 fi # Start the timeout and try to find line_match From 08e7b42c82fb1ccc0c54c54f818267e0daee9bac Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Feb 2021 03:57:33 +0100 Subject: [PATCH 221/363] logger.exception -> logger.error because logger.exception displays a stacktrace and it ain't relevant in any of these cases --- src/yunohost/app.py | 2 +- src/yunohost/backup.py | 6 +++--- src/yunohost/tools.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index ade39bf20..7e2204f12 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2174,7 +2174,7 @@ def _get_git_last_commit_hash(repository, reference='HEAD'): repository, reference), shell=True) except subprocess.CalledProcessError: - logger.exception("unable to get last commit from %s", repository) + logger.error("unable to get last commit from %s", repository) raise ValueError("Unable to get last commit with git") else: return commit.strip() diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index dbad45746..7aee5dfd1 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -700,7 +700,7 @@ class BackupManager(): 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)) + logger.error(m18n.n('backup_app_failed', app=app)) self.targets.set_result("apps", app, "Error") else: # Add app info @@ -942,7 +942,7 @@ class RestoreManager(): if system_part not in self.info['system'] or\ 'paths' not in self.info['system'][system_part] or\ len(self.info['system'][system_part]['paths']) == 0: - logger.exception(m18n.n('restore_hook_unavailable', part=system_part)) + logger.error(m18n.n('restore_hook_unavailable', part=system_part)) self.targets.set_result("system", system_part, "Skipped") continue @@ -1390,7 +1390,7 @@ class RestoreManager(): env=env_dict)[0] except: msg = m18n.n('restore_app_failed', app=app_instance_name) - logger.exception(msg) + logger.error(msg) operation_logger.error(msg) if msettings.get('interface') != 'api': diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 96bd01ed6..291c9c7b5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -150,7 +150,7 @@ def tools_adminpw(new_password, check_strength=True): try: ldap.update("cn=admin", {"userPassword": [new_hash], }) except: - logger.exception('unable to change admin password') + logger.error('unable to change admin password') raise YunohostError('admin_password_change_failed') else: # Write as root password From adc83b4c9c2c30e9ef75f3609c538b646f91f1db Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Feb 2021 04:25:05 +0100 Subject: [PATCH 222/363] Update changelog for 4.1.7 --- debian/changelog | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/debian/changelog b/debian/changelog index 6a3691b4e..4ac5b6cbf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,20 @@ +yunohost (4.1.7) stable; urgency=low + + - [fix] diagnosis: Handle case where DKIM record is split into several pieces (4b876ff0) + - [fix] i18n: de locale was broken (4725e054) + - [enh] diagnosis: Ignore /dev/loop devices in systemresources (536fd9be) + - [fix] backup: fix a small issue dur to var not existing in some edge case ... (2fc016e3) + - [fix] settings: service_regen_conf is deprecated in favor of regen_conf (62e84d8b) + - [fix] users: If uid is less than 1001, nsswitch ignores it (4e335e07, aef3ee14) + - [enh] misc: fixes/enh in yunoprompt (5ab5c83d, 9fbd1a02) + - [enh] helpers: Add ynh_exec_as (b94ff1c2, 6b2d76dd) + - [fix] helpers: Do not ynh_die if systemctl action fails, to avoid exiting during a remove script (29fe7c31) + - [fix] misc: logger.exception -> logger.error (08e7b42c) + + Thanks to all contributors <3 ! (ericgaspar, Kayou, ljf) + + -- Alexandre Aubin Tue, 02 Feb 2021 04:18:01 +0100 + yunohost (4.1.6) stable; urgency=low - [fix] Make dyndns update more resilient to ns0.yunohost.org being down ([#1140](https://github.com/yunohost/yunohost/pull/1140)) From c4439110e195c73cf0a4f93b5e7285d57f744f6c Mon Sep 17 00:00:00 2001 From: yunohost-bot Date: Tue, 2 Feb 2021 03:37:40 +0000 Subject: [PATCH 223/363] [CI] Format code --- data/actionsmap/yunohost_completion.py | 137 +- data/hooks/diagnosis/00-basesystem.py | 147 +- data/hooks/diagnosis/10-ip.py | 132 +- data/hooks/diagnosis/12-dnsrecords.py | 102 +- data/hooks/diagnosis/14-ports.py | 106 +- data/hooks/diagnosis/21-web.py | 112 +- data/hooks/diagnosis/24-mail.py | 217 ++- data/hooks/diagnosis/30-services.py | 9 +- data/hooks/diagnosis/50-systemresources.py | 104 +- data/hooks/diagnosis/70-regenconf.py | 24 +- doc/generate_helper_doc.py | 70 +- doc/generate_manpages.py | 14 +- src/yunohost/__init__.py | 227 ++- src/yunohost/app.py | 1461 +++++++++++------ src/yunohost/backup.py | 1084 +++++++----- src/yunohost/certificate.py | 289 +++- .../data_migrations/0015_migrate_to_buster.py | 125 +- .../0016_php70_to_php73_pools.py | 24 +- .../0017_postgresql_9p6_to_11.py | 41 +- .../data_migrations/0018_xtable_to_nftable.py | 62 +- .../0019_extend_permissions_features.py | 72 +- src/yunohost/diagnosis.py | 230 ++- src/yunohost/domain.py | 228 ++- src/yunohost/dyndns.py | 170 +- src/yunohost/firewall.py | 258 +-- src/yunohost/hook.py | 251 +-- src/yunohost/log.py | 157 +- src/yunohost/permission.py | 463 ++++-- src/yunohost/regenconf.py | 306 ++-- src/yunohost/service.py | 290 ++-- src/yunohost/settings.py | 193 ++- src/yunohost/ssh.py | 89 +- src/yunohost/tests/conftest.py | 3 + src/yunohost/tests/test_apps.py | 13 +- .../tests/test_apps_arguments_parsing.py | 580 +++++-- src/yunohost/tests/test_appscatalog.py | 62 +- src/yunohost/tests/test_appurl.py | 149 +- src/yunohost/tests/test_backuprestore.py | 295 ++-- src/yunohost/tests/test_changeurl.py | 11 +- src/yunohost/tests/test_permission.py | 858 ++++++---- src/yunohost/tests/test_regenconf.py | 12 +- src/yunohost/tests/test_service.py | 13 +- src/yunohost/tests/test_settings.py | 50 +- src/yunohost/tests/test_user-group.py | 68 +- src/yunohost/tools.py | 465 ++++-- src/yunohost/user.py | 538 +++--- src/yunohost/utils/error.py | 5 +- src/yunohost/utils/ldap.py | 28 +- src/yunohost/utils/legacy.py | 284 +++- src/yunohost/utils/network.py | 79 +- src/yunohost/utils/packages.py | 19 +- src/yunohost/utils/password.py | 24 +- src/yunohost/utils/yunopaste.py | 25 +- src/yunohost/vendor/acme_tiny/acme_tiny.py | 276 +++- tests/remove_stale_translated_strings.py | 11 +- tests/test_i18n_keys.py | 58 +- tests/test_translation_format_consistency.py | 15 +- 57 files changed, 7207 insertions(+), 3928 deletions(-) diff --git a/data/actionsmap/yunohost_completion.py b/data/actionsmap/yunohost_completion.py index f4fee30ca..bc32028d3 100644 --- a/data/actionsmap/yunohost_completion.py +++ b/data/actionsmap/yunohost_completion.py @@ -12,28 +12,33 @@ import os import yaml THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ACTIONSMAP_FILE = THIS_SCRIPT_DIR + '/yunohost.yml' -BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + '/../bash-completion.d/yunohost' +ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/yunohost.yml" +BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + "/../bash-completion.d/yunohost" def get_dict_actions(OPTION_SUBTREE, category): - ACTIONS = [action for action in OPTION_SUBTREE[category]["actions"].keys() - if not action.startswith('_')] - ACTIONS_STR = '{}'.format(' '.join(ACTIONS)) + ACTIONS = [ + action + for action in OPTION_SUBTREE[category]["actions"].keys() + if not action.startswith("_") + ] + ACTIONS_STR = "{}".format(" ".join(ACTIONS)) DICT = {"actions_str": ACTIONS_STR} return DICT -with open(ACTIONSMAP_FILE, 'r') as stream: +with open(ACTIONSMAP_FILE, "r") as stream: # Getting the dictionary containning what actions are possible per category OPTION_TREE = yaml.load(stream) - CATEGORY = [category for category in OPTION_TREE.keys() if not category.startswith('_')] + CATEGORY = [ + category for category in OPTION_TREE.keys() if not category.startswith("_") + ] - CATEGORY_STR = '{}'.format(' '.join(CATEGORY)) + CATEGORY_STR = "{}".format(" ".join(CATEGORY)) ACTIONS_DICT = {} for category in CATEGORY: ACTIONS_DICT[category] = get_dict_actions(OPTION_TREE, category) @@ -42,86 +47,112 @@ with open(ACTIONSMAP_FILE, 'r') as stream: ACTIONS_DICT[category]["subcategories_str"] = "" if "subcategories" in OPTION_TREE[category].keys(): - SUBCATEGORIES = [subcategory for subcategory in OPTION_TREE[category]["subcategories"].keys()] + SUBCATEGORIES = [ + subcategory + for subcategory in OPTION_TREE[category]["subcategories"].keys() + ] - SUBCATEGORIES_STR = '{}'.format(' '.join(SUBCATEGORIES)) + SUBCATEGORIES_STR = "{}".format(" ".join(SUBCATEGORIES)) ACTIONS_DICT[category]["subcategories_str"] = SUBCATEGORIES_STR for subcategory in SUBCATEGORIES: - ACTIONS_DICT[category]["subcategories"][subcategory] = get_dict_actions(OPTION_TREE[category]["subcategories"], subcategory) + ACTIONS_DICT[category]["subcategories"][subcategory] = get_dict_actions( + OPTION_TREE[category]["subcategories"], subcategory + ) - with open(BASH_COMPLETION_FILE, 'w') as generated_file: + with open(BASH_COMPLETION_FILE, "w") as generated_file: # header of the file - generated_file.write('#\n') - generated_file.write('# completion for yunohost\n') - generated_file.write('# automatically generated from the actionsmap\n') - generated_file.write('#\n\n') + generated_file.write("#\n") + generated_file.write("# completion for yunohost\n") + generated_file.write("# automatically generated from the actionsmap\n") + generated_file.write("#\n\n") # Start of the completion function - generated_file.write('_yunohost()\n') - generated_file.write('{\n') + generated_file.write("_yunohost()\n") + generated_file.write("{\n") # Defining local variable for previously and currently typed words - generated_file.write('\tlocal cur prev opts narg\n') - generated_file.write('\tCOMPREPLY=()\n\n') - generated_file.write('\t# the number of words already typed\n') - generated_file.write('\tnarg=${#COMP_WORDS[@]}\n\n') - generated_file.write('\t# the current word being typed\n') + generated_file.write("\tlocal cur prev opts narg\n") + generated_file.write("\tCOMPREPLY=()\n\n") + generated_file.write("\t# the number of words already typed\n") + generated_file.write("\tnarg=${#COMP_WORDS[@]}\n\n") + generated_file.write("\t# the current word being typed\n") generated_file.write('\tcur="${COMP_WORDS[COMP_CWORD]}"\n\n') # If one is currently typing a category then match with the category list - generated_file.write('\t# If one is currently typing a category,\n') - generated_file.write('\t# match with categorys\n') - generated_file.write('\tif [[ $narg == 2 ]]; then\n') + generated_file.write("\t# If one is currently typing a category,\n") + generated_file.write("\t# match with categorys\n") + generated_file.write("\tif [[ $narg == 2 ]]; then\n") generated_file.write('\t\topts="{}"\n'.format(CATEGORY_STR)) - generated_file.write('\tfi\n\n') + generated_file.write("\tfi\n\n") # If one is currently typing an action then match with the action list # of the previously typed category - generated_file.write('\t# If one already typed a category,\n') - generated_file.write('\t# match the actions or the subcategories of that category\n') - generated_file.write('\tif [[ $narg == 3 ]]; then\n') - generated_file.write('\t\t# the category typed\n') + generated_file.write("\t# If one already typed a category,\n") + generated_file.write( + "\t# match the actions or the subcategories of that category\n" + ) + generated_file.write("\tif [[ $narg == 3 ]]; then\n") + generated_file.write("\t\t# the category typed\n") generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n') for category in CATEGORY: - generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category)) - generated_file.write('\t\t\topts="{} {}"\n'.format(ACTIONS_DICT[category]["actions_str"], ACTIONS_DICT[category]["subcategories_str"])) - generated_file.write('\t\tfi\n') - generated_file.write('\tfi\n\n') + generated_file.write( + '\t\tif [[ $category == "{}" ]]; then\n'.format(category) + ) + generated_file.write( + '\t\t\topts="{} {}"\n'.format( + ACTIONS_DICT[category]["actions_str"], + ACTIONS_DICT[category]["subcategories_str"], + ) + ) + generated_file.write("\t\tfi\n") + generated_file.write("\tfi\n\n") - generated_file.write('\t# If one already typed an action or a subcategory,\n') - generated_file.write('\t# match the actions of that subcategory\n') - generated_file.write('\tif [[ $narg == 4 ]]; then\n') - generated_file.write('\t\t# the category typed\n') + generated_file.write("\t# If one already typed an action or a subcategory,\n") + generated_file.write("\t# match the actions of that subcategory\n") + generated_file.write("\tif [[ $narg == 4 ]]; then\n") + generated_file.write("\t\t# the category typed\n") generated_file.write('\t\tcategory="${COMP_WORDS[1]}"\n\n') - generated_file.write('\t\t# the action or the subcategory typed\n') + generated_file.write("\t\t# the action or the subcategory typed\n") generated_file.write('\t\taction_or_subcategory="${COMP_WORDS[2]}"\n\n') for category in CATEGORY: if len(ACTIONS_DICT[category]["subcategories"]): - generated_file.write('\t\tif [[ $category == "{}" ]]; then\n'.format(category)) + generated_file.write( + '\t\tif [[ $category == "{}" ]]; then\n'.format(category) + ) for subcategory in ACTIONS_DICT[category]["subcategories"]: - generated_file.write('\t\t\tif [[ $action_or_subcategory == "{}" ]]; then\n'.format(subcategory)) - generated_file.write('\t\t\t\topts="{}"\n'.format(ACTIONS_DICT[category]["subcategories"][subcategory]["actions_str"])) - generated_file.write('\t\t\tfi\n') - generated_file.write('\t\tfi\n') - generated_file.write('\tfi\n\n') + generated_file.write( + '\t\t\tif [[ $action_or_subcategory == "{}" ]]; then\n'.format( + subcategory + ) + ) + generated_file.write( + '\t\t\t\topts="{}"\n'.format( + ACTIONS_DICT[category]["subcategories"][subcategory][ + "actions_str" + ] + ) + ) + generated_file.write("\t\t\tfi\n") + generated_file.write("\t\tfi\n") + generated_file.write("\tfi\n\n") # If both category and action have been typed or the category # was not recognized propose --help (only once) - generated_file.write('\t# If no options were found propose --help\n') + generated_file.write("\t# If no options were found propose --help\n") generated_file.write('\tif [ -z "$opts" ]; then\n') generated_file.write('\t\tprev="${COMP_WORDS[COMP_CWORD-1]}"\n\n') generated_file.write('\t\tif [[ $prev != "--help" ]]; then\n') - generated_file.write('\t\t\topts=( --help )\n') - generated_file.write('\t\tfi\n') - generated_file.write('\tfi\n') + generated_file.write("\t\t\topts=( --help )\n") + generated_file.write("\t\tfi\n") + generated_file.write("\tfi\n") # generate the completion list from the possible options generated_file.write('\tCOMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )\n') - generated_file.write('\treturn 0\n') - generated_file.write('}\n\n') + generated_file.write("\treturn 0\n") + generated_file.write("}\n\n") # Add the function to bash completion - generated_file.write('complete -F _yunohost yunohost') + generated_file.write("complete -F _yunohost yunohost") diff --git a/data/hooks/diagnosis/00-basesystem.py b/data/hooks/diagnosis/00-basesystem.py index a582805e9..3623c10e2 100644 --- a/data/hooks/diagnosis/00-basesystem.py +++ b/data/hooks/diagnosis/00-basesystem.py @@ -27,38 +27,47 @@ class BaseSystemDiagnoser(Diagnoser): # Detect arch arch = check_output("dpkg --print-architecture") - hardware = dict(meta={"test": "hardware"}, - status="INFO", - data={"virt": virt, "arch": arch}, - summary="diagnosis_basesystem_hardware") + hardware = dict( + meta={"test": "hardware"}, + status="INFO", + data={"virt": virt, "arch": arch}, + summary="diagnosis_basesystem_hardware", + ) # Also possibly the board / hardware name if os.path.exists("/proc/device-tree/model"): - model = read_file('/proc/device-tree/model').strip().replace('\x00', '') + model = read_file("/proc/device-tree/model").strip().replace("\x00", "") hardware["data"]["model"] = model hardware["details"] = ["diagnosis_basesystem_hardware_model"] elif os.path.exists("/sys/devices/virtual/dmi/id/sys_vendor"): model = read_file("/sys/devices/virtual/dmi/id/sys_vendor").strip() if os.path.exists("/sys/devices/virtual/dmi/id/product_name"): - model = "%s %s" % (model, read_file("/sys/devices/virtual/dmi/id/product_name").strip()) + model = "%s %s" % ( + model, + read_file("/sys/devices/virtual/dmi/id/product_name").strip(), + ) hardware["data"]["model"] = model hardware["details"] = ["diagnosis_basesystem_hardware_model"] yield hardware # Kernel version - kernel_version = read_file('/proc/sys/kernel/osrelease').strip() - yield dict(meta={"test": "kernel"}, - data={"kernel_version": kernel_version}, - status="INFO", - summary="diagnosis_basesystem_kernel") + kernel_version = read_file("/proc/sys/kernel/osrelease").strip() + yield dict( + meta={"test": "kernel"}, + data={"kernel_version": kernel_version}, + status="INFO", + summary="diagnosis_basesystem_kernel", + ) # Debian release debian_version = read_file("/etc/debian_version").strip() - yield dict(meta={"test": "host"}, - data={"debian_version": debian_version}, - status="INFO", - summary="diagnosis_basesystem_host") + yield dict( + meta={"test": "host"}, + data={"debian_version": debian_version}, + status="INFO", + summary="diagnosis_basesystem_host", + ) # Yunohost packages versions # We check if versions are consistent (e.g. all 3.6 and not 3 packages with 3.6 and the other with 3.5) @@ -67,41 +76,62 @@ class BaseSystemDiagnoser(Diagnoser): # Here, ynh_core_version is for example "3.5.4.12", so [:3] is "3.5" and we check it's the same for all packages ynh_packages = ynh_packages_version() ynh_core_version = ynh_packages["yunohost"]["version"] - consistent_versions = all(infos["version"][:3] == ynh_core_version[:3] for infos in ynh_packages.values()) - ynh_version_details = [("diagnosis_basesystem_ynh_single_version", - {"package": package, - "version": infos["version"], - "repo": infos["repo"]} - ) - for package, infos in ynh_packages.items()] + consistent_versions = all( + infos["version"][:3] == ynh_core_version[:3] + for infos in ynh_packages.values() + ) + ynh_version_details = [ + ( + "diagnosis_basesystem_ynh_single_version", + { + "package": package, + "version": infos["version"], + "repo": infos["repo"], + }, + ) + for package, infos in ynh_packages.items() + ] - yield dict(meta={"test": "ynh_versions"}, - data={"main_version": ynh_core_version, "repo": ynh_packages["yunohost"]["repo"]}, - status="INFO" if consistent_versions else "ERROR", - summary="diagnosis_basesystem_ynh_main_version" if consistent_versions else "diagnosis_basesystem_ynh_inconsistent_versions", - details=ynh_version_details) + yield dict( + meta={"test": "ynh_versions"}, + data={ + "main_version": ynh_core_version, + "repo": ynh_packages["yunohost"]["repo"], + }, + status="INFO" if consistent_versions else "ERROR", + summary="diagnosis_basesystem_ynh_main_version" + if consistent_versions + else "diagnosis_basesystem_ynh_inconsistent_versions", + details=ynh_version_details, + ) if self.is_vulnerable_to_meltdown(): - yield dict(meta={"test": "meltdown"}, - status="ERROR", - summary="diagnosis_security_vulnerable_to_meltdown", - details=["diagnosis_security_vulnerable_to_meltdown_details"] - ) + yield dict( + meta={"test": "meltdown"}, + status="ERROR", + summary="diagnosis_security_vulnerable_to_meltdown", + details=["diagnosis_security_vulnerable_to_meltdown_details"], + ) bad_sury_packages = list(self.bad_sury_packages()) if bad_sury_packages: - cmd_to_fix = "apt install --allow-downgrades " \ - + " ".join(["%s=%s" % (package, version) for package, version in bad_sury_packages]) - yield dict(meta={"test": "packages_from_sury"}, - data={"cmd_to_fix": cmd_to_fix}, - status="WARNING", - summary="diagnosis_package_installed_from_sury", - details=["diagnosis_package_installed_from_sury_details"]) + cmd_to_fix = "apt install --allow-downgrades " + " ".join( + ["%s=%s" % (package, version) for package, version in bad_sury_packages] + ) + yield dict( + meta={"test": "packages_from_sury"}, + data={"cmd_to_fix": cmd_to_fix}, + status="WARNING", + summary="diagnosis_package_installed_from_sury", + details=["diagnosis_package_installed_from_sury_details"], + ) if self.backports_in_sources_list(): - yield dict(meta={"test": "backports_in_sources_list"}, - status="WARNING", - summary="diagnosis_backports_in_sources_list") + yield dict( + meta={"test": "backports_in_sources_list"}, + status="WARNING", + summary="diagnosis_backports_in_sources_list", + ) def bad_sury_packages(self): @@ -112,7 +142,10 @@ class BaseSystemDiagnoser(Diagnoser): if os.system(cmd) != 0: continue - cmd = "LC_ALL=C apt policy %s 2>&1 | grep http -B1 | tr -d '*' | grep '+deb' | grep -v 'gbp' | head -n 1 | awk '{print $1}'" % package + cmd = ( + "LC_ALL=C apt policy %s 2>&1 | grep http -B1 | tr -d '*' | grep '+deb' | grep -v 'gbp' | head -n 1 | awk '{print $1}'" + % package + ) version_to_downgrade_to = check_output(cmd) yield (package, version_to_downgrade_to) @@ -136,8 +169,12 @@ class BaseSystemDiagnoser(Diagnoser): cache_file = "/tmp/yunohost-meltdown-diagnosis" dpkg_log = "/var/log/dpkg.log" if os.path.exists(cache_file): - if not os.path.exists(dpkg_log) or os.path.getmtime(cache_file) > os.path.getmtime(dpkg_log): - self.logger_debug("Using cached results for meltdown checker, from %s" % cache_file) + if not os.path.exists(dpkg_log) or os.path.getmtime( + cache_file + ) > os.path.getmtime(dpkg_log): + self.logger_debug( + "Using cached results for meltdown checker, from %s" % cache_file + ) return read_json(cache_file)[0]["VULNERABLE"] # script taken from https://github.com/speed47/spectre-meltdown-checker @@ -149,10 +186,12 @@ class BaseSystemDiagnoser(Diagnoser): # [{"NAME":"MELTDOWN","CVE":"CVE-2017-5754","VULNERABLE":false,"INFOS":"PTI mitigates the vulnerability"}] try: self.logger_debug("Running meltdown vulnerability checker") - call = subprocess.Popen("bash %s --batch json --variant 3" % - SCRIPT_PATH, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + call = subprocess.Popen( + "bash %s --batch json --variant 3" % SCRIPT_PATH, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) # TODO / FIXME : here we are ignoring error messages ... # in particular on RPi2 and other hardware, the script complains about @@ -176,11 +215,17 @@ class BaseSystemDiagnoser(Diagnoser): assert CVEs[0]["NAME"] == "MELTDOWN" except Exception as e: import traceback + traceback.print_exc() - self.logger_warning("Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" % e) + self.logger_warning( + "Something wrong happened when trying to diagnose Meltdown vunerability, exception: %s" + % e + ) raise Exception("Command output for failed meltdown check: '%s'" % output) - self.logger_debug("Writing results from meltdown checker to cache file, %s" % cache_file) + self.logger_debug( + "Writing results from meltdown checker to cache file, %s" % cache_file + ) write_to_json(cache_file, CVEs) return CVEs[0]["VULNERABLE"] diff --git a/data/hooks/diagnosis/10-ip.py b/data/hooks/diagnosis/10-ip.py index b18b4f435..718cc56ad 100644 --- a/data/hooks/diagnosis/10-ip.py +++ b/data/hooks/diagnosis/10-ip.py @@ -28,9 +28,11 @@ class IPDiagnoser(Diagnoser): can_ping_ipv6 = self.can_ping_outside(6) if not can_ping_ipv4 and not can_ping_ipv6: - yield dict(meta={"test": "ping"}, - status="ERROR", - summary="diagnosis_ip_not_connected_at_all") + yield dict( + meta={"test": "ping"}, + status="ERROR", + summary="diagnosis_ip_not_connected_at_all", + ) # Not much else we can do if there's no internet at all return @@ -49,21 +51,29 @@ class IPDiagnoser(Diagnoser): # If it turns out that at the same time, resolvconf is bad, that's probably # the cause of this, so we use a different message in that case if not can_resolve_dns: - yield dict(meta={"test": "dnsresolv"}, - status="ERROR", - summary="diagnosis_ip_broken_dnsresolution" if good_resolvconf else "diagnosis_ip_broken_resolvconf") + yield dict( + meta={"test": "dnsresolv"}, + status="ERROR", + summary="diagnosis_ip_broken_dnsresolution" + if good_resolvconf + else "diagnosis_ip_broken_resolvconf", + ) return # Otherwise, if the resolv conf is bad but we were able to resolve domain name, # still warn that we're using a weird resolv conf ... elif not good_resolvconf: - yield dict(meta={"test": "dnsresolv"}, - status="WARNING", - summary="diagnosis_ip_weird_resolvconf", - details=["diagnosis_ip_weird_resolvconf_details"]) + yield dict( + meta={"test": "dnsresolv"}, + status="WARNING", + summary="diagnosis_ip_weird_resolvconf", + details=["diagnosis_ip_weird_resolvconf_details"], + ) else: - yield dict(meta={"test": "dnsresolv"}, - status="SUCCESS", - summary="diagnosis_ip_dnsresolution_working") + yield dict( + meta={"test": "dnsresolv"}, + status="SUCCESS", + summary="diagnosis_ip_dnsresolution_working", + ) # ##################################################### # # IP DIAGNOSIS : Check that we're actually able to talk # @@ -76,8 +86,11 @@ class IPDiagnoser(Diagnoser): network_interfaces = get_network_interfaces() def get_local_ip(version): - local_ip = {iface: addr[version].split("/")[0] - for iface, addr in network_interfaces.items() if version in addr} + local_ip = { + iface: addr[version].split("/")[0] + for iface, addr in network_interfaces.items() + if version in addr + } if not local_ip: return None elif len(local_ip): @@ -85,23 +98,34 @@ class IPDiagnoser(Diagnoser): else: return local_ip - yield dict(meta={"test": "ipv4"}, - data={"global": ipv4, "local": get_local_ip("ipv4")}, - status="SUCCESS" if ipv4 else "ERROR", - summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", - details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None) + yield dict( + meta={"test": "ipv4"}, + data={"global": ipv4, "local": get_local_ip("ipv4")}, + status="SUCCESS" if ipv4 else "ERROR", + summary="diagnosis_ip_connected_ipv4" if ipv4 else "diagnosis_ip_no_ipv4", + details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv4 else None, + ) - yield dict(meta={"test": "ipv6"}, - data={"global": ipv6, "local": get_local_ip("ipv6")}, - status="SUCCESS" if ipv6 else "WARNING", - summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", - details=["diagnosis_ip_global", "diagnosis_ip_local"] if ipv6 else ["diagnosis_ip_no_ipv6_tip"]) + yield dict( + meta={"test": "ipv6"}, + data={"global": ipv6, "local": get_local_ip("ipv6")}, + status="SUCCESS" if ipv6 else "WARNING", + summary="diagnosis_ip_connected_ipv6" if ipv6 else "diagnosis_ip_no_ipv6", + details=["diagnosis_ip_global", "diagnosis_ip_local"] + if ipv6 + else ["diagnosis_ip_no_ipv6_tip"], + ) # TODO / FIXME : add some attempt to detect ISP (using whois ?) ? def can_ping_outside(self, protocol=4): - assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol) + assert protocol in [ + 4, + 6, + ], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr( + protocol + ) # We can know that ipv6 is not available directly if this file does not exists if protocol == 6 and not os.path.exists("/proc/net/if_inet6"): @@ -115,26 +139,49 @@ class IPDiagnoser(Diagnoser): # But of course IPv6 is more complex ... e.g. on internet cube there's # no default route but a /3 which acts as a default-like route... # e.g. 2000:/3 dev tun0 ... - return r.startswith("default") or (":" in r and re.match(r".*/[0-3]$", r.split()[0])) + return r.startswith("default") or ( + ":" in r and re.match(r".*/[0-3]$", r.split()[0]) + ) + if not any(is_default_route(r) for r in routes): - self.logger_debug("No default route for IPv%s, so assuming there's no IP address for that version" % protocol) + self.logger_debug( + "No default route for IPv%s, so assuming there's no IP address for that version" + % protocol + ) return None # We use the resolver file as a list of well-known, trustable (ie not google ;)) IPs that we can ping - resolver_file = "/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf" - resolvers = [r.split(" ")[1] for r in read_file(resolver_file).split("\n") if r.startswith("nameserver")] + resolver_file = ( + "/usr/share/yunohost/templates/dnsmasq/plain/resolv.dnsmasq.conf" + ) + resolvers = [ + r.split(" ")[1] + for r in read_file(resolver_file).split("\n") + if r.startswith("nameserver") + ] if protocol == 4: resolvers = [r for r in resolvers if ":" not in r] if protocol == 6: resolvers = [r for r in resolvers if ":" in r] - assert resolvers != [], "Uhoh, need at least one IPv%s DNS resolver in %s ..." % (protocol, resolver_file) + assert ( + resolvers != [] + ), "Uhoh, need at least one IPv%s DNS resolver in %s ..." % ( + protocol, + resolver_file, + ) # So let's try to ping the first 4~5 resolvers (shuffled) # If we succesfully ping any of them, we conclude that we are indeed connected def ping(protocol, target): - return os.system("ping%s -c1 -W 3 %s >/dev/null 2>/dev/null" % ("" if protocol == 4 else "6", target)) == 0 + return ( + os.system( + "ping%s -c1 -W 3 %s >/dev/null 2>/dev/null" + % ("" if protocol == 4 else "6", target) + ) + == 0 + ) random.shuffle(resolvers) return any(ping(protocol, resolver) for resolver in resolvers[:5]) @@ -145,7 +192,13 @@ class IPDiagnoser(Diagnoser): def good_resolvconf(self): content = read_file("/etc/resolv.conf").strip().split("\n") # Ignore comments and empty lines - content = [l.strip() for l in content if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("search")] + content = [ + l.strip() + for l in content + if l.strip() + and not l.strip().startswith("#") + and not l.strip().startswith("search") + ] # We should only find a "nameserver 127.0.0.1" return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"] @@ -155,14 +208,21 @@ class IPDiagnoser(Diagnoser): # but if we want to be able to diagnose DNS resolution issues independently from # internet connectivity, we gotta rely on fixed IPs first.... - assert protocol in [4, 6], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr(protocol) + assert protocol in [ + 4, + 6, + ], "Invalid protocol version, it should be either 4 or 6 and was '%s'" % repr( + protocol + ) - url = 'https://ip%s.yunohost.org' % ('6' if protocol == 6 else '') + url = "https://ip%s.yunohost.org" % ("6" if protocol == 6 else "") try: return download_text(url, timeout=30).strip() except Exception as e: - self.logger_debug("Could not get public IPv%s : %s" % (str(protocol), str(e))) + self.logger_debug( + "Could not get public IPv%s : %s" % (str(protocol), str(e)) + ) return None diff --git a/data/hooks/diagnosis/12-dnsrecords.py b/data/hooks/diagnosis/12-dnsrecords.py index cb8193dd5..719ce4d6a 100644 --- a/data/hooks/diagnosis/12-dnsrecords.py +++ b/data/hooks/diagnosis/12-dnsrecords.py @@ -12,7 +12,7 @@ from yunohost.utils.network import dig from yunohost.diagnosis import Diagnoser from yunohost.domain import domain_list, _build_dns_conf, _get_maindomain -YNH_DYNDNS_DOMAINS = ['nohost.me', 'noho.st', 'ynh.fr'] +YNH_DYNDNS_DOMAINS = ["nohost.me", "noho.st", "ynh.fr"] class DNSRecordsDiagnoser(Diagnoser): @@ -29,20 +29,30 @@ class DNSRecordsDiagnoser(Diagnoser): for domain in all_domains: self.logger_debug("Diagnosing DNS conf for %s" % domain) is_subdomain = domain.split(".", 1)[1] in all_domains - for report in self.check_domain(domain, domain == main_domain, is_subdomain=is_subdomain): + for report in self.check_domain( + domain, domain == main_domain, is_subdomain=is_subdomain + ): yield report # Check if a domain buy by the user will expire soon psl = PublicSuffixList() - domains_from_registrar = [psl.get_public_suffix(domain) for domain in all_domains] - domains_from_registrar = [domain for domain in domains_from_registrar if "." in domain] - domains_from_registrar = set(domains_from_registrar) - set(YNH_DYNDNS_DOMAINS + ["netlib.re"]) + domains_from_registrar = [ + psl.get_public_suffix(domain) for domain in all_domains + ] + domains_from_registrar = [ + domain for domain in domains_from_registrar if "." in domain + ] + domains_from_registrar = set(domains_from_registrar) - set( + YNH_DYNDNS_DOMAINS + ["netlib.re"] + ) for report in self.check_expiration_date(domains_from_registrar): yield report def check_domain(self, domain, is_main_domain, is_subdomain): - expected_configuration = _build_dns_conf(domain, include_empty_AAAA_if_no_ipv6=True) + expected_configuration = _build_dns_conf( + domain, include_empty_AAAA_if_no_ipv6=True + ) categories = ["basic", "mail", "xmpp", "extra"] # For subdomains, we only diagnosis A and AAAA records @@ -92,14 +102,19 @@ class DNSRecordsDiagnoser(Diagnoser): status = "SUCCESS" summary = "diagnosis_dns_good_conf" - output = dict(meta={"domain": domain, "category": category}, - data=results, - status=status, - summary=summary) + output = dict( + meta={"domain": domain, "category": category}, + data=results, + status=status, + summary=summary, + ) if discrepancies: # For ynh-managed domains (nohost.me etc...), tell people to try to "yunohost dyndns update --force" - if any(domain.endswith(ynh_dyndns_domain) for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS): + if any( + domain.endswith(ynh_dyndns_domain) + for ynh_dyndns_domain in YNH_DYNDNS_DOMAINS + ): output["details"] = ["diagnosis_dns_try_dyndns_update_force"] # Otherwise point to the documentation else: @@ -134,11 +149,17 @@ class DNSRecordsDiagnoser(Diagnoser): # some DNS registrar sometime split it into several pieces like this: # "p=foo" "bar" (with a space and quotes in the middle)... expected = set(r["value"].strip(';" ').replace(";", " ").split()) - current = set(r["current"].replace('" "', '').strip(';" ').replace(";", " ").split()) + current = set( + r["current"].replace('" "', "").strip(';" ').replace(";", " ").split() + ) # For SPF, ignore parts starting by ip4: or ip6: if r["name"] == "@": - current = {part for part in current if not part.startswith("ip4:") and not part.startswith("ip6:")} + current = { + part + for part in current + if not part.startswith("ip4:") and not part.startswith("ip6:") + } return expected == current elif r["type"] == "MX": # For MX, we want to ignore the priority @@ -153,12 +174,7 @@ class DNSRecordsDiagnoser(Diagnoser): Alert if expiration date of a domain is soon """ - details = { - "not_found": [], - "error": [], - "warning": [], - "success": [] - } + details = {"not_found": [], "error": [], "warning": [], "success": []} for domain in domains: expire_date = self.get_domain_expiration(domain) @@ -167,9 +183,12 @@ class DNSRecordsDiagnoser(Diagnoser): status_ns, _ = dig(domain, "NS", resolvers="force_external") status_a, _ = dig(domain, "A", resolvers="force_external") if "ok" not in [status_ns, status_a]: - details["not_found"].append(( - "diagnosis_domain_%s_details" % (expire_date), - {"domain": domain})) + details["not_found"].append( + ( + "diagnosis_domain_%s_details" % (expire_date), + {"domain": domain}, + ) + ) else: self.logger_debug("Dyndns domain: %s" % (domain)) continue @@ -185,7 +204,7 @@ class DNSRecordsDiagnoser(Diagnoser): args = { "domain": domain, "days": expire_in.days - 1, - "expire_date": str(expire_date) + "expire_date": str(expire_date), } details[alert_type].append(("diagnosis_domain_expires_in", args)) @@ -198,11 +217,15 @@ class DNSRecordsDiagnoser(Diagnoser): # Allow to ignore specifically a single domain if len(details[alert_type]) == 1: meta["domain"] = details[alert_type][0][1]["domain"] - yield dict(meta=meta, - data={}, - status=alert_type.upper() if alert_type != "not_found" else "WARNING", - summary="diagnosis_domain_expiration_" + alert_type, - details=details[alert_type]) + yield dict( + meta=meta, + data={}, + status=alert_type.upper() + if alert_type != "not_found" + else "WARNING", + summary="diagnosis_domain_expiration_" + alert_type, + details=details[alert_type], + ) def get_domain_expiration(self, domain): """ @@ -212,25 +235,28 @@ class DNSRecordsDiagnoser(Diagnoser): out = check_output(command).split("\n") # Reduce output to determine if whois answer is equivalent to NOT FOUND - filtered_out = [line for line in out - if re.search(r'^[a-zA-Z0-9 ]{4,25}:', line, re.IGNORECASE) and - not re.match(r'>>> Last update of whois', line, re.IGNORECASE) and - not re.match(r'^NOTICE:', line, re.IGNORECASE) and - not re.match(r'^%%', line, re.IGNORECASE) and - not re.match(r'"https?:"', line, re.IGNORECASE)] + filtered_out = [ + line + for line in out + if re.search(r"^[a-zA-Z0-9 ]{4,25}:", line, re.IGNORECASE) + and not re.match(r">>> Last update of whois", line, re.IGNORECASE) + and not re.match(r"^NOTICE:", line, re.IGNORECASE) + and not re.match(r"^%%", line, re.IGNORECASE) + and not re.match(r'"https?:"', line, re.IGNORECASE) + ] # If there is less than 7 lines, it's NOT FOUND response if len(filtered_out) <= 6: return "not_found" for line in out: - match = re.search(r'Expir.+(\d{4}-\d{2}-\d{2})', line, re.IGNORECASE) + match = re.search(r"Expir.+(\d{4}-\d{2}-\d{2})", line, re.IGNORECASE) if match is not None: - return datetime.strptime(match.group(1), '%Y-%m-%d') + return datetime.strptime(match.group(1), "%Y-%m-%d") - match = re.search(r'Expir.+(\d{2}-\w{3}-\d{4})', line, re.IGNORECASE) + match = re.search(r"Expir.+(\d{2}-\w{3}-\d{4})", line, re.IGNORECASE) if match is not None: - return datetime.strptime(match.group(1), '%d-%b-%Y') + return datetime.strptime(match.group(1), "%d-%b-%Y") return "expiration_not_found" diff --git a/data/hooks/diagnosis/14-ports.py b/data/hooks/diagnosis/14-ports.py index b74b3907e..6faf29053 100644 --- a/data/hooks/diagnosis/14-ports.py +++ b/data/hooks/diagnosis/14-ports.py @@ -42,16 +42,18 @@ class PortsDiagnoser(Diagnoser): results = {} for ipversion in ipversions: try: - r = Diagnoser.remote_diagnosis('check-ports', - data={'ports': ports.keys()}, - ipversion=ipversion) + r = Diagnoser.remote_diagnosis( + "check-ports", data={"ports": ports.keys()}, ipversion=ipversion + ) results[ipversion] = r["ports"] except Exception as e: - yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, - data={"error": str(e)}, - status="WARNING", - summary="diagnosis_ports_could_not_diagnose", - details=["diagnosis_ports_could_not_diagnose_details"]) + yield dict( + meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_ports_could_not_diagnose", + details=["diagnosis_ports_could_not_diagnose_details"], + ) continue ipversions = results.keys() @@ -64,18 +66,27 @@ class PortsDiagnoser(Diagnoser): # If both IPv4 and IPv6 (if applicable) are good if all(results[ipversion].get(port) is True for ipversion in ipversions): - yield dict(meta={"port": port}, - data={"service": service, "category": category}, - status="SUCCESS", - summary="diagnosis_ports_ok", - details=["diagnosis_ports_needed_by"]) + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="SUCCESS", + summary="diagnosis_ports_ok", + details=["diagnosis_ports_needed_by"], + ) # If both IPv4 and IPv6 (if applicable) are failed - elif all(results[ipversion].get(port) is not True for ipversion in ipversions): - yield dict(meta={"port": port}, - data={"service": service, "category": category}, - status="ERROR", - summary="diagnosis_ports_unreachable", - details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"]) + elif all( + results[ipversion].get(port) is not True for ipversion in ipversions + ): + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="ERROR", + summary="diagnosis_ports_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: passed, failed = (4, 6) if results[4].get(port) is True else (6, 4) @@ -87,29 +98,54 @@ class PortsDiagnoser(Diagnoser): # If any AAAA record is set, IPv6 is important... def ipv6_is_important(): dnsrecords = Diagnoser.get_cached_report("dnsrecords") or {} - return any(record["data"].get("AAAA:@") in ["OK", "WRONG"] for record in dnsrecords.get("items", [])) + return any( + record["data"].get("AAAA:@") in ["OK", "WRONG"] + for record in dnsrecords.get("items", []) + ) if failed == 4 or ipv6_is_important(): - yield dict(meta={"port": port}, - data={"service": service, "category": category, "passed": passed, "failed": failed}, - status="ERROR", - summary="diagnosis_ports_partially_unreachable", - details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"]) + yield dict( + meta={"port": port}, + data={ + "service": service, + "category": category, + "passed": passed, + "failed": failed, + }, + status="ERROR", + summary="diagnosis_ports_partially_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) # So otherwise we report a success # And in addition we report an info about the failure in IPv6 # *with a different meta* (important to avoid conflicts when # fetching the other info...) else: - yield dict(meta={"port": port}, - data={"service": service, "category": category}, - status="SUCCESS", - summary="diagnosis_ports_ok", - details=["diagnosis_ports_needed_by"]) - yield dict(meta={"test": "ipv6", "port": port}, - data={"service": service, "category": category, "passed": passed, "failed": failed}, - status="INFO", - summary="diagnosis_ports_partially_unreachable", - details=["diagnosis_ports_needed_by", "diagnosis_ports_forwarding_tip"]) + yield dict( + meta={"port": port}, + data={"service": service, "category": category}, + status="SUCCESS", + summary="diagnosis_ports_ok", + details=["diagnosis_ports_needed_by"], + ) + yield dict( + meta={"test": "ipv6", "port": port}, + data={ + "service": service, + "category": category, + "passed": passed, + "failed": failed, + }, + status="INFO", + summary="diagnosis_ports_partially_unreachable", + details=[ + "diagnosis_ports_needed_by", + "diagnosis_ports_forwarding_tip", + ], + ) def main(args, env, loggers): diff --git a/data/hooks/diagnosis/21-web.py b/data/hooks/diagnosis/21-web.py index 94665a6e6..f7d8b9a16 100644 --- a/data/hooks/diagnosis/21-web.py +++ b/data/hooks/diagnosis/21-web.py @@ -28,14 +28,16 @@ class WebDiagnoser(Diagnoser): # probably because nginx conf manually modified... nginx_conf = "/etc/nginx/conf.d/%s.conf" % domain if ".well-known/ynh-diagnosis/" not in read_file(nginx_conf): - yield dict(meta={"domain": domain}, - status="WARNING", - summary="diagnosis_http_nginx_conf_not_up_to_date", - details=["diagnosis_http_nginx_conf_not_up_to_date_details"]) + yield dict( + meta={"domain": domain}, + status="WARNING", + summary="diagnosis_http_nginx_conf_not_up_to_date", + details=["diagnosis_http_nginx_conf_not_up_to_date_details"], + ) else: domains_to_check.append(domain) - self.nonce = ''.join(random.choice("0123456789abcedf") for i in range(16)) + self.nonce = "".join(random.choice("0123456789abcedf") for i in range(16)) os.system("rm -rf /tmp/.well-known/ynh-diagnosis/") os.system("mkdir -p /tmp/.well-known/ynh-diagnosis/") os.system("touch /tmp/.well-known/ynh-diagnosis/%s" % self.nonce) @@ -74,10 +76,12 @@ class WebDiagnoser(Diagnoser): try: requests.head("http://" + global_ipv4, timeout=5) except requests.exceptions.Timeout: - yield dict(meta={"test": "hairpinning"}, - status="WARNING", - summary="diagnosis_http_hairpinning_issue", - details=["diagnosis_http_hairpinning_issue_details"]) + yield dict( + meta={"test": "hairpinning"}, + status="WARNING", + summary="diagnosis_http_hairpinning_issue", + details=["diagnosis_http_hairpinning_issue_details"], + ) except: # Well I dunno what to do if that's another exception # type... That'll most probably *not* be an hairpinning @@ -89,17 +93,20 @@ class WebDiagnoser(Diagnoser): results = {} for ipversion in ipversions: try: - r = Diagnoser.remote_diagnosis('check-http', - data={'domains': domains, - "nonce": self.nonce}, - ipversion=ipversion) + r = Diagnoser.remote_diagnosis( + "check-http", + data={"domains": domains, "nonce": self.nonce}, + ipversion=ipversion, + ) results[ipversion] = r["http"] except Exception as e: - yield dict(meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, - data={"error": str(e)}, - status="WARNING", - summary="diagnosis_http_could_not_diagnose", - details=["diagnosis_http_could_not_diagnose_details"]) + yield dict( + meta={"reason": "remote_diagnosis_failed", "ipversion": ipversion}, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_http_could_not_diagnose", + details=["diagnosis_http_could_not_diagnose_details"], + ) continue ipversions = results.keys() @@ -109,22 +116,32 @@ class WebDiagnoser(Diagnoser): for domain in domains: # If both IPv4 and IPv6 (if applicable) are good - if all(results[ipversion][domain]["status"] == "ok" for ipversion in ipversions): + if all( + results[ipversion][domain]["status"] == "ok" for ipversion in ipversions + ): if 4 in ipversions: self.do_hairpinning_test = True - yield dict(meta={"domain": domain}, - status="SUCCESS", - summary="diagnosis_http_ok") + yield dict( + meta={"domain": domain}, + status="SUCCESS", + summary="diagnosis_http_ok", + ) # If both IPv4 and IPv6 (if applicable) are failed - elif all(results[ipversion][domain]["status"] != "ok" for ipversion in ipversions): + elif all( + results[ipversion][domain]["status"] != "ok" for ipversion in ipversions + ): detail = results[4 if 4 in ipversions else 6][domain]["status"] - yield dict(meta={"domain": domain}, - status="ERROR", - summary="diagnosis_http_unreachable", - details=[detail.replace("error_http_check", "diagnosis_http")]) + yield dict( + meta={"domain": domain}, + status="ERROR", + summary="diagnosis_http_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) # If only IPv4 is failed or only IPv6 is failed (if applicable) else: - passed, failed = (4, 6) if results[4][domain]["status"] == "ok" else (6, 4) + passed, failed = ( + (4, 6) if results[4][domain]["status"] == "ok" else (6, 4) + ) detail = results[failed][domain]["status"] # Failing in ipv4 is critical. @@ -132,17 +149,24 @@ class WebDiagnoser(Diagnoser): # It's an acceptable situation and we shall not report an # error def ipv6_is_important_for_this_domain(): - dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "basic"}) or {} + dnsrecords = ( + Diagnoser.get_cached_report( + "dnsrecords", item={"domain": domain, "category": "basic"} + ) + or {} + ) AAAA_status = dnsrecords.get("data", {}).get("AAAA:@") return AAAA_status in ["OK", "WRONG"] if failed == 4 or ipv6_is_important_for_this_domain(): - yield dict(meta={"domain": domain}, - data={"passed": passed, "failed": failed}, - status="ERROR", - summary="diagnosis_http_partially_unreachable", - details=[detail.replace("error_http_check", "diagnosis_http")]) + yield dict( + meta={"domain": domain}, + data={"passed": passed, "failed": failed}, + status="ERROR", + summary="diagnosis_http_partially_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) # So otherwise we report a success (note that this info is # later used to know that ACME challenge is doable) # @@ -151,14 +175,18 @@ class WebDiagnoser(Diagnoser): # fetching the other info...) else: self.do_hairpinning_test = True - yield dict(meta={"domain": domain}, - status="SUCCESS", - summary="diagnosis_http_ok") - yield dict(meta={"test": "ipv6", "domain": domain}, - data={"passed": passed, "failed": failed}, - status="INFO", - summary="diagnosis_http_partially_unreachable", - details=[detail.replace("error_http_check", "diagnosis_http")]) + yield dict( + meta={"domain": domain}, + status="SUCCESS", + summary="diagnosis_http_ok", + ) + yield dict( + meta={"test": "ipv6", "domain": domain}, + data={"passed": passed, "failed": failed}, + status="INFO", + summary="diagnosis_http_partially_unreachable", + details=[detail.replace("error_http_check", "diagnosis_http")], + ) def main(args, env, loggers): diff --git a/data/hooks/diagnosis/24-mail.py b/data/hooks/diagnosis/24-mail.py index ca35d53ad..63f685a26 100644 --- a/data/hooks/diagnosis/24-mail.py +++ b/data/hooks/diagnosis/24-mail.py @@ -34,8 +34,13 @@ class MailDiagnoser(Diagnoser): # TODO Validate DKIM and dmarc ? # TODO check that the recent mail logs are not filled with thousand of email sending (unusual number of mail sent) # TODO check for unusual failed sending attempt being refused in the logs ? - checks = ["check_outgoing_port_25", "check_ehlo", "check_fcrdns", - "check_blacklist", "check_queue"] + checks = [ + "check_outgoing_port_25", + "check_ehlo", + "check_fcrdns", + "check_blacklist", + "check_queue", + ] for check in checks: self.logger_debug("Running " + check) reports = list(getattr(self, check)()) @@ -43,9 +48,11 @@ class MailDiagnoser(Diagnoser): yield report if not reports: name = check[6:] - yield dict(meta={"test": "mail_" + name}, - status="SUCCESS", - summary="diagnosis_mail_" + name + "_ok") + yield dict( + meta={"test": "mail_" + name}, + status="SUCCESS", + summary="diagnosis_mail_" + name + "_ok", + ) def check_outgoing_port_25(self): """ @@ -54,14 +61,20 @@ class MailDiagnoser(Diagnoser): """ for ipversion in self.ipversions: - cmd = '/bin/nc -{ipversion} -z -w2 yunohost.org 25'.format(ipversion=ipversion) + cmd = "/bin/nc -{ipversion} -z -w2 yunohost.org 25".format( + ipversion=ipversion + ) if os.system(cmd) != 0: - yield dict(meta={"test": "outgoing_port_25", "ipversion": ipversion}, - data={}, - status="ERROR", - summary="diagnosis_mail_outgoing_port_25_blocked", - details=["diagnosis_mail_outgoing_port_25_blocked_details", - "diagnosis_mail_outgoing_port_25_blocked_relay_vpn"]) + yield dict( + meta={"test": "outgoing_port_25", "ipversion": ipversion}, + data={}, + status="ERROR", + summary="diagnosis_mail_outgoing_port_25_blocked", + details=[ + "diagnosis_mail_outgoing_port_25_blocked_details", + "diagnosis_mail_outgoing_port_25_blocked_relay_vpn", + ], + ) def check_ehlo(self): """ @@ -71,31 +84,40 @@ class MailDiagnoser(Diagnoser): for ipversion in self.ipversions: try: - r = Diagnoser.remote_diagnosis('check-smtp', - data={}, - ipversion=ipversion) + r = Diagnoser.remote_diagnosis( + "check-smtp", data={}, ipversion=ipversion + ) except Exception as e: - yield dict(meta={"test": "mail_ehlo", "reason": "remote_server_failed", - "ipversion": ipversion}, - data={"error": str(e)}, - status="WARNING", - summary="diagnosis_mail_ehlo_could_not_diagnose", - details=["diagnosis_mail_ehlo_could_not_diagnose_details"]) + yield dict( + meta={ + "test": "mail_ehlo", + "reason": "remote_server_failed", + "ipversion": ipversion, + }, + data={"error": str(e)}, + status="WARNING", + summary="diagnosis_mail_ehlo_could_not_diagnose", + details=["diagnosis_mail_ehlo_could_not_diagnose_details"], + ) continue if r["status"] != "ok": summary = r["status"].replace("error_smtp_", "diagnosis_mail_ehlo_") - yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion}, - data={}, - status="ERROR", - summary=summary, - details=[summary + "_details"]) + yield dict( + meta={"test": "mail_ehlo", "ipversion": ipversion}, + data={}, + status="ERROR", + summary=summary, + details=[summary + "_details"], + ) elif r["helo"] != self.ehlo_domain: - yield dict(meta={"test": "mail_ehlo", "ipversion": ipversion}, - data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain}, - status="ERROR", - summary="diagnosis_mail_ehlo_wrong", - details=["diagnosis_mail_ehlo_wrong_details"]) + yield dict( + meta={"test": "mail_ehlo", "ipversion": ipversion}, + data={"wrong_ehlo": r["helo"], "right_ehlo": self.ehlo_domain}, + status="ERROR", + summary="diagnosis_mail_ehlo_wrong", + details=["diagnosis_mail_ehlo_wrong_details"], + ) def check_fcrdns(self): """ @@ -107,43 +129,55 @@ class MailDiagnoser(Diagnoser): for ip in self.ips: if ":" in ip: ipversion = 6 - details = ["diagnosis_mail_fcrdns_nok_details", - "diagnosis_mail_fcrdns_nok_alternatives_6"] + details = [ + "diagnosis_mail_fcrdns_nok_details", + "diagnosis_mail_fcrdns_nok_alternatives_6", + ] else: ipversion = 4 - details = ["diagnosis_mail_fcrdns_nok_details", - "diagnosis_mail_fcrdns_nok_alternatives_4"] + details = [ + "diagnosis_mail_fcrdns_nok_details", + "diagnosis_mail_fcrdns_nok_alternatives_4", + ] rev = dns.reversename.from_address(ip) subdomain = str(rev.split(3)[0]) query = subdomain if ipversion == 4: - query += '.in-addr.arpa' + query += ".in-addr.arpa" else: - query += '.ip6.arpa' + query += ".ip6.arpa" # Do the DNS Query - status, value = dig(query, 'PTR', resolvers="force_external") + status, value = dig(query, "PTR", resolvers="force_external") if status == "nok": - yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion}, - data={"ip": ip, "ehlo_domain": self.ehlo_domain}, - status="ERROR", - summary="diagnosis_mail_fcrdns_dns_missing", - details=details) + yield dict( + meta={"test": "mail_fcrdns", "ipversion": ipversion}, + data={"ip": ip, "ehlo_domain": self.ehlo_domain}, + status="ERROR", + summary="diagnosis_mail_fcrdns_dns_missing", + details=details, + ) continue - rdns_domain = '' + rdns_domain = "" if len(value) > 0: - rdns_domain = value[0][:-1] if value[0].endswith('.') else value[0] + rdns_domain = value[0][:-1] if value[0].endswith(".") else value[0] if rdns_domain != self.ehlo_domain: - details = ["diagnosis_mail_fcrdns_different_from_ehlo_domain_details"] + details - yield dict(meta={"test": "mail_fcrdns", "ipversion": ipversion}, - data={"ip": ip, - "ehlo_domain": self.ehlo_domain, - "rdns_domain": rdns_domain}, - status="ERROR", - summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", - details=details) + details = [ + "diagnosis_mail_fcrdns_different_from_ehlo_domain_details" + ] + details + yield dict( + meta={"test": "mail_fcrdns", "ipversion": ipversion}, + data={ + "ip": ip, + "ehlo_domain": self.ehlo_domain, + "rdns_domain": rdns_domain, + }, + status="ERROR", + summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", + details=details, + ) def check_blacklist(self): """ @@ -156,9 +190,9 @@ class MailDiagnoser(Diagnoser): for blacklist in dns_blacklists: item_type = "domain" if ":" in item: - item_type = 'ipv6' - elif re.match(r'^\d+\.\d+\.\d+\.\d+$', item): - item_type = 'ipv4' + item_type = "ipv6" + elif re.match(r"^\d+\.\d+\.\d+\.\d+$", item): + item_type = "ipv4" if not blacklist[item_type]: continue @@ -168,58 +202,73 @@ class MailDiagnoser(Diagnoser): if item_type != "domain": rev = dns.reversename.from_address(item) subdomain = str(rev.split(3)[0]) - query = subdomain + '.' + blacklist['dns_server'] + query = subdomain + "." + blacklist["dns_server"] # Do the DNS Query - status, _ = dig(query, 'A') - if status != 'ok': + status, _ = dig(query, "A") + if status != "ok": continue # Try to get the reason details = [] - status, answers = dig(query, 'TXT') + status, answers = dig(query, "TXT") reason = "-" - if status == 'ok': - reason = ', '.join(answers) + if status == "ok": + reason = ", ".join(answers) details.append("diagnosis_mail_blacklist_reason") details.append("diagnosis_mail_blacklist_website") - yield dict(meta={"test": "mail_blacklist", "item": item, - "blacklist": blacklist["dns_server"]}, - data={'blacklist_name': blacklist['name'], - 'blacklist_website': blacklist['website'], - 'reason': reason}, - status="ERROR", - summary='diagnosis_mail_blacklist_listed_by', - details=details) + yield dict( + meta={ + "test": "mail_blacklist", + "item": item, + "blacklist": blacklist["dns_server"], + }, + data={ + "blacklist_name": blacklist["name"], + "blacklist_website": blacklist["website"], + "reason": reason, + }, + status="ERROR", + summary="diagnosis_mail_blacklist_listed_by", + details=details, + ) def check_queue(self): """ Check mail queue is not filled with hundreds of email pending """ - command = 'postqueue -p | grep -v "Mail queue is empty" | grep -c "^[A-Z0-9]" || true' + command = ( + 'postqueue -p | grep -v "Mail queue is empty" | grep -c "^[A-Z0-9]" || true' + ) try: output = check_output(command) pending_emails = int(output) except (ValueError, CalledProcessError) as e: - yield dict(meta={"test": "mail_queue"}, - data={"error": str(e)}, - status="ERROR", - summary="diagnosis_mail_queue_unavailable", - details="diagnosis_mail_queue_unavailable_details") + yield dict( + meta={"test": "mail_queue"}, + data={"error": str(e)}, + status="ERROR", + summary="diagnosis_mail_queue_unavailable", + details="diagnosis_mail_queue_unavailable_details", + ) else: if pending_emails > 100: - yield dict(meta={"test": "mail_queue"}, - data={'nb_pending': pending_emails}, - status="WARNING", - summary="diagnosis_mail_queue_too_big") + yield dict( + meta={"test": "mail_queue"}, + data={"nb_pending": pending_emails}, + status="WARNING", + summary="diagnosis_mail_queue_too_big", + ) else: - yield dict(meta={"test": "mail_queue"}, - data={'nb_pending': pending_emails}, - status="SUCCESS", - summary="diagnosis_mail_queue_ok") + yield dict( + meta={"test": "mail_queue"}, + data={"nb_pending": pending_emails}, + status="SUCCESS", + summary="diagnosis_mail_queue_ok", + ) def get_ips_checked(self): outgoing_ipversions = [] diff --git a/data/hooks/diagnosis/30-services.py b/data/hooks/diagnosis/30-services.py index 562d48d6d..adbcc73b9 100644 --- a/data/hooks/diagnosis/30-services.py +++ b/data/hooks/diagnosis/30-services.py @@ -18,8 +18,13 @@ class ServicesDiagnoser(Diagnoser): for service, result in sorted(all_result.items()): - item = dict(meta={"service": service}, - data={"status": result["status"], "configuration": result["configuration"]}) + item = dict( + meta={"service": service}, + data={ + "status": result["status"], + "configuration": result["configuration"], + }, + ) if result["status"] != "running": item["status"] = "ERROR" if result["status"] != "unknown" else "WARNING" diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index fdda2c2f0..1e8e2201a 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -17,7 +17,7 @@ class SystemResourcesDiagnoser(Diagnoser): def run(self): - MB = 1024**2 + MB = 1024 ** 2 GB = MB * 1024 # @@ -26,10 +26,14 @@ class SystemResourcesDiagnoser(Diagnoser): ram = psutil.virtual_memory() ram_available_percent = 100 * ram.available / ram.total - item = dict(meta={"test": "ram"}, - data={"total": human_size(ram.total), - "available": human_size(ram.available), - "available_percent": round_(ram_available_percent)}) + item = dict( + meta={"test": "ram"}, + data={ + "total": human_size(ram.total), + "available": human_size(ram.available), + "available_percent": round_(ram_available_percent), + }, + ) if ram.available < 100 * MB or ram_available_percent < 5: item["status"] = "ERROR" @@ -47,8 +51,10 @@ class SystemResourcesDiagnoser(Diagnoser): # swap = psutil.swap_memory() - item = dict(meta={"test": "swap"}, - data={"total": human_size(swap.total), "recommended": "512 MiB"}) + item = dict( + meta={"test": "swap"}, + data={"total": human_size(swap.total), "recommended": "512 MiB"}, + ) if swap.total <= 1 * MB: item["status"] = "INFO" item["summary"] = "diagnosis_swap_none" @@ -70,7 +76,9 @@ class SystemResourcesDiagnoser(Diagnoser): disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) # Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/) - disk_partitions = [d for d in disk_partitions if not d.device.startswith("/dev/loop")] + disk_partitions = [ + d for d in disk_partitions if not d.device.startswith("/dev/loop") + ] for disk_partition in disk_partitions: device = disk_partition.device @@ -79,22 +87,30 @@ class SystemResourcesDiagnoser(Diagnoser): usage = psutil.disk_usage(mountpoint) free_percent = 100 - round_(usage.percent) - item = dict(meta={"test": "diskusage", "mountpoint": mountpoint}, - data={"device": device, - # N.B.: we do not use usage.total because we want - # to take into account the 5% security margin - # correctly (c.f. the doc of psutil ...) - "total": human_size(usage.used + usage.free), - "free": human_size(usage.free), - "free_percent": free_percent}) + item = dict( + meta={"test": "diskusage", "mountpoint": mountpoint}, + data={ + "device": device, + # N.B.: we do not use usage.total because we want + # to take into account the 5% security margin + # correctly (c.f. the doc of psutil ...) + "total": human_size(usage.used + usage.free), + "free": human_size(usage.free), + "free_percent": free_percent, + }, + ) # We have an additional absolute constrain on / and /var because # system partitions are critical, having them full may prevent # upgrades etc... - if free_percent < 2.5 or (mountpoint in ["/", "/var"] and usage.free < 1 * GB): + if free_percent < 2.5 or ( + mountpoint in ["/", "/var"] and usage.free < 1 * GB + ): item["status"] = "ERROR" item["summary"] = "diagnosis_diskusage_verylow" - elif free_percent < 5 or (mountpoint in ["/", "/var"] and usage.free < 2 * GB): + elif free_percent < 5 or ( + mountpoint in ["/", "/var"] and usage.free < 2 * GB + ): item["status"] = "WARNING" item["summary"] = "diagnosis_diskusage_low" else: @@ -110,18 +126,26 @@ class SystemResourcesDiagnoser(Diagnoser): # which later causes issue when it gets full... # - main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ['/', '/var']] - main_space = sum([psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions]) + main_disk_partitions = [ + d for d in disk_partitions if d.mountpoint in ["/", "/var"] + ] + main_space = sum( + [psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions] + ) if main_space < 10 * GB: - yield dict(meta={"test": "rootfstotalspace"}, - data={"space": human_size(main_space)}, - status="ERROR", - summary="diagnosis_rootfstotalspace_critical") + yield dict( + meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="ERROR", + summary="diagnosis_rootfstotalspace_critical", + ) if main_space < 14 * GB: - yield dict(meta={"test": "rootfstotalspace"}, - data={"space": human_size(main_space)}, - status="WARNING", - summary="diagnosis_rootfstotalspace_warning") + yield dict( + meta={"test": "rootfstotalspace"}, + data={"space": human_size(main_space)}, + status="WARNING", + summary="diagnosis_rootfstotalspace_warning", + ) # # Recent kills by oom_reaper @@ -129,12 +153,16 @@ class SystemResourcesDiagnoser(Diagnoser): kills_count = self.recent_kills_by_oom_reaper() if kills_count: - kills_summary = "\n".join(["%s (x%s)" % (proc, count) for proc, count in kills_count]) + kills_summary = "\n".join( + ["%s (x%s)" % (proc, count) for proc, count in kills_count] + ) - yield dict(meta={"test": "oom_reaper"}, - status="WARNING", - summary="diagnosis_processes_killed_by_oom_reaper", - data={"kills_summary": kills_summary}) + yield dict( + meta={"test": "oom_reaper"}, + status="WARNING", + summary="diagnosis_processes_killed_by_oom_reaper", + data={"kills_summary": kills_summary}, + ) def recent_kills_by_oom_reaper(self): if not os.path.exists("/var/log/kern.log"): @@ -152,7 +180,7 @@ class SystemResourcesDiagnoser(Diagnoser): # Lines look like : # Aug 25 18:48:21 yolo kernel: [ 9623.613667] oom_reaper: reaped process 11509 (uwsgi), now anon-rss:0kB, file-rss:0kB, shmem-rss:328kB date_str = str(now.year) + " " + " ".join(line.split()[:3]) - date = datetime.datetime.strptime(date_str, '%Y %b %d %H:%M:%S') + date = datetime.datetime.strptime(date_str, "%Y %b %d %H:%M:%S") diff = now - date if diff.days >= 1: break @@ -160,7 +188,9 @@ class SystemResourcesDiagnoser(Diagnoser): yield process_killed processes = list(analyzed_kern_log()) - kills_count = [(p, len([p_ for p_ in processes if p_ == p])) for p in set(processes)] + kills_count = [ + (p, len([p_ for p_ in processes if p_ == p])) for p in set(processes) + ] kills_count = sorted(kills_count, key=lambda p: p[1], reverse=True) return kills_count @@ -168,11 +198,11 @@ class SystemResourcesDiagnoser(Diagnoser): def human_size(bytes_): # Adapted from https://stackoverflow.com/a/1094933 - for unit in ['', 'ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + for unit in ["", "ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]: if abs(bytes_) < 1024.0: return "%s %sB" % (round_(bytes_), unit) bytes_ /= 1024.0 - return "%s %sB" % (round_(bytes_), 'Yi') + return "%s %sB" % (round_(bytes_), "Yi") def round_(n): diff --git a/data/hooks/diagnosis/70-regenconf.py b/data/hooks/diagnosis/70-regenconf.py index 396f64a0f..5ab1e3808 100644 --- a/data/hooks/diagnosis/70-regenconf.py +++ b/data/hooks/diagnosis/70-regenconf.py @@ -17,17 +17,23 @@ class RegenconfDiagnoser(Diagnoser): regenconf_modified_files = list(self.manually_modified_files()) if not regenconf_modified_files: - yield dict(meta={"test": "regenconf"}, - status="SUCCESS", - summary="diagnosis_regenconf_allgood" - ) + yield dict( + meta={"test": "regenconf"}, + status="SUCCESS", + summary="diagnosis_regenconf_allgood", + ) else: for f in regenconf_modified_files: - yield dict(meta={"test": "regenconf", "category": f['category'], "file": f['path']}, - status="WARNING", - summary="diagnosis_regenconf_manually_modified", - details=["diagnosis_regenconf_manually_modified_details"] - ) + yield dict( + meta={ + "test": "regenconf", + "category": f["category"], + "file": f["path"], + }, + status="WARNING", + summary="diagnosis_regenconf_manually_modified", + details=["diagnosis_regenconf_manually_modified_details"], + ) def manually_modified_files(self): diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 908d6300e..a07f51ffe 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -5,21 +5,29 @@ import glob import datetime import subprocess + def get_current_commit(): - p = subprocess.Popen("git rev-parse --verify HEAD", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p = subprocess.Popen( + "git rev-parse --verify HEAD", + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) stdout, stderr = p.communicate() - current_commit = stdout.strip().decode('utf-8') + current_commit = stdout.strip().decode("utf-8") return current_commit + def render(helpers): current_commit = get_current_commit() - data = {"helpers": helpers, - "date": datetime.datetime.now().strftime("%m/%d/%Y"), - "version": open("../debian/changelog").readlines()[0].split()[1].strip("()") - } + data = { + "helpers": helpers, + "date": datetime.datetime.now().strftime("%m/%d/%Y"), + "version": open("../debian/changelog").readlines()[0].split()[1].strip("()"), + } from jinja2 import Template from ansi2html import Ansi2HTMLConverter @@ -33,15 +41,20 @@ def render(helpers): template = open("helper_doc_template.html", "r").read() t = Template(template) - t.globals['now'] = datetime.datetime.utcnow - result = t.render(current_commit=current_commit, data=data, convert=shell_to_html, shell_css=shell_css) + t.globals["now"] = datetime.datetime.utcnow + result = t.render( + current_commit=current_commit, + data=data, + convert=shell_to_html, + shell_css=shell_css, + ) open("helpers.html", "w").write(result) + ############################################################################## -class Parser(): - +class Parser: def __init__(self, filename): self.filename = filename @@ -53,10 +66,7 @@ class Parser(): self.blocks = [] current_reading = "void" - current_block = {"name": None, - "line": -1, - "comments": [], - "code": []} + current_block = {"name": None, "line": -1, "comments": [], "code": []} for i, line in enumerate(self.file): @@ -73,7 +83,7 @@ class Parser(): current_block["comments"].append(line[2:]) else: pass - #assert line == "", malformed_error(i) + # assert line == "", malformed_error(i) continue elif current_reading == "comments": @@ -84,11 +94,12 @@ class Parser(): elif line.strip() == "": # Well eh that was not an actual helper definition ... start over ? current_reading = "void" - current_block = {"name": None, - "line": -1, - "comments": [], - "code": [] - } + current_block = { + "name": None, + "line": -1, + "comments": [], + "code": [], + } elif not (line.endswith("{") or line.endswith("()")): # Well we're not actually entering a function yet eh # (c.f. global vars) @@ -96,7 +107,10 @@ class Parser(): else: # We're getting out of a comment bloc, we should find # the name of the function - assert len(line.split()) >= 1, "Malformed line %s in %s" % (i, self.filename) + assert len(line.split()) >= 1, "Malformed line %s in %s" % ( + i, + self.filename, + ) current_block["line"] = i current_block["name"] = line.split()[0].strip("(){") # Then we expect to read the function @@ -112,10 +126,12 @@ class Parser(): # (we ignore helpers containing [internal] ...) if not "[internal]" in current_block["comments"]: self.blocks.append(current_block) - current_block = {"name": None, - "line": -1, - "comments": [], - "code": []} + current_block = { + "name": None, + "line": -1, + "comments": [], + "code": [], + } else: current_block["code"].append(line) @@ -129,7 +145,7 @@ class Parser(): b["args"] = [] b["ret"] = "" - subblocks = '\n'.join(b["comments"]).split("\n\n") + subblocks = "\n".join(b["comments"]).split("\n\n") for i, subblock in enumerate(subblocks): subblock = subblock.strip() @@ -192,7 +208,7 @@ class Parser(): def is_global_comment(line): - return line.startswith('#') + return line.startswith("#") def malformed_error(line_number): diff --git a/doc/generate_manpages.py b/doc/generate_manpages.py index 67d51e024..f681af7dd 100644 --- a/doc/generate_manpages.py +++ b/doc/generate_manpages.py @@ -22,20 +22,24 @@ template = Template(open(os.path.join(base_path, "manpage.template")).read()) THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, '../data/actionsmap/yunohost.yml') +ACTIONSMAP_FILE = os.path.join(THIS_SCRIPT_DIR, "../data/actionsmap/yunohost.yml") def ordered_yaml_load(stream): class OrderedLoader(yaml.Loader): pass + OrderedLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, - lambda loader, node: OrderedDict(loader.construct_pairs(node))) + lambda loader, node: OrderedDict(loader.construct_pairs(node)), + ) return yaml.load(stream, OrderedLoader) def main(): - parser = argparse.ArgumentParser(description="generate yunohost manpage based on actionsmap.yml") + parser = argparse.ArgumentParser( + description="generate yunohost manpage based on actionsmap.yml" + ) parser.add_argument("-o", "--output", default="output/yunohost") parser.add_argument("-z", "--gzip", action="store_true", default=False) @@ -55,7 +59,7 @@ def main(): output_path = args.output # man pages of "yunohost *" - with open(ACTIONSMAP_FILE, 'r') as actionsmap: + with open(ACTIONSMAP_FILE, "r") as actionsmap: # Getting the dictionary containning what actions are possible per domain actionsmap = ordered_yaml_load(actionsmap) @@ -81,5 +85,5 @@ def main(): output.write(result.encode()) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index a21dad2b9..2f6d400c8 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -11,7 +11,7 @@ from moulinette.interfaces.cli import colorize, get_locale def is_installed(): - return os.path.isfile('/etc/yunohost/installed') + return os.path.isfile("/etc/yunohost/installed") def cli(debug, quiet, output_as, timeout, args, parser): @@ -22,12 +22,7 @@ def cli(debug, quiet, output_as, timeout, args, parser): if not is_installed(): check_command_is_valid_before_postinstall(args) - ret = moulinette.cli( - args, - output_as=output_as, - timeout=timeout, - top_parser=parser - ) + ret = moulinette.cli(args, output_as=output_as, timeout=timeout, top_parser=parser) sys.exit(ret) @@ -36,7 +31,7 @@ def api(debug, host, port): init_logging(interface="api", debug=debug) def is_installed_api(): - return {'installed': is_installed()} + return {"installed": is_installed()} # FIXME : someday, maybe find a way to disable route /postinstall if # postinstall already done ... @@ -44,23 +39,25 @@ def api(debug, host, port): ret = moulinette.api( host=host, port=port, - routes={('GET', '/installed'): is_installed_api}, + routes={("GET", "/installed"): is_installed_api}, ) sys.exit(ret) def check_command_is_valid_before_postinstall(args): - allowed_if_not_postinstalled = ['tools postinstall', - 'tools versions', - 'tools shell', - 'backup list', - 'backup restore', - 'log display'] + allowed_if_not_postinstalled = [ + "tools postinstall", + "tools versions", + "tools shell", + "backup list", + "backup restore", + "log display", + ] - if (len(args) < 2 or (args[0] + ' ' + args[1] not in allowed_if_not_postinstalled)): + if len(args) < 2 or (args[0] + " " + args[1] not in allowed_if_not_postinstalled): init_i18n() - print(colorize(m18n.g('error'), 'red') + " " + m18n.n('yunohost_not_installed')) + print(colorize(m18n.g("error"), "red") + " " + m18n.n("yunohost_not_installed")) sys.exit(1) @@ -72,6 +69,7 @@ def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): init_logging(interface=interface, debug=debug, quiet=quiet, logdir=logdir) init_i18n() from moulinette.core import MoulinetteLock + lock = MoulinetteLock("yunohost", timeout=30) lock.acquire() return lock @@ -80,14 +78,11 @@ def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): def init_i18n(): # This should only be called when not willing to go through moulinette.cli # or moulinette.api but still willing to call m18n.n/g... - m18n.load_namespace('yunohost') + m18n.load_namespace("yunohost") m18n.set_locale(get_locale()) -def init_logging(interface="cli", - debug=False, - quiet=False, - logdir="/var/log/yunohost"): +def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"): logfile = os.path.join(logdir, "yunohost-%s.log" % interface) @@ -98,110 +93,112 @@ def init_logging(interface="cli", # Logging configuration for CLI (or any other interface than api...) # # ####################################################################### # if interface != "api": - configure_logging({ - 'version': 1, - 'main_logger': "yunohost", - 'disable_existing_loggers': True, - 'formatters': { - 'tty-debug': { - 'format': '%(relativeCreated)-4d %(fmessage)s' + configure_logging( + { + "version": 1, + "main_logger": "yunohost", + "disable_existing_loggers": True, + "formatters": { + "tty-debug": {"format": "%(relativeCreated)-4d %(fmessage)s"}, + "precise": { + "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" + }, }, - 'precise': { - 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + "filters": { + "action": { + "()": "moulinette.utils.log.ActionFilter", + }, }, - }, - 'filters': { - 'action': { - '()': 'moulinette.utils.log.ActionFilter', + "handlers": { + "tty": { + "level": "DEBUG" if debug else "INFO", + "class": "moulinette.interfaces.cli.TTYHandler", + "formatter": "tty-debug" if debug else "", + }, + "file": { + "class": "logging.FileHandler", + "formatter": "precise", + "filename": logfile, + "filters": ["action"], + }, }, - }, - 'handlers': { - 'tty': { - 'level': 'DEBUG' if debug else 'INFO', - 'class': 'moulinette.interfaces.cli.TTYHandler', - 'formatter': 'tty-debug' if debug else '', + "loggers": { + "yunohost": { + "level": "DEBUG", + "handlers": ["file", "tty"] if not quiet else ["file"], + "propagate": False, + }, + "moulinette": { + "level": "DEBUG", + "handlers": [], + "propagate": True, + }, + "moulinette.interface": { + "level": "DEBUG", + "handlers": ["file", "tty"] if not quiet else ["file"], + "propagate": False, + }, }, - 'file': { - 'class': 'logging.FileHandler', - 'formatter': 'precise', - 'filename': logfile, - 'filters': ['action'], + "root": { + "level": "DEBUG", + "handlers": ["file", "tty"] if debug else ["file"], }, - }, - 'loggers': { - 'yunohost': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - 'moulinette': { - 'level': 'DEBUG', - 'handlers': [], - 'propagate': True, - }, - 'moulinette.interface': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if not quiet else ['file'], - 'propagate': False, - }, - }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['file', 'tty'] if debug else ['file'], - }, - }) + } + ) # ####################################################################### # # Logging configuration for API # # ####################################################################### # else: - configure_logging({ - 'version': 1, - 'disable_existing_loggers': True, - 'formatters': { - 'console': { - 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + configure_logging( + { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "console": { + "format": "%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" + }, + "precise": { + "format": "%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s" + }, }, - 'precise': { - 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + "filters": { + "action": { + "()": "moulinette.utils.log.ActionFilter", + }, }, - }, - 'filters': { - 'action': { - '()': 'moulinette.utils.log.ActionFilter', + "handlers": { + "api": { + "level": "DEBUG" if debug else "INFO", + "class": "moulinette.interfaces.api.APIQueueHandler", + }, + "file": { + "class": "logging.handlers.WatchedFileHandler", + "formatter": "precise", + "filename": logfile, + "filters": ["action"], + }, + "console": { + "class": "logging.StreamHandler", + "formatter": "console", + "stream": "ext://sys.stdout", + "filters": ["action"], + }, }, - }, - 'handlers': { - 'api': { - 'level': 'DEBUG' if debug else 'INFO', - 'class': 'moulinette.interfaces.api.APIQueueHandler', + "loggers": { + "yunohost": { + "level": "DEBUG", + "handlers": ["file", "api"] + (["console"] if debug else []), + "propagate": False, + }, + "moulinette": { + "level": "DEBUG", + "handlers": [], + "propagate": True, + }, }, - 'file': { - 'class': 'logging.handlers.WatchedFileHandler', - 'formatter': 'precise', - 'filename': logfile, - 'filters': ['action'], + "root": { + "level": "DEBUG", + "handlers": ["file"] + (["console"] if debug else []), }, - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'console', - 'stream': 'ext://sys.stdout', - 'filters': ['action'], - }, - }, - 'loggers': { - 'yunohost': { - 'level': 'DEBUG', - 'handlers': ['file', 'api'] + (['console'] if debug else []), - 'propagate': False, - }, - 'moulinette': { - 'level': 'DEBUG', - 'handlers': [], - 'propagate': True, - }, - }, - 'root': { - 'level': 'DEBUG', - 'handlers': ['file'] + (['console'] if debug else []), - }, - }) + } + ) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index bb3e9a285..a5055a246 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -40,28 +40,39 @@ from moulinette import msignals, m18n, msettings from moulinette.utils.log import getActionLogger from moulinette.utils.network import download_json from moulinette.utils.process import run_commands, check_output -from moulinette.utils.filesystem import read_file, read_json, read_toml, read_yaml, write_to_file, write_to_json, write_to_yaml, chmod, chown, mkdir +from moulinette.utils.filesystem import ( + read_file, + read_json, + read_toml, + read_yaml, + write_to_file, + write_to_json, + write_to_yaml, + chmod, + chown, + mkdir, +) from yunohost.service import service_status, _run_service_command from yunohost.utils import packages from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation, OperationLogger -logger = getActionLogger('yunohost.app') +logger = getActionLogger("yunohost.app") -APPS_PATH = '/usr/share/yunohost/apps' -APPS_SETTING_PATH = '/etc/yunohost/apps/' -INSTALL_TMP = '/var/cache/yunohost' -APP_TMP_FOLDER = INSTALL_TMP + '/from_file' +APPS_PATH = "/usr/share/yunohost/apps" +APPS_SETTING_PATH = "/etc/yunohost/apps/" +INSTALL_TMP = "/var/cache/yunohost" +APP_TMP_FOLDER = INSTALL_TMP + "/from_file" -APPS_CATALOG_CACHE = '/var/cache/yunohost/repo' -APPS_CATALOG_CONF = '/etc/yunohost/apps_catalog.yml' +APPS_CATALOG_CACHE = "/var/cache/yunohost/repo" +APPS_CATALOG_CONF = "/etc/yunohost/apps_catalog.yml" APPS_CATALOG_CRON_PATH = "/etc/cron.daily/yunohost-fetch-apps-catalog" APPS_CATALOG_API_VERSION = 2 APPS_CATALOG_DEFAULT_URL = "https://app.yunohost.org/default" re_app_instance_name = re.compile( - r'^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$' + r"^(?P[\w-]+?)(__(?P[1-9][0-9]*))?$" ) @@ -78,15 +89,19 @@ def app_catalog(full=False, with_categories=False): for app, infos in catalog["apps"].items(): infos["installed"] = app in installed_apps - infos["manifest"]["description"] = _value_for_locale(infos['manifest']['description']) + infos["manifest"]["description"] = _value_for_locale( + infos["manifest"]["description"] + ) if not full: catalog["apps"][app] = { - "description": infos['manifest']['description'], + "description": infos["manifest"]["description"], "level": infos["level"], } else: - infos["manifest"]["arguments"] = _set_default_ask_questions(infos["manifest"].get("arguments", {})) + infos["manifest"]["arguments"] = _set_default_ask_questions( + infos["manifest"].get("arguments", {}) + ) # Trim info for categories if not using --full for category in catalog["categories"]: @@ -96,9 +111,10 @@ def app_catalog(full=False, with_categories=False): subtags["title"] = _value_for_locale(subtags["title"]) if not full: - catalog["categories"] = [{"id": c["id"], - "description": c["description"]} - for c in catalog["categories"]] + catalog["categories"] = [ + {"id": c["id"], "description": c["description"]} + for c in catalog["categories"] + ] if not with_categories: return {"apps": catalog["apps"]} @@ -116,8 +132,10 @@ def app_search(string): # Selecting apps according to a match in app name or description for app in catalog_of_apps["apps"].items(): - if not (re.search(string, app[0], flags=re.IGNORECASE) or - re.search(string, app[1]['description'], flags=re.IGNORECASE)): + if not ( + re.search(string, app[0], flags=re.IGNORECASE) + or re.search(string, app[1]["description"], flags=re.IGNORECASE) + ): del catalog_of_apps["apps"][app[0]] return catalog_of_apps @@ -125,8 +143,11 @@ def app_search(string): # Old legacy function... def app_fetchlist(): - logger.warning("'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead") + logger.warning( + "'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead" + ) from yunohost.tools import tools_update + tools_update(apps=True) @@ -138,11 +159,15 @@ def app_list(full=False, installed=False, filter=None): # Old legacy argument ... app_list was a combination of app_list and # app_catalog before 3.8 ... if installed: - logger.warning("Argument --installed ain't needed anymore when using 'yunohost app list'. It directly returns the list of installed apps..") + logger.warning( + "Argument --installed ain't needed anymore when using 'yunohost app list'. It directly returns the list of installed apps.." + ) # Filter is a deprecated option... if filter: - logger.warning("Using -f $appname in 'yunohost app list' is deprecated. Just use 'yunohost app list | grep -q 'id: $appname' to check a specific app is installed") + logger.warning( + "Using -f $appname in 'yunohost app list' is deprecated. Just use 'yunohost app list | grep -q 'id: $appname' to check a specific app is installed" + ) out = [] for app_id in sorted(_installed_apps()): @@ -158,7 +183,7 @@ def app_list(full=False, installed=False, filter=None): app_info_dict["id"] = app_id out.append(app_info_dict) - return {'apps': out} + return {"apps": out} def app_info(app, full=False): @@ -168,7 +193,9 @@ def app_info(app, full=False): from yunohost.permission import user_permission_list if not _is_installed(app): - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) local_manifest = _get_manifest_of_app(os.path.join(APPS_SETTING_PATH, app)) permissions = user_permission_list(full=True, absolute_urls=True)["permissions"] @@ -176,9 +203,9 @@ def app_info(app, full=False): settings = _get_app_settings(app) ret = { - 'description': _value_for_locale(local_manifest['description']), - 'name': permissions.get(app + ".main", {}).get("label", local_manifest['name']), - 'version': local_manifest.get('version', '-'), + "description": _value_for_locale(local_manifest["description"]), + "name": permissions.get(app + ".main", {}).get("label", local_manifest["name"]), + "version": local_manifest.get("version", "-"), } if "domain" in settings and "path" in settings: @@ -188,21 +215,30 @@ def app_info(app, full=False): return ret ret["manifest"] = local_manifest - ret["manifest"]["arguments"] = _set_default_ask_questions(ret["manifest"].get("arguments", {})) - ret['settings'] = settings + ret["manifest"]["arguments"] = _set_default_ask_questions( + ret["manifest"].get("arguments", {}) + ) + ret["settings"] = settings absolute_app_name, _ = _parse_app_instance_name(app) ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) - ret['upgradable'] = _app_upgradable(ret) - ret['supports_change_url'] = os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")) - ret['supports_backup_restore'] = (os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "backup")) and - os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore"))) - ret['supports_multi_instance'] = is_true(local_manifest.get("multi_instance", False)) + ret["upgradable"] = _app_upgradable(ret) + ret["supports_change_url"] = os.path.exists( + os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url") + ) + ret["supports_backup_restore"] = os.path.exists( + os.path.join(APPS_SETTING_PATH, app, "scripts", "backup") + ) and os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "restore")) + ret["supports_multi_instance"] = is_true( + local_manifest.get("multi_instance", False) + ) - ret['permissions'] = {p: i for p, i in permissions.items() if p.startswith(app + ".")} - ret['label'] = permissions.get(app + ".main", {}).get("label") + ret["permissions"] = { + p: i for p, i in permissions.items() if p.startswith(app + ".") + } + ret["label"] = permissions.get(app + ".main", {}).get("label") - if not ret['label']: + if not ret["label"]: logger.warning("Failed to get label for app %s ?" % app) return ret @@ -214,18 +250,23 @@ def _app_upgradable(app_infos): app_in_catalog = app_infos.get("from_catalog") installed_version = version.parse(app_infos.get("version", "0~ynh0")) - version_in_catalog = version.parse(app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0")) + version_in_catalog = version.parse( + app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0") + ) if not app_in_catalog: return "url_required" # Do not advertise upgrades for bad-quality apps - if not app_in_catalog.get("level", -1) >= 5 or app_in_catalog.get("state") != "working": + if ( + not app_in_catalog.get("level", -1) >= 5 + or app_in_catalog.get("state") != "working" + ): return "bad_quality" # If the app uses the standard version scheme, use it to determine # upgradability - if '~ynh' in str(installed_version) and '~ynh' in str(version_in_catalog): + if "~ynh" in str(installed_version) and "~ynh" in str(version_in_catalog): if installed_version < version_in_catalog: return "yes" else: @@ -234,12 +275,14 @@ def _app_upgradable(app_infos): # Legacy stuff for app with old / non-standard version numbers... # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded - if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"): + if not app_infos["from_catalog"].get("lastUpdate") or not app_infos[ + "from_catalog" + ].get("git"): return "url_required" settings = app_infos["settings"] - local_update_time = settings.get('update_time', settings.get('install_time', 0)) - if app_infos["from_catalog"]['lastUpdate'] > local_update_time: + local_update_time = settings.get("update_time", settings.get("install_time", 0)) + if app_infos["from_catalog"]["lastUpdate"] > local_update_time: return "yes" else: return "no" @@ -279,8 +322,12 @@ def app_map(app=None, raw=False, user=None): if app is not None: if not _is_installed(app): - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) - apps = [app, ] + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) + apps = [ + app, + ] else: apps = os.listdir(APPS_SETTING_PATH) @@ -289,25 +336,34 @@ def app_map(app=None, raw=False, user=None): app_settings = _get_app_settings(app_id) if not app_settings: continue - if 'domain' not in app_settings: + if "domain" not in app_settings: continue - if 'path' not in app_settings: + if "path" not in app_settings: # we assume that an app that doesn't have a path doesn't have an HTTP api continue # This 'no_sso' settings sound redundant to not having $path defined .... # At least from what I can see, all apps using it don't have a path defined ... - if 'no_sso' in app_settings: # I don't think we need to check for the value here + if ( + "no_sso" in app_settings + ): # I don't think we need to check for the value here continue # Users must at least have access to the main permission to have access to extra permissions if user: if not app_id + ".main" in permissions: - logger.warning("Uhoh, no main permission was found for app %s ... sounds like an app was only partially removed due to another bug :/" % app_id) + logger.warning( + "Uhoh, no main permission was found for app %s ... sounds like an app was only partially removed due to another bug :/" + % app_id + ) continue main_perm = permissions[app_id + ".main"] if user not in main_perm["corresponding_users"]: continue - this_app_perms = {p: i for p, i in permissions.items() if p.startswith(app_id + ".") and (i["url"] or i['additional_urls'])} + this_app_perms = { + p: i + for p, i in permissions.items() + if p.startswith(app_id + ".") and (i["url"] or i["additional_urls"]) + } for perm_name, perm_info in this_app_perms.items(): # If we're building the map for a specific user, check the user @@ -315,8 +371,12 @@ def app_map(app=None, raw=False, user=None): if user and user not in perm_info["corresponding_users"]: continue - perm_label = perm_info['label'] - perm_all_urls = [] + ([perm_info["url"]] if perm_info["url"] else []) + perm_info['additional_urls'] + perm_label = perm_info["label"] + perm_all_urls = ( + [] + + ([perm_info["url"]] if perm_info["url"] else []) + + perm_info["additional_urls"] + ) for url in perm_all_urls: @@ -336,16 +396,13 @@ def app_map(app=None, raw=False, user=None): else: if "/" in url: perm_domain, perm_path = url.split("/", 1) - perm_path = '/' + perm_path + perm_path = "/" + perm_path else: perm_domain = url perm_path = "/" if perm_domain not in result: result[perm_domain] = {} - result[perm_domain][perm_path] = { - 'label': perm_label, - 'id': app_id - } + result[perm_domain][perm_path] = {"label": perm_label, "id": app_id} return result @@ -365,9 +422,13 @@ def app_change_url(operation_logger, app, domain, path): installed = _is_installed(app) if not installed: - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) - if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): + if not os.path.exists( + os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url") + ): raise YunohostError("app_change_url_no_script", app_name=app) old_domain = app_setting(app, "domain") @@ -378,7 +439,9 @@ def app_change_url(operation_logger, app, domain, path): domain, path = _normalize_domain_path(domain, path) if (domain, path) == (old_domain, old_path): - raise YunohostError("app_change_url_identical_domains", domain=domain, path=path) + raise YunohostError( + "app_change_url_identical_domains", domain=domain, path=path + ) # Check the url is available _assert_no_conflicting_apps(domain, path, ignore_app=app) @@ -387,7 +450,7 @@ def app_change_url(operation_logger, app, domain, path): # Retrieve arguments list for change_url script # TODO: Allow to specify arguments - args_odict = _parse_args_from_manifest(manifest, 'change_url') + args_odict = _parse_args_from_manifest(manifest, "change_url") # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app, args=args_odict) @@ -397,29 +460,38 @@ def app_change_url(operation_logger, app, domain, path): env_dict["YNH_APP_NEW_PATH"] = path if domain != old_domain: - operation_logger.related_to.append(('domain', old_domain)) - operation_logger.extra.update({'env': env_dict}) + 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")) - shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"), - os.path.join(APP_TMP_FOLDER, "scripts")) + 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")) + 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"))) - os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts", "change_url"))) + 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")) + ) - if hook_exec(os.path.join(APP_TMP_FOLDER, 'scripts/change_url'), - env=env_dict)[0] != 0: + if ( + hook_exec(os.path.join(APP_TMP_FOLDER, "scripts/change_url"), env=env_dict)[0] + != 0 + ): msg = "Failed to change '%s' url." % app logger.error(msg) operation_logger.error(msg) @@ -431,8 +503,8 @@ def app_change_url(operation_logger, app, domain, 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_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) app_ssowatconf() @@ -442,12 +514,13 @@ def app_change_url(operation_logger, app, domain, path): # 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 = check_output("nginx -t; exit 0") - raise YunohostError("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors) + raise YunohostError( + "app_change_url_failed_nginx_reload", nginx_errors=nginx_errors + ) - logger.success(m18n.n("app_change_url_success", - app=app, domain=domain, path=path)) + logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) - hook_callback('post_app_change_url', env=env_dict) + hook_callback("post_app_change_url", env=env_dict) def app_upgrade(app=[], url=None, file=None, force=False): @@ -479,32 +552,36 @@ def app_upgrade(app=[], url=None, file=None, force=False): # Abort if any of those app is in fact not installed.. for app in [app_ for app_ in apps if not _is_installed(app_)]: - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) if len(apps) == 0: - raise YunohostError('apps_already_up_to_date') + raise YunohostError("apps_already_up_to_date") if len(apps) > 1: logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps))) for number, app_instance_name in enumerate(apps): - logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name)) + logger.info(m18n.n("app_upgrade_app_name", app=app_instance_name)) app_dict = app_info(app_instance_name, full=True) if file and isinstance(file, dict): # We use this dirty hack to test chained upgrades in unit/functional tests - manifest, extracted_app_folder = _extract_app_from_file(file[app_instance_name]) + manifest, extracted_app_folder = _extract_app_from_file( + file[app_instance_name] + ) elif file: manifest, extracted_app_folder = _extract_app_from_file(file) elif url: manifest, extracted_app_folder = _fetch_app_from_git(url) elif app_dict["upgradable"] == "url_required": - logger.warning(m18n.n('custom_app_url_required', app=app_instance_name)) + logger.warning(m18n.n("custom_app_url_required", app=app_instance_name)) continue elif app_dict["upgradable"] == "yes" or force: manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) else: - logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) + logger.success(m18n.n("app_already_up_to_date", app=app_instance_name)) continue # Manage upgrade type and avoid any upgrade if there is nothing to do @@ -516,19 +593,27 @@ def app_upgrade(app=[], url=None, file=None, force=False): if app_current_version >= app_new_version and not force: # In case of upgrade from file or custom repository # No new version available - logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) + logger.success(m18n.n("app_already_up_to_date", app=app_instance_name)) # Save update time now = int(time.time()) - app_setting(app_instance_name, 'update_time', now) - app_setting(app_instance_name, 'current_revision', manifest.get('remote', {}).get('revision', "?")) + app_setting(app_instance_name, "update_time", now) + app_setting( + app_instance_name, + "current_revision", + manifest.get("remote", {}).get("revision", "?"), + ) continue elif app_current_version > app_new_version: upgrade_type = "DOWNGRADE_FORCED" elif app_current_version == app_new_version: upgrade_type = "UPGRADE_FORCED" else: - app_current_version_upstream, app_current_version_pkg = str(app_current_version).split("~ynh") - app_new_version_upstream, app_new_version_pkg = str(app_new_version).split("~ynh") + app_current_version_upstream, app_current_version_pkg = str( + app_current_version + ).split("~ynh") + app_new_version_upstream, app_new_version_pkg = str( + app_new_version + ).split("~ynh") if app_current_version_upstream == app_new_version_upstream: upgrade_type = "UPGRADE_PACKAGE" elif app_current_version_pkg == app_new_version_pkg: @@ -540,11 +625,11 @@ def app_upgrade(app=[], url=None, file=None, force=False): _check_manifest_requirements(manifest, app_instance_name=app_instance_name) _assert_system_is_sane_for_app(manifest, "pre") - app_setting_path = APPS_SETTING_PATH + '/' + app_instance_name + app_setting_path = APPS_SETTING_PATH + "/" + app_instance_name # Retrieve arguments list for upgrade script # TODO: Allow to specify arguments - args_odict = _parse_args_from_manifest(manifest, 'upgrade') + args_odict = _parse_args_from_manifest(manifest, "upgrade") # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) @@ -562,37 +647,45 @@ def app_upgrade(app=[], url=None, file=None, force=False): _patch_legacy_php_versions(extracted_app_folder) # Start register change on system - related_to = [('app', app_instance_name)] - operation_logger = OperationLogger('app_upgrade', related_to, env=env_dict) + related_to = [("app", app_instance_name)] + operation_logger = OperationLogger("app_upgrade", related_to, env=env_dict) operation_logger.start() # Execute App upgrade script - os.system('chown -hR admin: %s' % INSTALL_TMP) + os.system("chown -hR admin: %s" % INSTALL_TMP) # Execute the app upgrade script upgrade_failed = True try: - upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade', - env=env_dict)[0] + upgrade_retcode = hook_exec( + extracted_app_folder + "/scripts/upgrade", env=env_dict + )[0] upgrade_failed = True if upgrade_retcode != 0 else False if upgrade_failed: - error = m18n.n('app_upgrade_script_failed') - logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=error)) + error = m18n.n("app_upgrade_script_failed") + logger.error( + m18n.n("app_upgrade_failed", app=app_instance_name, error=error) + ) failure_message_with_debug_instructions = operation_logger.error(error) - if msettings.get('interface') != 'api': + if msettings.get("interface") != "api": dump_app_log_extract_for_debugging(operation_logger) # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception except (KeyboardInterrupt, EOFError): upgrade_retcode = -1 - error = m18n.n('operation_interrupted') - logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=error)) + error = m18n.n("operation_interrupted") + logger.error( + m18n.n("app_upgrade_failed", app=app_instance_name, error=error) + ) failure_message_with_debug_instructions = operation_logger.error(error) # Something wrong happened in Yunohost's code (most probably hook_exec) except Exception: import traceback - error = m18n.n('unexpected_error', error="\n" + traceback.format_exc()) - logger.error(m18n.n("app_install_failed", app=app_instance_name, error=error)) + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + logger.error( + m18n.n("app_install_failed", app=app_instance_name, error=error) + ) failure_message_with_debug_instructions = operation_logger.error(error) finally: # Whatever happened (install success or failure) we check if it broke the system @@ -602,64 +695,111 @@ def app_upgrade(app=[], url=None, file=None, force=False): _assert_system_is_sane_for_app(manifest, "post") except Exception as e: broke_the_system = True - logger.error(m18n.n("app_upgrade_failed", app=app_instance_name, error=str(e))) + logger.error( + m18n.n("app_upgrade_failed", app=app_instance_name, error=str(e)) + ) failure_message_with_debug_instructions = operation_logger.error(str(e)) # We'll check that the app didn't brutally edit some system configuration manually_modified_files_after_install = manually_modified_files() - manually_modified_files_by_app = set(manually_modified_files_after_install) - set(manually_modified_files_before_install) + manually_modified_files_by_app = set( + manually_modified_files_after_install + ) - set(manually_modified_files_before_install) if manually_modified_files_by_app: - logger.error("Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + '\n -'.join(manually_modified_files_by_app)) + logger.error( + "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + + "\n -".join(manually_modified_files_by_app) + ) # If upgrade failed or broke the system, # raise an error and interrupt all other pending upgrades if upgrade_failed or broke_the_system: # display this if there are remaining apps - if apps[number + 1:]: + if apps[number + 1 :]: not_upgraded_apps = apps[number:] - logger.error(m18n.n('app_not_upgraded', - failed_app=app_instance_name, - apps=', '.join(not_upgraded_apps))) + logger.error( + m18n.n( + "app_not_upgraded", + failed_app=app_instance_name, + apps=", ".join(not_upgraded_apps), + ) + ) - raise YunohostError(failure_message_with_debug_instructions, raw_msg=True) + raise YunohostError( + failure_message_with_debug_instructions, raw_msg=True + ) # Otherwise we're good and keep going ! now = int(time.time()) - app_setting(app_instance_name, 'update_time', now) - app_setting(app_instance_name, 'current_revision', manifest.get('remote', {}).get('revision', "?")) + app_setting(app_instance_name, "update_time", now) + app_setting( + app_instance_name, + "current_revision", + manifest.get("remote", {}).get("revision", "?"), + ) # 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) + 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) # Replace scripts and manifest and conf (if exists) - os.system('rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"' % (app_setting_path, app_setting_path, app_setting_path, app_setting_path)) + os.system( + 'rm -rf "%s/scripts" "%s/manifest.toml %s/manifest.json %s/conf"' + % ( + app_setting_path, + app_setting_path, + app_setting_path, + app_setting_path, + ) + ) if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")): - os.system('mv "%s/manifest.json" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, 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, "manifest.toml")): - os.system('mv "%s/manifest.toml" "%s/scripts" %s' % (extracted_app_folder, extracted_app_folder, app_setting_path)) + os.system( + 'mv "%s/manifest.toml" "%s/scripts" %s' + % (extracted_app_folder, extracted_app_folder, app_setting_path) + ) - for file_to_copy in ["actions.json", "actions.toml", "config_panel.json", "config_panel.toml", "conf"]: + for file_to_copy in [ + "actions.json", + "actions.toml", + "config_panel.json", + "config_panel.toml", + "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)) + os.system( + "cp -R %s/%s %s" + % (extracted_app_folder, file_to_copy, app_setting_path) + ) # So much win - logger.success(m18n.n('app_upgraded', app=app_instance_name)) + logger.success(m18n.n("app_upgraded", app=app_instance_name)) - hook_callback('post_app_upgrade', env=env_dict) + hook_callback("post_app_upgrade", env=env_dict) operation_logger.success() permission_sync_to_user() - logger.success(m18n.n('upgrade_complete')) + logger.success(m18n.n("upgrade_complete")) @is_unit_operation() -def app_install(operation_logger, app, label=None, args=None, no_remove_on_failure=False, force=False): +def app_install( + operation_logger, + app, + label=None, + args=None, + no_remove_on_failure=False, + force=False, +): """ Install apps @@ -673,7 +813,12 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger - from yunohost.permission import user_permission_list, permission_create, permission_delete, permission_sync_to_user + from yunohost.permission import ( + user_permission_list, + permission_create, + permission_delete, + permission_sync_to_user, + ) from yunohost.regenconf import manually_modified_files # Fetch or extract sources @@ -683,33 +828,34 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu def confirm_install(confirm): # Ignore if there's nothing for confirm (good quality app), if --force is used # or if request on the API (confirm already implemented on the API side) - if confirm is None or force or msettings.get('interface') == 'api': + if confirm is None or force or msettings.get("interface") == "api": return if confirm in ["danger", "thirdparty"]: - answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm, - answers='Yes, I understand'), - color="red") + answer = msignals.prompt( + m18n.n("confirm_app_install_" + confirm, answers="Yes, I understand"), + color="red", + ) if answer != "Yes, I understand": raise YunohostError("aborting") else: - answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm, - answers='Y/N'), - color="yellow") + answer = msignals.prompt( + m18n.n("confirm_app_install_" + confirm, answers="Y/N"), color="yellow" + ) if answer.upper() != "Y": raise YunohostError("aborting") raw_app_list = _load_apps_catalog()["apps"] - if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app): + if app in raw_app_list or ("@" in app) or ("http://" in app) or ("https://" in app): # If we got an app name directly (e.g. just "wordpress"), we gonna test this name if app in raw_app_list: app_name_to_test = app # If we got an url like "https://github.com/foo/bar_ynh, we want to # extract "bar" and test if we know this app - elif ('http://' in app) or ('https://' in app): + elif ("http://" in app) or ("https://" in app): app_name_to_test = app.strip("/").split("/")[-1].replace("_ynh", "") else: # FIXME : watdo if '@' in app ? @@ -735,14 +881,14 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu confirm_install("thirdparty") manifest, extracted_app_folder = _extract_app_from_file(app) else: - raise YunohostError('app_unknown') + raise YunohostError("app_unknown") # Check ID - if 'id' not in manifest or '__' in manifest['id']: - raise YunohostError('app_id_invalid') + if "id" not in manifest or "__" in manifest["id"]: + raise YunohostError("app_id_invalid") - app_id = manifest['id'] - label = label if label else manifest['name'] + app_id = manifest["id"] + label = label if label else manifest["name"] # Check requirements _check_manifest_requirements(manifest, app_id) @@ -751,18 +897,19 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 if instance_number > 1: - if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']): - raise YunohostError('app_already_installed', app=app_id) + if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]): + raise YunohostError("app_already_installed", app=app_id) # Change app_id to the forked app id - app_instance_name = app_id + '__' + str(instance_number) + app_instance_name = app_id + "__" + str(instance_number) else: app_instance_name = app_id # Retrieve arguments list for install script - args_dict = {} if not args else \ - dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) - args_odict = _parse_args_from_manifest(manifest, 'install', args=args_dict) + args_dict = ( + {} if not args else dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) + ) + args_odict = _parse_args_from_manifest(manifest, "install", args=args_dict) # Validate domain / path availability for webapps _validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder) @@ -779,11 +926,19 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Tell the operation_logger to redact all password-type args # Also redact the % escaped version of the password that might appear in # the 'args' section of metadata (relevant for password with non-alphanumeric char) - data_to_redact = [value[0] for value in args_odict.values() if value[1] == "password"] - data_to_redact += [urllib.parse.quote(data) for data in data_to_redact if urllib.parse.quote(data) != data] + data_to_redact = [ + value[0] for value in args_odict.values() if value[1] == "password" + ] + data_to_redact += [ + urllib.parse.quote(data) + for data in data_to_redact + if urllib.parse.quote(data) != data + ] operation_logger.data_to_redact.extend(data_to_redact) - operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"] + 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() @@ -797,32 +952,47 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # Set initial app settings app_settings = { - 'id': app_instance_name, - 'install_time': int(time.time()), - 'current_revision': manifest.get('remote', {}).get('revision', "?") + "id": app_instance_name, + "install_time": int(time.time()), + "current_revision": manifest.get("remote", {}).get("revision", "?"), } _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) + os.system("chown -hR admin: %s" % INSTALL_TMP) # Move scripts and manifest to the right place if os.path.exists(os.path.join(extracted_app_folder, "manifest.json")): - os.system('cp %s/manifest.json %s' % (extracted_app_folder, app_setting_path)) + os.system("cp %s/manifest.json %s" % (extracted_app_folder, app_setting_path)) if os.path.exists(os.path.join(extracted_app_folder, "manifest.toml")): - os.system('cp %s/manifest.toml %s' % (extracted_app_folder, app_setting_path)) - os.system('cp -R %s/scripts %s' % (extracted_app_folder, app_setting_path)) + os.system("cp %s/manifest.toml %s" % (extracted_app_folder, app_setting_path)) + os.system("cp -R %s/scripts %s" % (extracted_app_folder, app_setting_path)) - for file_to_copy in ["actions.json", "actions.toml", "config_panel.json", "config_panel.toml", "conf"]: + for file_to_copy in [ + "actions.json", + "actions.toml", + "config_panel.json", + "config_panel.toml", + "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)) + os.system( + "cp -R %s/%s %s" + % (extracted_app_folder, file_to_copy, app_setting_path) + ) # Initialize the main permission for the app # The permission is initialized with no url associated, and with tile disabled # For web app, the root path of the app will be added as url and the tile # will be enabled during the app install. C.f. 'app_register_url()' below. - permission_create(app_instance_name + ".main", allowed=["all_users"], label=label, show_tile=False, protected=False) + permission_create( + app_instance_name + ".main", + allowed=["all_users"], + label=label, + show_tile=False, + protected=False, + ) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app_instance_name, args=args_odict) @@ -832,32 +1002,32 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu if arg_value_and_type[1] == "password": del env_dict_for_logging["YNH_APP_ARG_%s" % arg_name.upper()] - operation_logger.extra.update({'env': env_dict_for_logging}) + operation_logger.extra.update({"env": env_dict_for_logging}) # Execute the app install script install_failed = True try: install_retcode = hook_exec( - os.path.join(extracted_app_folder, 'scripts/install'), - env=env_dict + os.path.join(extracted_app_folder, "scripts/install"), env=env_dict )[0] # "Common" app install failure : the script failed and returned exit code != 0 install_failed = True if install_retcode != 0 else False if install_failed: - error = m18n.n('app_install_script_failed') + error = m18n.n("app_install_script_failed") logger.error(m18n.n("app_install_failed", app=app_id, error=error)) failure_message_with_debug_instructions = operation_logger.error(error) - if msettings.get('interface') != 'api': + if msettings.get("interface") != "api": dump_app_log_extract_for_debugging(operation_logger) # Script got manually interrupted ... N.B. : KeyboardInterrupt does not inherit from Exception except (KeyboardInterrupt, EOFError): - error = m18n.n('operation_interrupted') + error = m18n.n("operation_interrupted") logger.error(m18n.n("app_install_failed", app=app_id, error=error)) failure_message_with_debug_instructions = operation_logger.error(error) # Something wrong happened in Yunohost's code (most probably hook_exec) except Exception: import traceback - error = m18n.n('unexpected_error', error="\n" + traceback.format_exc()) + + error = m18n.n("unexpected_error", error="\n" + traceback.format_exc()) logger.error(m18n.n("app_install_failed", app=app_id, error=error)) failure_message_with_debug_instructions = operation_logger.error(error) finally: @@ -873,16 +1043,25 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu # We'll check that the app didn't brutally edit some system configuration manually_modified_files_after_install = manually_modified_files() - manually_modified_files_by_app = set(manually_modified_files_after_install) - set(manually_modified_files_before_install) + manually_modified_files_by_app = set( + manually_modified_files_after_install + ) - set(manually_modified_files_before_install) if manually_modified_files_by_app: - logger.error("Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + '\n -'.join(manually_modified_files_by_app)) + logger.error( + "Packagers /!\\ This app manually modified some system configuration files! This should not happen! If you need to do so, you should implement a proper conf_regen hook. Those configuration were affected:\n - " + + "\n -".join(manually_modified_files_by_app) + ) # If the install failed or broke the system, we remove it if install_failed or broke_the_system: # This option is meant for packagers to debug their apps more easily if no_remove_on_failure: - raise YunohostError("The installation of %s failed, but was not cleaned up as requested by --no-remove-on-failure." % app_id, raw_msg=True) + raise YunohostError( + "The installation of %s failed, but was not cleaned up as requested by --no-remove-on-failure." + % app_id, + raw_msg=True, + ) else: logger.warning(m18n.n("app_remove_after_failed_install")) @@ -894,16 +1073,19 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") # Execute remove script - operation_logger_remove = OperationLogger('remove_on_failed_install', - [('app', app_instance_name)], - env=env_dict_remove) + operation_logger_remove = OperationLogger( + "remove_on_failed_install", + [("app", app_instance_name)], + env=env_dict_remove, + ) operation_logger_remove.start() # Try to remove the app try: remove_retcode = hook_exec( - os.path.join(extracted_app_folder, 'scripts/remove'), - args=[app_instance_name], env=env_dict_remove + os.path.join(extracted_app_folder, "scripts/remove"), + args=[app_instance_name], + env=env_dict_remove, )[0] # Here again, calling hook_exec could fail miserably, or get @@ -913,7 +1095,10 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu except (KeyboardInterrupt, EOFError, Exception): remove_retcode = -1 import traceback - logger.error(m18n.n('unexpected_error', error="\n" + traceback.format_exc())) + + logger.error( + m18n.n("unexpected_error", error="\n" + traceback.format_exc()) + ) # Remove all permission in LDAP for permission_name in user_permission_list()["permissions"].keys(): @@ -921,8 +1106,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu permission_delete(permission_name, force=True, sync_perm=False) if remove_retcode != 0: - msg = m18n.n('app_not_properly_removed', - app=app_instance_name) + msg = m18n.n("app_not_properly_removed", app=app_instance_name) logger.warning(msg) operation_logger_remove.error(msg) else: @@ -939,23 +1123,27 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu permission_sync_to_user() - raise YunohostError(failure_message_with_debug_instructions, raw_msg=True, log_ref=operation_logger.name) + raise YunohostError( + failure_message_with_debug_instructions, + raw_msg=True, + log_ref=operation_logger.name, + ) # 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) + 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) # Clean and set permissions shutil.rmtree(extracted_app_folder) - os.system('chmod -R 400 %s' % app_setting_path) - os.system('chown -R root: %s' % app_setting_path) - os.system('chown -R admin: %s/scripts' % app_setting_path) + os.system("chmod -R 400 %s" % app_setting_path) + os.system("chown -R root: %s" % app_setting_path) + os.system("chown -R admin: %s/scripts" % app_setting_path) - logger.success(m18n.n('installation_complete')) + logger.success(m18n.n("installation_complete")) - hook_callback('post_app_install', env=env_dict) + hook_callback("post_app_install", env=env_dict) def dump_app_log_extract_for_debugging(operation_logger): @@ -972,7 +1160,7 @@ def dump_app_log_extract_for_debugging(operation_logger): r"args_array=.*$", r"local -A args_array$", r"ynh_handle_getopts_args", - r"ynh_script_progression" + r"ynh_script_progression", ] filters = [re.compile(f_) for f_ in filters] @@ -998,7 +1186,9 @@ def dump_app_log_extract_for_debugging(operation_logger): elif len(lines_to_display) > 20: lines_to_display.pop(0) - logger.warning("Here's an extract of the logs before the crash. It might help debugging the error:") + logger.warning( + "Here's an extract of the logs before the crash. It might help debugging the error:" + ) for line in lines_to_display: logger.info(line) @@ -1013,9 +1203,16 @@ def app_remove(operation_logger, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback - from yunohost.permission import user_permission_list, permission_delete, permission_sync_to_user + from yunohost.permission import ( + user_permission_list, + permission_delete, + permission_sync_to_user, + ) + if not _is_installed(app): - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) operation_logger.start() @@ -1025,7 +1222,7 @@ def app_remove(operation_logger, app): # TODO: display fail messages from script try: - shutil.rmtree('/tmp/yunohost_remove') + shutil.rmtree("/tmp/yunohost_remove") except Exception: pass @@ -1038,9 +1235,12 @@ def app_remove(operation_logger, app): manifest = _get_manifest_of_app(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') + 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") env_dict = {} app_id, app_instance_nb = _parse_app_instance_name(app) @@ -1048,12 +1248,11 @@ def app_remove(operation_logger, app): env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") - operation_logger.extra.update({'env': env_dict}) + operation_logger.extra.update({"env": env_dict}) operation_logger.flush() try: - ret = hook_exec('/tmp/yunohost_remove/scripts/remove', - env=env_dict)[0] + ret = hook_exec("/tmp/yunohost_remove/scripts/remove", env=env_dict)[0] # Here again, calling hook_exec could fail miserably, or get # manually interrupted (by mistake or because script was stuck) # In that case we still want to proceed with the rest of the @@ -1061,17 +1260,18 @@ def app_remove(operation_logger, app): except (KeyboardInterrupt, EOFError, Exception): ret = -1 import traceback - logger.error(m18n.n('unexpected_error', error="\n" + traceback.format_exc())) + + logger.error(m18n.n("unexpected_error", error="\n" + traceback.format_exc())) if ret == 0: - logger.success(m18n.n('app_removed', app=app)) - hook_callback('post_app_remove', env=env_dict) + logger.success(m18n.n("app_removed", app=app)) + hook_callback("post_app_remove", env=env_dict) else: - logger.warning(m18n.n('app_not_properly_removed', app=app)) + logger.warning(m18n.n("app_not_properly_removed", app=app)) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) - shutil.rmtree('/tmp/yunohost_remove') + shutil.rmtree("/tmp/yunohost_remove") hook_remove(app) # Remove all permission in LDAP @@ -1094,14 +1294,18 @@ def app_addaccess(apps, users=[]): """ from yunohost.permission import user_permission_update - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") + logger.warning( + "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions." + ) output = {} for app in apps: - permission = user_permission_update(app + ".main", add=users, remove="all_users") + permission = user_permission_update( + app + ".main", add=users, remove="all_users" + ) output[app] = permission["corresponding_users"] - return {'allowed_users': output} + return {"allowed_users": output} def app_removeaccess(apps, users=[]): @@ -1115,14 +1319,16 @@ def app_removeaccess(apps, users=[]): """ from yunohost.permission import user_permission_update - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") + logger.warning( + "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions." + ) output = {} for app in apps: permission = user_permission_update(app + ".main", remove=users) output[app] = permission["corresponding_users"] - return {'allowed_users': output} + return {"allowed_users": output} def app_clearaccess(apps): @@ -1135,14 +1341,16 @@ def app_clearaccess(apps): """ from yunohost.permission import user_permission_reset - logger.warning("/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions.") + logger.warning( + "/!\\ Packagers ! This app is using the legacy permission system. Please use the new helpers ynh_permission_{create,url,update,delete} and the 'visitors' group to manage permissions." + ) output = {} for app in apps: permission = user_permission_reset(app + ".main") output[app] = permission["corresponding_users"] - return {'allowed_users': output} + return {"allowed_users": output} @is_unit_operation() @@ -1158,18 +1366,22 @@ def app_makedefault(operation_logger, app, domain=None): from yunohost.domain import domain_list app_settings = _get_app_settings(app) - app_domain = app_settings['domain'] - app_path = app_settings['path'] + app_domain = app_settings["domain"] + app_path = app_settings["path"] if domain is None: domain = app_domain - operation_logger.related_to.append(('domain', domain)) - elif domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + operation_logger.related_to.append(("domain", domain)) + elif domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) - if '/' in app_map(raw=True)[domain]: - raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain, - other_app=app_map(raw=True)[domain]["/"]["id"]) + if "/" in app_map(raw=True)[domain]: + raise YunohostError( + "app_make_default_location_already_used", + app=app, + domain=app_domain, + other_app=app_map(raw=True)[domain]["/"]["id"], + ) operation_logger.start() @@ -1177,20 +1389,22 @@ def app_makedefault(operation_logger, app, domain=None): # This is really not robust and should be improved # e.g. have a flag in /etc/yunohost/apps/$app/ to say that this is the # default app or idk... - if not os.path.exists('/etc/ssowat/conf.json.persistent'): + if not os.path.exists("/etc/ssowat/conf.json.persistent"): ssowat_conf = {} else: - ssowat_conf = read_json('/etc/ssowat/conf.json.persistent') + ssowat_conf = read_json("/etc/ssowat/conf.json.persistent") - if 'redirected_urls' not in ssowat_conf: - ssowat_conf['redirected_urls'] = {} + 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 - write_to_json('/etc/ssowat/conf.json.persistent', ssowat_conf, sort_keys=True, indent=4) - os.system('chmod 644 /etc/ssowat/conf.json.persistent') + write_to_json( + "/etc/ssowat/conf.json.persistent", ssowat_conf, sort_keys=True, indent=4 + ) + os.system("chmod 644 /etc/ssowat/conf.json.persistent") - logger.success(m18n.n('ssowat_conf_updated')) + logger.success(m18n.n("ssowat_conf_updated")) def app_setting(app, key, value=None, delete=False): @@ -1211,18 +1425,31 @@ def app_setting(app, key, value=None, delete=False): # (unprotected, protected, skipped_uri/regex) # - is_legacy_permission_setting = any(key.startswith(word + "_") for word in ["unprotected", "protected", "skipped"]) + is_legacy_permission_setting = any( + key.startswith(word + "_") for word in ["unprotected", "protected", "skipped"] + ) if is_legacy_permission_setting: - from yunohost.permission import user_permission_list, user_permission_update, permission_create, permission_delete, permission_url - permissions = user_permission_list(full=True)['permissions'] - permission_name = "%s.legacy_%s_uris" % (app, key.split('_')[0]) + from yunohost.permission import ( + user_permission_list, + user_permission_update, + permission_create, + permission_delete, + permission_url, + ) + + permissions = user_permission_list(full=True)["permissions"] + permission_name = "%s.legacy_%s_uris" % (app, key.split("_")[0]) permission = permissions.get(permission_name) # GET if value is None and not delete: - return ','.join(permission.get('uris', []) + permission['additional_urls']) if permission else None + return ( + ",".join(permission.get("uris", []) + permission["additional_urls"]) + if permission + else None + ) # DELETE if delete: @@ -1233,8 +1460,11 @@ def app_setting(app, key, value=None, delete=False): # In that case, we interpret the request for "deleting # unprotected/skipped" setting as willing to make the app # private - if 'is_public' in app_settings and 'visitors' in permissions[app + ".main"]['allowed']: - if key.startswith('unprotected_') or key.startswith('skipped_'): + if ( + "is_public" in app_settings + and "visitors" in permissions[app + ".main"]["allowed"] + ): + if key.startswith("unprotected_") or key.startswith("skipped_"): user_permission_update(app + ".main", remove="visitors") if permission: @@ -1242,33 +1472,43 @@ def app_setting(app, key, value=None, delete=False): # SET else: - logger.warning("/!\\ Packagers! This app is still using the skipped/protected/unprotected_uris/regex settings which are now obsolete and deprecated... Instead, you should use the new helpers 'ynh_permission_{create,urls,update,delete}' and the 'visitors' group to initialize the public/private access. Check out the documentation at the bottom of yunohost.org/groups_and_permissions to learn how to use the new permission mechanism.") + logger.warning( + "/!\\ Packagers! This app is still using the skipped/protected/unprotected_uris/regex settings which are now obsolete and deprecated... Instead, you should use the new helpers 'ynh_permission_{create,urls,update,delete}' and the 'visitors' group to initialize the public/private access. Check out the documentation at the bottom of yunohost.org/groups_and_permissions to learn how to use the new permission mechanism." + ) urls = value # If the request is about the root of the app (/), ( = the vast majority of cases) # we interpret this as a change for the main permission # (i.e. allowing/disallowing visitors) - if urls == '/': + if urls == "/": if key.startswith("unprotected_") or key.startswith("skipped_"): - permission_url(app + ".main", url='/', sync_perm=False) + permission_url(app + ".main", url="/", sync_perm=False) user_permission_update(app + ".main", add="visitors") else: user_permission_update(app + ".main", remove="visitors") else: urls = urls.split(",") - if key.endswith('_regex'): - urls = ['re:' + url for url in urls] + if key.endswith("_regex"): + urls = ["re:" + url for url in urls] if permission: # In case of new regex, save the urls, to add a new time in the additional_urls # In case of new urls, we do the same thing but inversed - if key.endswith('_regex'): + if key.endswith("_regex"): # List of urls to save - current_urls_or_regex = [url for url in permission['additional_urls'] if not url.startswith('re:')] + current_urls_or_regex = [ + url + for url in permission["additional_urls"] + if not url.startswith("re:") + ] else: # List of regex to save - current_urls_or_regex = [url for url in permission['additional_urls'] if url.startswith('re:')] + current_urls_or_regex = [ + url + for url in permission["additional_urls"] + if url.startswith("re:") + ] new_urls = urls + current_urls_or_regex # We need to clear urls because in the old setting the new setting override the old one and dont just add some urls @@ -1276,16 +1516,21 @@ def app_setting(app, key, value=None, delete=False): permission_url(permission_name, add_url=new_urls) else: from yunohost.utils.legacy import legacy_permission_label + # Let's create a "special" permission for the legacy settings - permission_create(permission=permission_name, - # FIXME find a way to limit to only the user allowed to the main permission - allowed=['all_users'] if key.startswith('protected_') else ['all_users', 'visitors'], - url=None, - additional_urls=urls, - auth_header=not key.startswith('skipped_'), - label=legacy_permission_label(app, key.split('_')[0]), - show_tile=False, - protected=True) + permission_create( + permission=permission_name, + # FIXME find a way to limit to only the user allowed to the main permission + allowed=["all_users"] + if key.startswith("protected_") + else ["all_users", "visitors"], + url=None, + additional_urls=urls, + auth_header=not key.startswith("skipped_"), + label=legacy_permission_label(app, key.split("_")[0]), + show_tile=False, + protected=True, + ) return @@ -1304,7 +1549,7 @@ def app_setting(app, key, value=None, delete=False): # SET else: - if key in ['redirected_urls', 'redirected_regex']: + if key in ["redirected_urls", "redirected_regex"]: value = yaml.load(value) app_settings[key] = value @@ -1320,7 +1565,11 @@ def app_register_url(app, domain, 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) """ - from yunohost.permission import permission_url, user_permission_update, permission_sync_to_user + from yunohost.permission import ( + permission_url, + user_permission_update, + permission_sync_to_user, + ) domain, path = _normalize_domain_path(domain, path) @@ -1330,13 +1579,13 @@ def app_register_url(app, domain, path): if _is_installed(app): settings = _get_app_settings(app) if "path" in settings.keys() and "domain" in settings.keys(): - raise YunohostError('app_already_installed_cant_change_url') + raise YunohostError("app_already_installed_cant_change_url") # Check the url is available _assert_no_conflicting_apps(domain, path) - app_setting(app, 'domain', value=domain) - app_setting(app, 'path', value=path) + app_setting(app, "domain", value=domain) + app_setting(app, "path", value=path) # Initially, the .main permission is created with no url at all associated # When the app register/books its web url, we also add the url '/' @@ -1344,7 +1593,7 @@ def app_register_url(app, domain, path): # and enable the tile to the SSO, and both of this should match 95% of apps # For more specific cases, the app is free to change / add urls or disable # the tile using the permission helpers. - permission_url(app + ".main", url='/', sync_perm=False) + permission_url(app + ".main", url="/", sync_perm=False) user_permission_update(app + ".main", show_tile=True, sync_perm=False) permission_sync_to_user() @@ -1359,91 +1608,106 @@ def app_ssowatconf(): from yunohost.permission import user_permission_list main_domain = _get_maindomain() - domains = domain_list()['domains'] - all_permissions = user_permission_list(full=True, ignore_system_perms=True, absolute_urls=True)['permissions'] + domains = domain_list()["domains"] + all_permissions = user_permission_list( + full=True, ignore_system_perms=True, absolute_urls=True + )["permissions"] permissions = { - 'core_skipped': { + "core_skipped": { "users": [], "label": "Core permissions - skipped", "show_tile": False, "auth_header": False, "public": True, - "uris": - [domain + '/yunohost/admin' for domain in domains] + - [domain + '/yunohost/api' for domain in domains] + [ + "uris": [domain + "/yunohost/admin" for domain in domains] + + [domain + "/yunohost/api" for domain in domains] + + [ "re:^[^/]*/%.well%-known/ynh%-diagnosis/.*$", "re:^[^/]*/%.well%-known/acme%-challenge/.*$", - "re:^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$" - ] + "re:^[^/]*/%.well%-known/autoconfig/mail/config%-v1%.1%.xml.*$", + ], } } - redirected_regex = {main_domain + r'/yunohost[\/]?$': 'https://' + main_domain + '/yunohost/sso/'} + redirected_regex = { + main_domain + r"/yunohost[\/]?$": "https://" + main_domain + "/yunohost/sso/" + } redirected_urls = {} for app in _installed_apps(): - app_settings = read_yaml(APPS_SETTING_PATH + app + '/settings.yml') + app_settings = read_yaml(APPS_SETTING_PATH + app + "/settings.yml") # Redirected - redirected_urls.update(app_settings.get('redirected_urls', {})) - redirected_regex.update(app_settings.get('redirected_regex', {})) + redirected_urls.update(app_settings.get("redirected_urls", {})) + redirected_regex.update(app_settings.get("redirected_regex", {})) # New permission system for perm_name, perm_info in all_permissions.items(): - uris = [] + ([perm_info['url']] if perm_info['url'] else []) + perm_info['additional_urls'] + uris = ( + [] + + ([perm_info["url"]] if perm_info["url"] else []) + + perm_info["additional_urls"] + ) # Ignore permissions for which there's no url defined if not uris: continue permissions[perm_name] = { - "users": perm_info['corresponding_users'], - "label": perm_info['label'], - "show_tile": perm_info['show_tile'] and perm_info['url'] and (not perm_info["url"].startswith('re:')), - "auth_header": perm_info['auth_header'], + "users": perm_info["corresponding_users"], + "label": perm_info["label"], + "show_tile": perm_info["show_tile"] + and perm_info["url"] + and (not perm_info["url"].startswith("re:")), + "auth_header": perm_info["auth_header"], "public": "visitors" in perm_info["allowed"], - "uris": uris + "uris": uris, } conf_dict = { - 'portal_domain': main_domain, - 'portal_path': '/yunohost/sso/', - 'additional_headers': { - 'Auth-User': 'uid', - 'Remote-User': 'uid', - 'Name': 'cn', - 'Email': 'mail' + "portal_domain": main_domain, + "portal_path": "/yunohost/sso/", + "additional_headers": { + "Auth-User": "uid", + "Remote-User": "uid", + "Name": "cn", + "Email": "mail", }, - 'domains': domains, - 'redirected_urls': redirected_urls, - 'redirected_regex': redirected_regex, - 'permissions': permissions, + "domains": domains, + "redirected_urls": redirected_urls, + "redirected_regex": redirected_regex, + "permissions": permissions, } - write_to_json('/etc/ssowat/conf.json', conf_dict, sort_keys=True, indent=4) + write_to_json("/etc/ssowat/conf.json", conf_dict, sort_keys=True, indent=4) from .utils.legacy import translate_legacy_rules_in_ssowant_conf_json_persistent + translate_legacy_rules_in_ssowant_conf_json_persistent() - logger.debug(m18n.n('ssowat_conf_generated')) + logger.debug(m18n.n("ssowat_conf_generated")) def app_change_label(app, new_label): from yunohost.permission import user_permission_update + installed = _is_installed(app) if not installed: - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) - logger.warning(m18n.n('app_label_deprecated')) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) + logger.warning(m18n.n("app_label_deprecated")) user_permission_update(app + ".main", label=new_label) # actions todo list: # * docstring + def app_action_list(app): - logger.warning(m18n.n('experimental_feature')) + logger.warning(m18n.n("experimental_feature")) # this will take care of checking if the app is installed app_info_dict = app_info(app) @@ -1451,13 +1715,13 @@ def app_action_list(app): return { "app": app, "app_name": app_info_dict["name"], - "actions": _get_app_actions(app) + "actions": _get_app_actions(app), } @is_unit_operation() def app_action_run(operation_logger, app, action, args=None): - logger.warning(m18n.n('experimental_feature')) + logger.warning(m18n.n("experimental_feature")) from yunohost.hook import hook_exec import tempfile @@ -1467,17 +1731,25 @@ def app_action_run(operation_logger, app, action, args=None): actions = {x["id"]: x for x in actions} if action not in actions: - raise YunohostError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys())), raw_msg=True) + raise YunohostError( + "action '%s' not available for app '%s', available actions are: %s" + % (action, app, ", ".join(actions.keys())), + raw_msg=True, + ) operation_logger.start() action_declaration = actions[action] # Retrieve arguments list for install script - args_dict = dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} + args_dict = ( + dict(urllib.parse.parse_qsl(args, keep_blank_values=True)) if args else {} + ) args_odict = _parse_args_for_action(actions[action], args=args_dict) - env_dict = _make_environment_for_app_script(app, args=args_odict, args_prefix="ACTION_") + env_dict = _make_environment_for_app_script( + app, args=args_odict, args_prefix="ACTION_" + ) env_dict["YNH_ACTION"] = action _, path = tempfile.mkstemp() @@ -1500,7 +1772,11 @@ def app_action_run(operation_logger, app, action, args=None): )[0] if retcode not in action_declaration.get("accepted_return_codes", [0]): - msg = "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode) + msg = "Error while executing action '%s' of app '%s': return code %s" % ( + action, + app, + retcode, + ) operation_logger.error(msg) raise YunohostError(msg, raw_msg=True) @@ -1515,7 +1791,7 @@ def app_action_run(operation_logger, app, action, args=None): # * merge translations on the json once the workflow is in place @is_unit_operation() def app_config_show_panel(operation_logger, app): - logger.warning(m18n.n('experimental_feature')) + logger.warning(m18n.n("experimental_feature")) from yunohost.hook import hook_exec @@ -1524,7 +1800,7 @@ def app_config_show_panel(operation_logger, app): operation_logger.start() config_panel = _get_app_config_panel(app) - config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') + config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config") app_id, app_instance_nb = _parse_app_instance_name(app) @@ -1542,14 +1818,15 @@ def app_config_show_panel(operation_logger, app): "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), } - return_code, parsed_values = hook_exec(config_script, - args=["show"], - env=env, - return_format="plain_dict" - ) + return_code, parsed_values = hook_exec( + config_script, args=["show"], env=env, return_format="plain_dict" + ) if return_code != 0: - raise Exception("script/config show return value code: %s (considered as an error)", return_code) + 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", []): @@ -1558,9 +1835,17 @@ def app_config_show_panel(operation_logger, app): section_id = section["id"] for option in section.get("options", []): option_name = option["name"] - generated_name = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name)).upper() + generated_name = ( + "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name) + ).upper() option["name"] = generated_name - logger.debug(" * '%s'.'%s'.'%s' -> %s", tab.get("name"), section.get("name"), option.get("name"), generated_name) + logger.debug( + " * '%s'.'%s'.'%s' -> %s", + tab.get("name"), + section.get("name"), + option.get("name"), + generated_name, + ) if generated_name in parsed_values: # code is not adapted for that so we have to mock expected format :/ @@ -1573,12 +1858,14 @@ def app_config_show_panel(operation_logger, app): option["default"] = parsed_values[generated_name] args_dict = _parse_args_in_yunohost_format( - {option["name"]: parsed_values[generated_name]}, - [option] + {option["name"]: parsed_values[generated_name]}, [option] ) option["default"] = args_dict[option["name"]][0] else: - logger.debug("Variable '%s' is not declared by config script, using default", generated_name) + logger.debug( + "Variable '%s' is not declared by config script, using default", + generated_name, + ) # do nothing, we'll use the default if present return { @@ -1592,16 +1879,18 @@ def app_config_show_panel(operation_logger, app): @is_unit_operation() def app_config_apply(operation_logger, app, args): - logger.warning(m18n.n('experimental_feature')) + logger.warning(m18n.n("experimental_feature")) from yunohost.hook import hook_exec installed = _is_installed(app) if not installed: - raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() + ) config_panel = _get_app_config_panel(app) - config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') + config_script = os.path.join(APPS_SETTING_PATH, app, "scripts", "config") if not config_panel or not os.path.exists(config_script): # XXX real exception @@ -1622,10 +1911,14 @@ def app_config_apply(operation_logger, app, args): section_id = section["id"] for option in section.get("options", []): option_name = option["name"] - generated_name = ("YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name)).upper() + generated_name = ( + "YNH_CONFIG_%s_%s_%s" % (tab_id, section_id, option_name) + ).upper() if generated_name in args: - logger.debug("include into env %s=%s", generated_name, args[generated_name]) + logger.debug( + "include into env %s=%s", generated_name, args[generated_name] + ) env[generated_name] = args[generated_name] else: logger.debug("no value for key id %s", generated_name) @@ -1633,15 +1926,21 @@ def app_config_apply(operation_logger, app, args): # 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) + 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, - )[0] + return_code = hook_exec( + config_script, + args=["apply"], + env=env, + )[0] if return_code != 0: - msg = "'script/config apply' return value code: %s (considered as an error)" % return_code + msg = ( + "'script/config apply' return value code: %s (considered as an error)" + % return_code + ) operation_logger.error(msg) raise Exception(msg) @@ -1670,8 +1969,8 @@ def _get_all_installed_apps_id(): def _get_app_actions(app_id): "Get app config panel stored in json or in toml" - actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, 'actions.toml') - actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, 'actions.json') + actions_toml_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.toml") + actions_json_path = os.path.join(APPS_SETTING_PATH, app_id, "actions.json") # sample data to get an idea of what is going on # this toml extract: @@ -1755,8 +2054,12 @@ def _get_app_actions(app_id): def _get_app_config_panel(app_id): "Get app config panel stored in json or in toml" - config_panel_toml_path = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.toml') - config_panel_json_path = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') + config_panel_toml_path = os.path.join( + APPS_SETTING_PATH, app_id, "config_panel.toml" + ) + config_panel_json_path = os.path.join( + APPS_SETTING_PATH, app_id, "config_panel.json" + ) # sample data to get an idea of what is going on # this toml extract: @@ -1824,7 +2127,9 @@ def _get_app_config_panel(app_id): # u'type': u'bool'}, if os.path.exists(config_panel_toml_path): - toml_config_panel = toml.load(open(config_panel_toml_path, "r"), _dict=OrderedDict) + toml_config_panel = toml.load( + open(config_panel_toml_path, "r"), _dict=OrderedDict + ) # transform toml format into json format config_panel = { @@ -1833,7 +2138,12 @@ def _get_app_config_panel(app_id): "panel": [], } - panels = [key_value for key_value in toml_config_panel.items() if key_value[0] not in ("name", "version") and isinstance(key_value[1], OrderedDict)] + panels = [ + key_value + for key_value in toml_config_panel.items() + if key_value[0] not in ("name", "version") + and isinstance(key_value[1], OrderedDict) + ] for key, value in panels: panel = { @@ -1842,7 +2152,11 @@ def _get_app_config_panel(app_id): "sections": [], } - sections = [k_v1 for k_v1 in value.items() if k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict)] + sections = [ + k_v1 + for k_v1 in value.items() + if k_v1[0] not in ("name",) and isinstance(k_v1[1], OrderedDict) + ] for section_key, section_value in sections: section = { @@ -1851,7 +2165,11 @@ def _get_app_config_panel(app_id): "options": [], } - options = [k_v for k_v in section_value.items() if k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict)] + options = [ + k_v + for k_v in section_value.items() + if k_v[0] not in ("name",) and isinstance(k_v[1], OrderedDict) + ] for option_key, option_value in options: option = dict(option_value) @@ -1882,10 +2200,11 @@ def _get_app_settings(app_id): """ if not _is_installed(app_id): - raise YunohostError('app_not_installed', app=app_id, all_apps=_get_all_installed_apps_id()) + raise YunohostError( + "app_not_installed", app=app_id, all_apps=_get_all_installed_apps_id() + ) try: - with open(os.path.join( - APPS_SETTING_PATH, app_id, 'settings.yml')) as f: + with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml")) as f: settings = yaml.load(f) # If label contains unicode char, this may later trigger issues when building strings... # FIXME: this should be propagated to read_yaml so that this fix applies everywhere I think... @@ -1898,15 +2217,17 @@ def _get_app_settings(app_id): # So we yolofix the settings if such an issue is found >_> # A simple call to `yunohost app list` (which happens quite often) should be enough # to migrate all app settings ... so this can probably be removed once we're past Bullseye... - if settings.get("path") != "/" and (settings.get("path", "").endswith("/") or not settings.get("path", "/").startswith("/")): + if settings.get("path") != "/" and ( + settings.get("path", "").endswith("/") + or not settings.get("path", "/").startswith("/") + ): settings["path"] = "/" + settings["path"].strip("/") _set_app_settings(app_id, settings) - if app_id == settings['id']: + if app_id == settings["id"]: return settings except (IOError, TypeError, KeyError): - logger.error(m18n.n('app_not_correctly_installed', - app=app_id)) + logger.error(m18n.n("app_not_correctly_installed", app=app_id)) return {} @@ -1919,8 +2240,7 @@ def _set_app_settings(app_id, settings): settings -- Dict with app settings """ - with open(os.path.join( - APPS_SETTING_PATH, app_id, 'settings.yml'), 'w') as f: + with open(os.path.join(APPS_SETTING_PATH, app_id, "settings.yml"), "w") as f: yaml.safe_dump(settings, f, default_flow_style=False) @@ -1936,7 +2256,7 @@ def _extract_app_from_file(path, remove=False): Dict manifest """ - logger.debug(m18n.n('extracting')) + logger.debug(m18n.n("extracting")) if os.path.exists(APP_TMP_FOLDER): shutil.rmtree(APP_TMP_FOLDER) @@ -1945,39 +2265,43 @@ def _extract_app_from_file(path, remove=False): 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) - if path[-1] != '/': - path = path + '/' + if path[-1] != "/": + path = path + "/" extract_result = os.system('cp -a "%s" %s' % (path, APP_TMP_FOLDER)) else: extract_result = 1 if extract_result != 0: - raise YunohostError('app_extraction_failed') + raise YunohostError("app_extraction_failed") try: 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 manifest = _get_manifest_of_app(extracted_app_folder) - manifest['lastUpdate'] = int(time.time()) + manifest["lastUpdate"] = int(time.time()) except IOError: - raise YunohostError('app_install_files_invalid') + raise YunohostError("app_install_files_invalid") except ValueError as e: - raise YunohostError('app_manifest_invalid', error=e) + raise YunohostError("app_manifest_invalid", error=e) - logger.debug(m18n.n('done')) + logger.debug(m18n.n("done")) - manifest['remote'] = {'type': 'file', 'path': path} + manifest["remote"] = {"type": "file", "path": path} return manifest, extracted_app_folder @@ -2093,7 +2417,9 @@ def _get_manifest_of_app(path): manifest = manifest_toml.copy() install_arguments = [] - for name, values in manifest_toml.get("arguments", {}).get("install", {}).items(): + for name, values in ( + manifest_toml.get("arguments", {}).get("install", {}).items() + ): args = values.copy() args["name"] = name @@ -2104,7 +2430,11 @@ def _get_manifest_of_app(path): elif os.path.exists(os.path.join(path, "manifest.json")): manifest = read_json(os.path.join(path, "manifest.json")) else: - raise YunohostError("There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." % path, raw_msg=True) + raise YunohostError( + "There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." + % path, + raw_msg=True, + ) manifest["arguments"] = _set_default_ask_questions(manifest.get("arguments", {})) return manifest @@ -2131,11 +2461,13 @@ def _set_default_ask_questions(arguments): # type namei # N.B. : this is only for install script ... should be reworked for other # scripts if we supports args for other scripts in the future... - questions_with_default = [("domain", "domain"), # i18n: app_manifest_install_ask_domain - ("path", "path"), # i18n: app_manifest_install_ask_path - ("password", "password"), # i18n: app_manifest_install_ask_password - ("user", "admin"), # i18n: app_manifest_install_ask_admin - ("boolean", "is_public")] # i18n: app_manifest_install_ask_is_public + questions_with_default = [ + ("domain", "domain"), # i18n: app_manifest_install_ask_domain + ("path", "path"), # i18n: app_manifest_install_ask_path + ("password", "password"), # i18n: app_manifest_install_ask_password + ("user", "admin"), # i18n: app_manifest_install_ask_admin + ("boolean", "is_public"), + ] # i18n: app_manifest_install_ask_is_public for script_name, arg_list in arguments.items(): @@ -2150,7 +2482,10 @@ def _set_default_ask_questions(arguments): # continue # If this arg corresponds to a question with default ask message... - if any((arg.get("type"), arg["name"]) == question for question in questions_with_default): + if any( + (arg.get("type"), arg["name"]) == question + for question in questions_with_default + ): # The key is for example "app_manifest_install_ask_domain" key = "app_manifest_%s_ask_%s" % (script_name, arg["name"]) arg["ask"] = m18n.n(key) @@ -2158,7 +2493,7 @@ def _set_default_ask_questions(arguments): return arguments -def _get_git_last_commit_hash(repository, reference='HEAD'): +def _get_git_last_commit_hash(repository, reference="HEAD"): """ Attempt to retrieve the last commit hash of a git repository @@ -2167,8 +2502,9 @@ def _get_git_last_commit_hash(repository, reference='HEAD'): """ try: - cmd = "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'"\ - .format(repository, reference) + cmd = "git ls-remote --exit-code {0} {1} | awk '{{print $1}}'".format( + repository, reference + ) commit = check_output(cmd) except subprocess.CalledProcessError: logger.error("unable to get last commit from %s", repository) @@ -2190,65 +2526,74 @@ def _fetch_app_from_git(app): """ extracted_app_folder = APP_TMP_FOLDER - app_tmp_archive = '{0}.zip'.format(extracted_app_folder) + app_tmp_archive = "{0}.zip".format(extracted_app_folder) if os.path.exists(extracted_app_folder): shutil.rmtree(extracted_app_folder) if os.path.exists(app_tmp_archive): os.remove(app_tmp_archive) - logger.debug(m18n.n('downloading')) + logger.debug(m18n.n("downloading")) # Extract URL, branch and revision to download - if ('@' in app) or ('http://' in app) or ('https://' in app): + if ("@" in app) or ("http://" in app) or ("https://" in app): url = app - branch = 'master' + branch = "master" if "/tree/" in url: url, branch = url.split("/tree/", 1) - revision = 'HEAD' + revision = "HEAD" else: app_dict = _load_apps_catalog()["apps"] app_id, _ = _parse_app_instance_name(app) if app_id not in app_dict: - raise YunohostError('app_unknown') - elif 'git' not in app_dict[app_id]: - raise YunohostError('app_unsupported_remote_type') + raise YunohostError("app_unknown") + elif "git" not in app_dict[app_id]: + raise YunohostError("app_unsupported_remote_type") app_info = app_dict[app_id] - url = app_info['git']['url'] - branch = app_info['git']['branch'] - revision = str(app_info['git']['revision']) + url = app_info["git"]["url"] + branch = app_info["git"]["branch"] + revision = str(app_info["git"]["revision"]) # Download only this commit try: # We don't use git clone because, git clone can't download # a specific revision only - run_commands([['git', 'init', extracted_app_folder]], shell=False) - run_commands([ - ['git', 'remote', 'add', 'origin', url], - ['git', 'fetch', '--depth=1', 'origin', - branch if revision == 'HEAD' else revision], - ['git', 'reset', '--hard', 'FETCH_HEAD'] - ], cwd=extracted_app_folder, shell=False) + run_commands([["git", "init", extracted_app_folder]], shell=False) + run_commands( + [ + ["git", "remote", "add", "origin", url], + [ + "git", + "fetch", + "--depth=1", + "origin", + branch if revision == "HEAD" else revision, + ], + ["git", "reset", "--hard", "FETCH_HEAD"], + ], + cwd=extracted_app_folder, + shell=False, + ) manifest = _get_manifest_of_app(extracted_app_folder) except subprocess.CalledProcessError: - raise YunohostError('app_sources_fetch_failed') + raise YunohostError("app_sources_fetch_failed") except ValueError as e: - raise YunohostError('app_manifest_invalid', error=e) + raise YunohostError("app_manifest_invalid", error=e) else: - logger.debug(m18n.n('done')) + logger.debug(m18n.n("done")) # Store remote repository info into the returned manifest - manifest['remote'] = {'type': 'git', 'url': url, 'branch': branch} - if revision == 'HEAD': + manifest["remote"] = {"type": "git", "url": url, "branch": branch} + if revision == "HEAD": try: - manifest['remote']['revision'] = _get_git_last_commit_hash(url, branch) + manifest["remote"]["revision"] = _get_git_last_commit_hash(url, branch) except Exception as e: logger.debug("cannot get last commit hash because: %s ", e) else: - manifest['remote']['revision'] = revision - manifest['lastUpdate'] = app_info['lastUpdate'] + manifest["remote"]["revision"] = revision + manifest["lastUpdate"] = app_info["lastUpdate"] return manifest, extracted_app_folder @@ -2276,10 +2621,10 @@ def _installed_instance_number(app, last=False): for installed_app in installed_apps: if number == 0 and app == installed_app: number = 1 - elif '__' in installed_app: - if app == installed_app[:installed_app.index('__')]: - if int(installed_app[installed_app.index('__') + 2:]) > number: - number = int(installed_app[installed_app.index('__') + 2:]) + elif "__" in installed_app: + if app == installed_app[: installed_app.index("__")]: + if int(installed_app[installed_app.index("__") + 2 :]) > number: + number = int(installed_app[installed_app.index("__") + 2 :]) return number @@ -2288,7 +2633,7 @@ def _installed_instance_number(app, last=False): instances_dict = app_map(app=app, raw=True) for key, domain in instances_dict.items(): for key, path in domain.items(): - instance_number_list.append(path['instance']) + instance_number_list.append(path["instance"]) return sorted(instance_number_list) @@ -2338,24 +2683,28 @@ def _value_for_locale(values): def _check_manifest_requirements(manifest, app_instance_name): """Check if required packages are met from the manifest""" - packaging_format = int(manifest.get('packaging_format', 0)) + packaging_format = int(manifest.get("packaging_format", 0)) if packaging_format not in [0, 1]: raise YunohostError("app_packaging_format_not_supported") - requirements = manifest.get('requirements', dict()) + requirements = manifest.get("requirements", dict()) if not requirements: return - logger.debug(m18n.n('app_requirements_checking', app=app_instance_name)) + logger.debug(m18n.n("app_requirements_checking", app=app_instance_name)) # Iterate over requirements for pkgname, spec in requirements.items(): if not packages.meets_version_specifier(pkgname, spec): version = packages.ynh_packages_version()[pkgname]["version"] - raise YunohostError('app_requirements_unmeet', - pkgname=pkgname, version=version, - spec=spec, app=app_instance_name) + raise YunohostError( + "app_requirements_unmeet", + pkgname=pkgname, + version=version, + spec=spec, + app=app_instance_name, + ) def _parse_args_from_manifest(manifest, action, args={}): @@ -2372,11 +2721,11 @@ def _parse_args_from_manifest(manifest, action, args={}): args -- A dictionnary of arguments to parse """ - if action not in manifest['arguments']: + if action not in manifest["arguments"]: logger.debug("no arguments found for '%s' in manifest", action) return OrderedDict() - action_args = manifest['arguments'][action] + action_args = manifest["arguments"][action] return _parse_args_in_yunohost_format(args, action_args) @@ -2395,11 +2744,11 @@ def _parse_args_for_action(action, args={}): """ args_dict = OrderedDict() - if 'arguments' not in action: + if "arguments" not in action: logger.debug("no arguments found for '%s' in manifest", action) return args_dict - action_args = action['arguments'] + action_args = action["arguments"] return _parse_args_in_yunohost_format(args, action_args) @@ -2414,11 +2763,11 @@ class YunoHostArgumentFormatParser(object): def parse_question(self, question, user_answers): parsed_question = Question() - parsed_question.name = question['name'] - parsed_question.default = question.get('default', None) - parsed_question.choices = question.get('choices', []) - parsed_question.optional = question.get('optional', False) - parsed_question.ask = question.get('ask') + parsed_question.name = question["name"] + parsed_question.default = question.get("default", None) + parsed_question.choices = question.get("choices", []) + parsed_question.optional = question.get("optional", False) + parsed_question.ask = question.get("ask") parsed_question.value = user_answers.get(parsed_question.name) if parsed_question.ask is None: @@ -2434,19 +2783,27 @@ class YunoHostArgumentFormatParser(object): question = self.parse_question(question, user_answers) if question.value is None: - text_for_user_input_in_cli = self._format_text_for_user_input_in_cli(question) + text_for_user_input_in_cli = self._format_text_for_user_input_in_cli( + question + ) try: - question.value = msignals.prompt(text_for_user_input_in_cli, self.hide_user_input_in_prompt) + question.value = msignals.prompt( + text_for_user_input_in_cli, self.hide_user_input_in_prompt + ) except NotImplementedError: question.value = None # we don't have an answer, check optional and default_value - if question.value is None or question.value == '': + if question.value is None or question.value == "": if not question.optional and question.default is None: - raise YunohostError('app_argument_required', name=question.name) + raise YunohostError("app_argument_required", name=question.name) else: - question.value = getattr(self, "default_value", None) if question.default is None else question.default + question.value = ( + getattr(self, "default_value", None) + if question.default is None + else question.default + ) # we have an answer, do some post checks if question.value is not None: @@ -2460,17 +2817,20 @@ class YunoHostArgumentFormatParser(object): return (question.value, self.argument_type) def _raise_invalid_answer(self, question): - raise YunohostError('app_argument_choice_invalid', name=question.name, - choices=', '.join(question.choices)) + raise YunohostError( + "app_argument_choice_invalid", + name=question.name, + choices=", ".join(question.choices), + ) def _format_text_for_user_input_in_cli(self, question): text_for_user_input_in_cli = _value_for_locale(question.ask) if question.choices: - text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question.choices)) + text_for_user_input_in_cli += " [{0}]".format(" | ".join(question.choices)) if question.default is not None: - text_for_user_input_in_cli += ' (default: {0})'.format(question.default) + text_for_user_input_in_cli += " (default: {0})".format(question.default) return text_for_user_input_in_cli @@ -2490,21 +2850,26 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): forbidden_chars = "{}" def parse_question(self, question, user_answers): - question = super(PasswordArgumentParser, self).parse_question(question, user_answers) + question = super(PasswordArgumentParser, self).parse_question( + question, user_answers + ) if question.default is not None: - raise YunohostError('app_argument_password_no_default', name=question.name) + raise YunohostError("app_argument_password_no_default", name=question.name) return question def _post_parse_value(self, question): if any(char in question.value for char in self.forbidden_chars): - raise YunohostError('pattern_password_app', forbidden_chars=self.forbidden_chars) + raise YunohostError( + "pattern_password_app", forbidden_chars=self.forbidden_chars + ) # If it's an optional argument the value should be empty or strong enough if not question.optional or question.value: from yunohost.utils.password import assert_password_is_strong_enough - assert_password_is_strong_enough('user', question.value) + + assert_password_is_strong_enough("user", question.value) return super(PasswordArgumentParser, self)._post_parse_value(question) @@ -2519,7 +2884,9 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser): default_value = False def parse_question(self, question, user_answers): - question = super(BooleanArgumentParser, self).parse_question(question, user_answers) + question = super(BooleanArgumentParser, self).parse_question( + question, user_answers + ) if question.default is None: question.default = False @@ -2533,7 +2900,7 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser): if question.default is not None: formatted_default = "yes" if question.default else "no" - text_for_user_input_in_cli += ' (default: {0})'.format(formatted_default) + text_for_user_input_in_cli += " (default: {0})".format(formatted_default) return text_for_user_input_in_cli @@ -2547,8 +2914,11 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser): if str(question.value).lower() in ["0", "no", "n", "false"]: return 0 - raise YunohostError('app_argument_choice_invalid', name=question.name, - choices='yes, no, y, n, 1, 0') + raise YunohostError( + "app_argument_choice_invalid", + name=question.name, + choices="yes, no, y, n, 1, 0", + ) class DomainArgumentParser(YunoHostArgumentFormatParser): @@ -2557,7 +2927,9 @@ class DomainArgumentParser(YunoHostArgumentFormatParser): def parse_question(self, question, user_answers): from yunohost.domain import domain_list, _get_maindomain - question = super(DomainArgumentParser, self).parse_question(question, user_answers) + question = super(DomainArgumentParser, self).parse_question( + question, user_answers + ) if question.default is None: question.default = _get_maindomain() @@ -2567,8 +2939,9 @@ class DomainArgumentParser(YunoHostArgumentFormatParser): return question def _raise_invalid_answer(self, question): - raise YunohostError('app_argument_invalid', name=question.name, - error=m18n.n('domain_unknown')) + raise YunohostError( + "app_argument_invalid", name=question.name, error=m18n.n("domain_unknown") + ) class UserArgumentParser(YunoHostArgumentFormatParser): @@ -2578,7 +2951,9 @@ class UserArgumentParser(YunoHostArgumentFormatParser): from yunohost.user import user_list, user_info from yunohost.domain import _get_maindomain - question = super(UserArgumentParser, self).parse_question(question, user_answers) + question = super(UserArgumentParser, self).parse_question( + question, user_answers + ) question.choices = user_list()["users"] if question.default is None: root_mail = "root@%s" % _get_maindomain() @@ -2590,8 +2965,11 @@ class UserArgumentParser(YunoHostArgumentFormatParser): return question def _raise_invalid_answer(self, question): - raise YunohostError('app_argument_invalid', name=question.name, - error=m18n.n('user_unknown', user=question.value)) + raise YunohostError( + "app_argument_invalid", + name=question.name, + error=m18n.n("user_unknown", user=question.value), + ) class NumberArgumentParser(YunoHostArgumentFormatParser): @@ -2599,7 +2977,9 @@ class NumberArgumentParser(YunoHostArgumentFormatParser): default_value = "" def parse_question(self, question, user_answers): - question = super(NumberArgumentParser, self).parse_question(question, user_answers) + question = super(NumberArgumentParser, self).parse_question( + question, user_answers + ) if question.default is None: question.default = 0 @@ -2613,8 +2993,9 @@ class NumberArgumentParser(YunoHostArgumentFormatParser): if isinstance(question.value, str) and question.value.isdigit(): return int(question.value) - raise YunohostError('app_argument_invalid', name=question.name, - error=m18n.n('invalid_number')) + raise YunohostError( + "app_argument_invalid", name=question.name, error=m18n.n("invalid_number") + ) class DisplayTextArgumentParser(YunoHostArgumentFormatParser): @@ -2664,8 +3045,12 @@ def _validate_and_normalize_webpath(manifest, args_dict, app_folder): # If there's only one "domain" and "path", validate that domain/path # is an available url and normalize the path. - domain_args = [(name, value[0]) for name, value in args_dict.items() if value[1] == "domain"] - path_args = [(name, value[0]) for name, value in args_dict.items() if value[1] == "path"] + domain_args = [ + (name, value[0]) for name, value in args_dict.items() if value[1] == "domain" + ] + path_args = [ + (name, value[0]) for name, value in args_dict.items() if value[1] == "path" + ] if len(domain_args) == 1 and len(path_args) == 1: @@ -2691,10 +3076,15 @@ def _validate_and_normalize_webpath(manifest, args_dict, app_folder): # Full-domain apps typically declare something like path_url="/" or path=/ # and use ynh_webpath_register or yunohost_app_checkurl inside the install script - install_script_content = open(os.path.join(app_folder, 'scripts/install')).read() + install_script_content = open( + os.path.join(app_folder, "scripts/install") + ).read() - if re.search(r"\npath(_url)?=[\"']?/[\"']?\n", install_script_content) \ - and re.search(r"(ynh_webpath_register|yunohost app checkurl)", install_script_content): + if re.search( + r"\npath(_url)?=[\"']?/[\"']?\n", install_script_content + ) and re.search( + r"(ynh_webpath_register|yunohost app checkurl)", install_script_content + ): domain = domain_args[0][1] _assert_no_conflicting_apps(domain, "/", full_domain=True) @@ -2707,9 +3097,9 @@ def _normalize_domain_path(domain, path): # Remove http/https prefix if it's there if domain.startswith("https://"): - domain = domain[len("https://"):] + domain = domain[len("https://") :] elif domain.startswith("http://"): - domain = domain[len("http://"):] + domain = domain[len("http://") :] # Remove trailing slashes domain = domain.rstrip("/").lower() @@ -2733,8 +3123,8 @@ def _get_conflicting_apps(domain, path, ignore_app=None): domain, path = _normalize_domain_path(domain, path) # Abort if domain is unknown - if domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + if domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) # Fetch apps map apps_map = app_map(raw=True) @@ -2763,17 +3153,19 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False 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, - )) + 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, + ) + ) if full_domain: - raise YunohostError('app_full_domain_unavailable', domain=domain) + raise YunohostError("app_full_domain_unavailable", domain=domain) else: - raise YunohostError('app_location_unavailable', apps="\n".join(apps)) + raise YunohostError("app_location_unavailable", apps="\n".join(apps)) def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): @@ -2787,11 +3179,13 @@ def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): "YNH_APP_ID": app_id, "YNH_APP_INSTANCE_NAME": app, "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), - "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?") + "YNH_APP_MANIFEST_VERSION": manifest.get("version", "?"), } for arg_name, arg_value_and_type in args.items(): - env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str(arg_value_and_type[0]) + env_dict["YNH_%s%s" % (args_prefix, arg_name.upper())] = str( + arg_value_and_type[0] + ) return env_dict @@ -2820,8 +3214,12 @@ def _parse_app_instance_name(app_instance_name): """ match = re_app_instance_name.match(app_instance_name) assert match, "Could not parse app instance name : %s" % app_instance_name - appid = match.groupdict().get('appid') - app_instance_nb = int(match.groupdict().get('appinstancenb')) if match.groupdict().get('appinstancenb') is not None else 1 + appid = match.groupdict().get("appid") + app_instance_nb = ( + int(match.groupdict().get("appinstancenb")) + if match.groupdict().get("appinstancenb") is not None + else 1 + ) return (appid, app_instance_nb) @@ -2848,15 +3246,19 @@ def _initialize_apps_catalog_system(): cron_job.append("(sleep $((RANDOM%3600));") cron_job.append("yunohost tools update --apps > /dev/null) &") try: - logger.debug("Initializing apps catalog system with YunoHost's default app list") + logger.debug( + "Initializing apps catalog system with YunoHost's default app list" + ) write_to_yaml(APPS_CATALOG_CONF, default_apps_catalog_list) logger.debug("Installing apps catalog fetch daily cron job") - write_to_file(APPS_CATALOG_CRON_PATH, '\n'.join(cron_job)) + write_to_file(APPS_CATALOG_CRON_PATH, "\n".join(cron_job)) chown(APPS_CATALOG_CRON_PATH, uid="root", gid="root") chmod(APPS_CATALOG_CRON_PATH, 0o755) except Exception as e: - raise YunohostError("Could not initialize the apps catalog system... : %s" % str(e)) + raise YunohostError( + "Could not initialize the apps catalog system... : %s" % str(e) + ) logger.success(m18n.n("apps_catalog_init_success")) @@ -2877,7 +3279,9 @@ def _read_apps_catalog_list(): def _actual_apps_catalog_api_url(base_url): - return "{base_url}/v{version}/apps.json".format(base_url=base_url, version=APPS_CATALOG_API_VERSION) + return "{base_url}/v{version}/apps.json".format( + base_url=base_url, version=APPS_CATALOG_API_VERSION + ) def _update_apps_catalog(): @@ -2901,7 +3305,7 @@ def _update_apps_catalog(): # Create cache folder if needed if not os.path.exists(APPS_CATALOG_CACHE): logger.debug("Initialize folder for apps catalog cache") - mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid='root') + mkdir(APPS_CATALOG_CACHE, mode=0o750, parents=True, uid="root") for apps_catalog in apps_catalog_list: apps_catalog_id = apps_catalog["id"] @@ -2911,17 +3315,26 @@ def _update_apps_catalog(): try: apps_catalog_content = download_json(actual_api_url) except Exception as e: - raise YunohostError("apps_catalog_failed_to_download", apps_catalog=apps_catalog_id, error=str(e)) + raise YunohostError( + "apps_catalog_failed_to_download", + apps_catalog=apps_catalog_id, + error=str(e), + ) # Remember the apps_catalog api version for later apps_catalog_content["from_api_version"] = APPS_CATALOG_API_VERSION # Save the apps_catalog data in the cache - cache_file = "{cache_folder}/{list}.json".format(cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id) + cache_file = "{cache_folder}/{list}.json".format( + cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id + ) try: write_to_json(cache_file, apps_catalog_content) except Exception as e: - raise YunohostError("Unable to write cache data for %s apps_catalog : %s" % (apps_catalog_id, str(e))) + raise YunohostError( + "Unable to write cache data for %s apps_catalog : %s" + % (apps_catalog_id, str(e)) + ) logger.success(m18n.n("apps_catalog_update_success")) @@ -2932,25 +3345,32 @@ def _load_apps_catalog(): corresponding to all known apps and categories """ - merged_catalog = { - "apps": {}, - "categories": [] - } + merged_catalog = {"apps": {}, "categories": []} for apps_catalog_id in [L["id"] for L in _read_apps_catalog_list()]: # Let's load the json from cache for this catalog - cache_file = "{cache_folder}/{list}.json".format(cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id) + cache_file = "{cache_folder}/{list}.json".format( + cache_folder=APPS_CATALOG_CACHE, list=apps_catalog_id + ) try: - apps_catalog_content = read_json(cache_file) if os.path.exists(cache_file) else None + apps_catalog_content = ( + read_json(cache_file) if os.path.exists(cache_file) else None + ) except Exception as e: - raise YunohostError("Unable to read cache for apps_catalog %s : %s" % (cache_file, e), raw_msg=True) + raise YunohostError( + "Unable to read cache for apps_catalog %s : %s" % (cache_file, e), + raw_msg=True, + ) # Check that the version of the data matches version .... # ... otherwise it means we updated yunohost in the meantime # and need to update the cache for everything to be consistent - if not apps_catalog_content or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION: + if ( + not apps_catalog_content + or apps_catalog_content.get("from_api_version") != APPS_CATALOG_API_VERSION + ): logger.info(m18n.n("apps_catalog_obsolete_cache")) _update_apps_catalog() apps_catalog_content = read_json(cache_file) @@ -2963,11 +3383,13 @@ def _load_apps_catalog(): # (N.B. : there's a small edge case where multiple apps catalog could be listing the same apps ... # in which case we keep only the first one found) if app in merged_catalog["apps"]: - logger.warning("Duplicate app %s found between apps catalog %s and %s" - % (app, apps_catalog_id, merged_catalog["apps"][app]['repository'])) + logger.warning( + "Duplicate app %s found between apps catalog %s and %s" + % (app, apps_catalog_id, merged_catalog["apps"][app]["repository"]) + ) continue - info['repository'] = apps_catalog_id + info["repository"] = apps_catalog_id merged_catalog["apps"][app] = info # Annnnd categories @@ -2975,6 +3397,7 @@ def _load_apps_catalog(): return merged_catalog + # # ############################### # # Small utilities # @@ -2996,9 +3419,9 @@ def is_true(arg): if isinstance(arg, bool): return arg elif isinstance(arg, str): - return arg.lower() in ['yes', 'true', 'on'] + return arg.lower() in ["yes", "true", "on"] else: - logger.debug('arg should be a boolean or a string, got %r', arg) + logger.debug("arg should be a boolean or a string, got %r", arg) return True if arg else False @@ -3008,7 +3431,10 @@ def unstable_apps(): for infos in app_list(full=True)["apps"]: - if not infos.get("from_catalog") or infos.get("from_catalog").get("state") in ["inprogress", "notworking"]: + if not infos.get("from_catalog") or infos.get("from_catalog").get("state") in [ + "inprogress", + "notworking", + ]: output.append(infos["id"]) return output @@ -3026,6 +3452,7 @@ def _assert_system_is_sane_for_app(manifest, when): return "php7.3-fpm" else: return service + services = [replace_alias(s) for s in services] # We only check those, mostly to ignore "custom" services @@ -3043,11 +3470,14 @@ def _assert_system_is_sane_for_app(manifest, when): faulty_services = [s for s in services if service_status(s)["status"] != "running"] if faulty_services: if when == "pre": - raise YunohostError('app_action_cannot_be_ran_because_required_services_down', - services=', '.join(faulty_services)) + raise YunohostError( + "app_action_cannot_be_ran_because_required_services_down", + services=", ".join(faulty_services), + ) elif when == "post": - raise YunohostError('app_action_broke_system', - services=', '.join(faulty_services)) + raise YunohostError( + "app_action_broke_system", services=", ".join(faulty_services) + ) if packages.dpkg_is_broken(): if when == "pre": @@ -3063,8 +3493,14 @@ LEGACY_PHP_VERSION_REPLACEMENTS = [ ("/var/run/php/php7.0-fpm", "/var/run/php/php7.3-fpm"), ("php5", "php7.3"), ("php7.0", "php7.3"), - ('phpversion="${phpversion:-7.0}"', 'phpversion="${phpversion:-7.3}"'), # Many helpers like the composer ones use 7.0 by default ... - ('"$phpversion" == "7.0"', '$(bc <<< "$phpversion >= 7.3") -eq 1') # patch ynh_install_php to refuse installing/removing php <= 7.3 + ( + 'phpversion="${phpversion:-7.0}"', + 'phpversion="${phpversion:-7.3}"', + ), # Many helpers like the composer ones use 7.0 by default ... + ( + '"$phpversion" == "7.0"', + '$(bc <<< "$phpversion >= 7.3") -eq 1', + ), # patch ynh_install_php to refuse installing/removing php <= 7.3 ] @@ -3084,15 +3520,20 @@ def _patch_legacy_php_versions(app_folder): if not os.path.isfile(filename): continue - c = "sed -i " \ - + "".join("-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r) for p, r in LEGACY_PHP_VERSION_REPLACEMENTS) \ + c = ( + "sed -i " + + "".join( + "-e 's@{pattern}@{replace}@g' ".format(pattern=p, replace=r) + for p, r in LEGACY_PHP_VERSION_REPLACEMENTS + ) + "%s" % filename + ) os.system(c) def _patch_legacy_php_versions_in_settings(app_folder): - settings = read_yaml(os.path.join(app_folder, 'settings.yml')) + settings = read_yaml(os.path.join(app_folder, "settings.yml")) if settings.get("fpm_config_dir") == "/etc/php/7.0/fpm": settings["fpm_config_dir"] = "/etc/php/7.3/fpm" @@ -3102,12 +3543,14 @@ def _patch_legacy_php_versions_in_settings(app_folder): settings["phpversion"] = "7.3" # We delete these checksums otherwise the file will appear as manually modified - list_to_remove = ["checksum__etc_php_7.0_fpm_pool", - "checksum__etc_nginx_conf.d"] - settings = {k: v for k, v in settings.items() - if not any(k.startswith(to_remove) for to_remove in list_to_remove)} + list_to_remove = ["checksum__etc_php_7.0_fpm_pool", "checksum__etc_nginx_conf.d"] + settings = { + k: v + for k, v in settings.items() + if not any(k.startswith(to_remove) for to_remove in list_to_remove) + } - write_to_yaml(app_folder + '/settings.yml', settings) + write_to_yaml(app_folder + "/settings.yml", settings) def _patch_legacy_helpers(app_folder): @@ -3124,7 +3567,7 @@ def _patch_legacy_helpers(app_folder): "yunohost app initdb": { "pattern": r"(sudo )?yunohost app initdb \"?(\$\{?\w+\}?)\"?\s+-p\s\"?(\$\{?\w+\}?)\"?", "replace": r"ynh_mysql_setup_db --db_user=\2 --db_name=\2 --db_pwd=\3", - "important": True + "important": True, }, # Replace # sudo yunohost app checkport whaterver @@ -3133,7 +3576,7 @@ def _patch_legacy_helpers(app_folder): "yunohost app checkport": { "pattern": r"(sudo )?yunohost app checkport", "replace": r"ynh_port_available", - "important": True + "important": True, }, # We can't migrate easily port-available # .. but at the time of writing this code, only two non-working apps are using it. @@ -3145,7 +3588,7 @@ def _patch_legacy_helpers(app_folder): "yunohost app checkurl": { "pattern": r"(sudo )?yunohost app checkurl \"?(\$\{?\w+\}?)\/?(\$\{?\w+\}?)\"?\s+-a\s\"?(\$\{?\w+\}?)\"?", "replace": r"ynh_webpath_register --app=\4 --domain=\2 --path_url=\3", - "important": True + "important": True, }, # Remove # Automatic diagnosis data from YunoHost @@ -3154,39 +3597,38 @@ def _patch_legacy_helpers(app_folder): "yunohost tools diagnosis": { "pattern": r"(Automatic diagnosis data from YunoHost( *\n)*)? *(__\w+__)? *\$\(yunohost tools diagnosis.*\)(__\w+__)?", "replace": r"", - "important": False + "important": False, }, # Old $1, $2 in backup/restore scripts... "app=$2": { "only_for": ["scripts/backup", "scripts/restore"], "pattern": r"app=\$2", "replace": r"app=$YNH_APP_INSTANCE_NAME", - "important": True + "important": True, }, # Old $1, $2 in backup/restore scripts... "backup_dir=$1": { "only_for": ["scripts/backup", "scripts/restore"], "pattern": r"backup_dir=\$1", "replace": r"backup_dir=.", - "important": True + "important": True, }, # Old $1, $2 in backup/restore scripts... "restore_dir=$1": { "only_for": ["scripts/restore"], "pattern": r"restore_dir=\$1", "replace": r"restore_dir=.", - "important": True + "important": True, }, # Old $1, $2 in install scripts... # We ain't patching that shit because it ain't trivial to patch all args... - "domain=$1": { - "only_for": ["scripts/install"], - "important": True - } + "domain=$1": {"only_for": ["scripts/install"], "important": True}, } for helper, infos in stuff_to_replace.items(): - infos["pattern"] = re.compile(infos["pattern"]) if infos.get("pattern") else None + infos["pattern"] = ( + re.compile(infos["pattern"]) if infos.get("pattern") else None + ) infos["replace"] = infos.get("replace") for filename in files_to_patch: @@ -3202,7 +3644,9 @@ def _patch_legacy_helpers(app_folder): for helper, infos in stuff_to_replace.items(): # Ignore if not relevant for this file - if infos.get("only_for") and not any(filename.endswith(f) for f in infos["only_for"]): + if infos.get("only_for") and not any( + filename.endswith(f) for f in infos["only_for"] + ): continue # If helper is used, attempt to patch the file @@ -3216,13 +3660,22 @@ def _patch_legacy_helpers(app_folder): # couldn't patch the deprecated helper in the previous lines. In # that case, abort the install or whichever step is performed if helper in content and infos["important"]: - raise YunohostError("This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", raw_msg=True) + raise YunohostError( + "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", + raw_msg=True, + ) if replaced_stuff: # Check the app do load the helper # If it doesn't, add the instruction ourselve (making sure it's after the #!/bin/bash if it's there... - if filename.split("/")[-1] in ["install", "remove", "upgrade", "backup", "restore"]: + if filename.split("/")[-1] in [ + "install", + "remove", + "upgrade", + "backup", + "restore", + ]: source_helpers = "source /usr/share/yunohost/helpers" if source_helpers not in content: content.replace("#!/bin/bash", "#!/bin/bash\n" + source_helpers) @@ -3234,4 +3687,6 @@ def _patch_legacy_helpers(app_folder): if show_warning: # And complain about those damn deprecated helpers - logger.error(r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ...") + logger.error( + r"/!\ Packagers ! This app uses a very old deprecated helpers ... Yunohost automatically patched the helpers to use the new recommended practice, but please do consider fixing the upstream code right now ..." + ) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 455c39f84..0392619de 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -44,16 +44,21 @@ from moulinette.utils.filesystem import read_file, mkdir, write_to_yaml, read_ya from moulinette.utils.process import check_output from yunohost.app import ( - app_info, _is_installed, + app_info, + _is_installed, _make_environment_for_app_script, dump_app_log_extract_for_debugging, _patch_legacy_helpers, _patch_legacy_php_versions, _patch_legacy_php_versions_in_settings, - LEGACY_PHP_VERSION_REPLACEMENTS + LEGACY_PHP_VERSION_REPLACEMENTS, ) from yunohost.hook import ( - hook_list, hook_info, hook_callback, hook_exec, CUSTOM_HOOK_FOLDER + hook_list, + hook_info, + hook_callback, + hook_exec, + CUSTOM_HOOK_FOLDER, ) from yunohost.tools import tools_postinstall from yunohost.regenconf import regen_conf @@ -62,13 +67,13 @@ from yunohost.utils.error import YunohostError from yunohost.utils.packages import ynh_packages_version from yunohost.settings import settings_get -BACKUP_PATH = '/home/yunohost.backup' -ARCHIVES_PATH = '%s/archives' % BACKUP_PATH +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') +logger = getActionLogger("yunohost.backup") class BackupRestoreTargetsManager(object): @@ -81,10 +86,7 @@ class BackupRestoreTargetsManager(object): def __init__(self): self.targets = {} - self.results = { - "system": {}, - "apps": {} - } + self.results = {"system": {}, "apps": {}} def set_result(self, category, element, value): """ @@ -107,14 +109,18 @@ class BackupRestoreTargetsManager(object): self.results[category][element] = value else: currentValue = self.results[category][element] - if (levels.index(currentValue) > levels.index(value)): + 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): + 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 @@ -146,13 +152,15 @@ class BackupRestoreTargetsManager(object): # 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] + 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] + 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") @@ -173,19 +181,26 @@ class BackupRestoreTargetsManager(object): 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) + 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] + 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] + return [ + target + for target in self.targets[category] + if self.results[category][target] not in exclude + ] -class BackupManager(): +class BackupManager: """ This class collect files to backup in a list and apply one or several @@ -241,7 +256,7 @@ class BackupManager(): backup_manager.backup() """ - def __init__(self, name=None, description='', methods=[], work_dir=None): + def __init__(self, name=None, description="", methods=[], work_dir=None): """ BackupManager constructor @@ -255,15 +270,12 @@ class BackupManager(): 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.description = description or "" self.created_at = int(time.time()) self.apps_return = {} self.system_return = {} self.paths_to_backup = [] - self.size_details = { - 'system': {}, - 'apps': {} - } + self.size_details = {"system": {}, "apps": {}} self.targets = BackupRestoreTargetsManager() # Define backup name if needed @@ -274,11 +286,13 @@ class BackupManager(): # 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.work_dir = os.path.join(BACKUP_PATH, "tmp", name) self._init_work_dir() # Initialize backup methods - self.methods = [BackupMethod.create(method, self, repo=work_dir) for method in methods] + self.methods = [ + BackupMethod.create(method, self, repo=work_dir) for method in methods + ] # # Misc helpers # @@ -288,20 +302,20 @@ class BackupManager(): 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, - 'from_yunohost_version': ynh_packages_version()["yunohost"]["version"] + "description": self.description, + "created_at": self.created_at, + "size": self.size, + "size_details": self.size_details, + "apps": self.apps_return, + "system": self.system_return, + "from_yunohost_version": ynh_packages_version()["yunohost"]["version"], } @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) + return self.work_dir == os.path.join(BACKUP_PATH, "tmp", self.name) def __repr__(self): return json.dumps(self.info) @@ -313,7 +327,7 @@ class BackupManager(): (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', time.gmtime()) + return time.strftime("%Y%m%d-%H%M%S", time.gmtime()) def _init_work_dir(self): """Initialize preparation directory @@ -324,21 +338,23 @@ class BackupManager(): # 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, 0o750, parents=True, uid='admin') + filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") elif self.is_tmp_work_dir: - logger.debug("temporary directory for backup '%s' already exists... attempting to clean it", - self.work_dir) + logger.debug( + "temporary directory for backup '%s' already exists... attempting to clean it", + self.work_dir, + ) # Try to recursively unmount stuff (from a previously failed backup ?) if not _recursive_umount(self.work_dir): - raise YunohostError('backup_output_directory_not_empty') + raise YunohostError("backup_output_directory_not_empty") else: # If umount succeeded, remove the directory (we checked that # we're in /home/yunohost.backup/tmp so that should be okay... # c.f. method clean() which also does this) filesystem.rm(self.work_dir, recursive=True, force=True) - filesystem.mkdir(self.work_dir, 0o750, parents=True, uid='admin') + filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin") # # Backup target management # @@ -353,12 +369,13 @@ class BackupManager(): 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 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=[]): """ @@ -369,12 +386,13 @@ class BackupManager(): 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) + 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 @@ -385,11 +403,11 @@ class BackupManager(): 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)) + 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)) + logger.warning(m18n.n("backup_with_no_restore_script_for_app", app=app)) self.targets.set_result("apps", app, "Warning") # @@ -434,7 +452,7 @@ class BackupManager(): 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}) + self.paths_to_backup.append({"source": source, "dest": dest}) def _write_csv(self): """ @@ -461,20 +479,21 @@ class BackupManager(): 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') + 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) + 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')) + 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')) + logger.error(m18n.n("backup_csv_addition_failed")) self.csv_file.close() # @@ -513,17 +532,17 @@ class BackupManager(): if not successfull_apps and not successfull_system: filesystem.rm(self.work_dir, True, True) - raise YunohostError('backup_nothings_done') + raise YunohostError("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') + 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') + 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() @@ -532,7 +551,7 @@ class BackupManager(): self._compute_backup_size() # Create backup info file - with open("%s/info.json" % self.work_dir, 'w') as f: + with open("%s/info.json" % self.work_dir, "w") as f: f.write(json.dumps(self.info)) def _get_env_var(self, app=None): @@ -549,13 +568,15 @@ class BackupManager(): """ env_var = {} - _, tmp_csv = tempfile.mkstemp(prefix='backupcsv_') - env_var['YNH_BACKUP_DIR'] = self.work_dir - env_var['YNH_BACKUP_CSV'] = tmp_csv + _, 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: env_var.update(_make_environment_for_app_script(app)) - env_var["YNH_APP_BACKUP_DIR"] = os.path.join(self.work_dir, 'apps', app, 'backup') + env_var["YNH_APP_BACKUP_DIR"] = os.path.join( + self.work_dir, "apps", app, "backup" + ) return env_var @@ -580,25 +601,35 @@ class BackupManager(): if system_targets == []: return - logger.debug(m18n.n('backup_running_hooks')) + logger.debug(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) + ret = hook_callback( + "backup", + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir, + ) - ret_succeed = {hook: [path for path, result in infos.items() if result["state"] == "succeed"] - for hook, infos in ret.items() - if any(result["state"] == "succeed" for result in infos.values())} - ret_failed = {hook: [path for path, result in infos.items() if result["state"] == "failed"] - for hook, infos in ret.items() - if any(result["state"] == "failed" for result in infos.values())} + ret_succeed = { + hook: [ + path for path, result in infos.items() if result["state"] == "succeed" + ] + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + } + ret_failed = { + hook: [ + path for path, result in infos.items() if result["state"] == "failed" + ] + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + } if list(ret_succeed.keys()) != []: self.system_return = ret_succeed @@ -612,8 +643,7 @@ class BackupManager(): 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=0o750, - parents=True, uid='admin') + filesystem.mkdir(restore_hooks_dir, mode=0o750, parents=True, uid="admin") restore_hooks = hook_list("restore")["hooks"] @@ -624,11 +654,11 @@ class BackupManager(): 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)) + 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)) + logger.error(m18n.n("backup_system_part_failed", part=part)) self.targets.set_result("system", part, "Error") def _collect_apps_files(self): @@ -666,52 +696,57 @@ class BackupManager(): """ from yunohost.permission import user_permission_list - app_setting_path = os.path.join('/etc/yunohost/apps/', app) + 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') + settings_dir = os.path.join(self.work_dir, "apps", app, "settings") logger.info(m18n.n("app_start_backup", app=app)) - tmp_script = None # This is to make sure the var exists later in the 'finally' ... + tmp_script = ( + None # This is to make sure the var exists later in the 'finally' ... + ) try: # Prepare backup directory for the app - filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid='admin') + filesystem.mkdir(tmp_app_bkp_dir, 0o750, 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]) + _, 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, - raise_on_error=True, - chdir=tmp_app_bkp_dir, - env=env_dict)[0] + hook_exec( + tmp_script, raise_on_error=True, chdir=tmp_app_bkp_dir, env=env_dict + )[0] self._import_to_list_to_backup(env_dict["YNH_BACKUP_CSV"]) # backup permissions - logger.debug(m18n.n('backup_permission', app=app)) + logger.debug(m18n.n("backup_permission", app=app)) permissions = user_permission_list(full=True)["permissions"] - this_app_permissions = {name: infos for name, infos in permissions.items() if name.startswith(app + ".")} + this_app_permissions = { + name: infos + for name, infos in permissions.items() + if name.startswith(app + ".") + } write_to_yaml("%s/permissions.yml" % settings_dir, this_app_permissions) except: - abs_tmp_app_dir = os.path.join(self.work_dir, 'apps/', app) + abs_tmp_app_dir = os.path.join(self.work_dir, "apps/", app) shutil.rmtree(abs_tmp_app_dir, ignore_errors=True) - logger.error(m18n.n('backup_app_failed', app=app)) + logger.error(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'], + "version": i["version"], + "name": i["name"], + "description": i["description"], } self.targets.set_result("apps", app, "Success") @@ -729,9 +764,9 @@ class BackupManager(): """Apply backup methods""" for method in self.methods: - logger.debug(m18n.n('backup_applying_method_' + method.method_name)) + logger.debug(m18n.n("backup_applying_method_" + method.method_name)) method.mount_and_backup() - logger.debug(m18n.n('backup_method_' + method.method_name + '_finished')) + logger.debug(m18n.n("backup_method_" + method.method_name + "_finished")) def _compute_backup_size(self): """ @@ -754,27 +789,27 @@ class BackupManager(): # size info self.size = 0 for system_key in self.system_return: - self.size_details['system'][system_key] = 0 + self.size_details["system"][system_key] = 0 for app_key in self.apps_return: - self.size_details['apps'][app_key] = 0 + self.size_details["apps"][app_key] = 0 for row in self.paths_to_backup: - if row['dest'] != "info.json": - size = disk_usage(row['source']) + if row["dest"] != "info.json": + size = disk_usage(row["source"]) # Add size to apps details - splitted_dest = row['dest'].split('/') + splitted_dest = row["dest"].split("/") category = splitted_dest[0] - if category == 'apps': + if category == "apps": for app_key in self.apps_return: - if row['dest'].startswith('apps/' + app_key): - self.size_details['apps'][app_key] += size + 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': + 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 + if row["dest"].startswith(system_key.replace("_", "/")): + self.size_details["system"][system_key] += size break self.size += size @@ -782,7 +817,7 @@ class BackupManager(): return self.size -class RestoreManager(): +class RestoreManager: """ RestoreManager allow to restore a past backup archive @@ -813,7 +848,7 @@ class RestoreManager(): return restore_manager.result """ - def __init__(self, name, method='tar'): + def __init__(self, name, method="tar"): """ RestoreManager constructor @@ -825,7 +860,7 @@ class RestoreManager(): # 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.archive_path = self.info["path"] self.name = name self.method = BackupMethod.create(method, self) self.targets = BackupRestoreTargetsManager() @@ -840,8 +875,7 @@ class RestoreManager(): 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 + return len(successful_apps) != 0 or len(successful_system) != 0 def _read_info_files(self): """ @@ -850,7 +884,7 @@ class RestoreManager(): # Retrieve backup info info_file = os.path.join(self.work_dir, "info.json") try: - with open(info_file, 'r') as f: + with open(info_file, "r") as f: self.info = json.load(f) # Historically, "system" was "hooks" @@ -858,29 +892,38 @@ class RestoreManager(): self.info["system"] = self.info["hooks"] except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) - raise YunohostError('backup_archive_cant_retrieve_info_json', archive=self.archive_path) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=self.archive_path + ) else: - logger.debug("restoring from backup '%s' created on %s", self.name, - datetime.utcfromtimestamp(self.info['created_at'])) + logger.debug( + "restoring from backup '%s' created on %s", + self.name, + datetime.utcfromtimestamp(self.info["created_at"]), + ) def _postinstall_if_needed(self): """ Post install yunohost if needed """ # Check if YunoHost is installed - if not os.path.isfile('/etc/yunohost/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: + 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) + logger.debug( + "unable to retrieve current_host from the backup", exc_info=1 + ) # FIXME include the current_host by default ? - raise YunohostError("The main domain name cannot be retrieved from inside the archive, and is needed to perform the postinstall", raw_msg=True) + raise YunohostError( + "The main domain name cannot be retrieved from inside the archive, and is needed to perform the postinstall", + raw_msg=True, + ) logger.debug("executing the post-install...") - tools_postinstall(domain, 'Yunohost', True) + tools_postinstall(domain, "Yunohost", True) def clean(self): """ @@ -894,7 +937,7 @@ class RestoreManager(): if os.path.ismount(self.work_dir): ret = subprocess.call(["umount", self.work_dir]) if ret != 0: - logger.warning(m18n.n('restore_cleaning_failed')) + logger.warning(m18n.n("restore_cleaning_failed")) filesystem.rm(self.work_dir, recursive=True, force=True) # @@ -912,13 +955,11 @@ class RestoreManager(): """ def unknown_error(part): - logger.error(m18n.n("backup_archive_system_part_not_available", - part=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) + 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 @@ -926,7 +967,7 @@ class RestoreManager(): # These are the hooks on the current installation available_restore_system_hooks = hook_list("restore")["hooks"] - custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, 'restore') + custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") filesystem.mkdir(custom_restore_hook_folder, 755, parents=True, force=True) for system_part in target_list: @@ -942,22 +983,27 @@ class RestoreManager(): # Otherwise, attempt to find it (or them?) in the archive # If we didn't find it, we ain't gonna be able to restore it - if system_part not in self.info['system'] or\ - 'paths' not in self.info['system'][system_part] or\ - len(self.info['system'][system_part]['paths']) == 0: - logger.error(m18n.n('restore_hook_unavailable', part=system_part)) + if ( + system_part not in self.info["system"] + or "paths" not in self.info["system"][system_part] + or len(self.info["system"][system_part]["paths"]) == 0 + ): + logger.error(m18n.n("restore_hook_unavailable", part=system_part)) self.targets.set_result("system", system_part, "Skipped") continue - hook_paths = self.info['system'][system_part]['paths'] - hook_paths = ['hooks/restore/%s' % os.path.basename(p) for p in hook_paths] + hook_paths = self.info["system"][system_part]["paths"] + hook_paths = ["hooks/restore/%s" % os.path.basename(p) for p in hook_paths] # Otherwise, add it from the archive to the system # FIXME: Refactor hook_add and use it instead 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) + logger.debug( + "Adding restoration script '%s' to the system " + "from the backup archive '%s'", + hook_path, + self.archive_path, + ) self.method.copy(hook_path, custom_restore_hook_folder) def set_apps_targets(self, apps=[]): @@ -971,13 +1017,11 @@ class RestoreManager(): """ def unknown_error(app): - logger.error(m18n.n('backup_archive_app_not_found', - app=app)) + logger.error(m18n.n("backup_archive_app_not_found", app=app)) - to_be_restored = self.targets.set_wanted("apps", - apps, - self.info['apps'].keys(), - unknown_error) + to_be_restored = self.targets.set_wanted( + "apps", apps, self.info["apps"].keys(), unknown_error + ) # If all apps to restore are already installed, stop right here. # Otherwise, if at least one app can be restored, we keep going on @@ -985,9 +1029,16 @@ class RestoreManager(): already_installed = [app for app in to_be_restored if _is_installed(app)] if already_installed != []: if already_installed == to_be_restored: - raise YunohostError("restore_already_installed_apps", apps=', '.join(already_installed)) + raise YunohostError( + "restore_already_installed_apps", apps=", ".join(already_installed) + ) else: - logger.warning(m18n.n("restore_already_installed_apps", apps=', '.join(already_installed))) + logger.warning( + m18n.n( + "restore_already_installed_apps", + apps=", ".join(already_installed), + ) + ) # # Archive mounting # @@ -1005,22 +1056,22 @@ class RestoreManager(): 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]) + 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]) + subprocess.call(["rmdir", self.work_dir]) logger.debug("Unmount dir: {}".format(self.work_dir)) else: - raise YunohostError('restore_removing_tmp_dir_failed') + raise YunohostError("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]) + 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 YunohostError('restore_removing_tmp_dir_failed') + raise YunohostError("restore_removing_tmp_dir_failed") filesystem.mkdir(self.work_dir, parents=True) @@ -1043,30 +1094,32 @@ class RestoreManager(): """ 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()) + 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'] != {}: + 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] + 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] + size += self.info["size_details"]["apps"][app] margin = APP_MARGIN_SPACE_SIZE * 1024 * 1024 - if not os.path.isfile('/etc/yunohost/installed'): + if not os.path.isfile("/etc/yunohost/installed"): size += POSTINSTALL_ESTIMATE_SPACE_SIZE * 1024 * 1024 return (size, margin) @@ -1082,9 +1135,19 @@ class RestoreManager(): return True elif free_space > needed_space: # TODO Add --force options to avoid the error raising - raise YunohostError('restore_may_be_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin) + raise YunohostError( + "restore_may_be_not_enough_disk_space", + free_space=free_space, + needed_space=needed_space, + margin=margin, + ) else: - raise YunohostError('restore_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin) + raise YunohostError( + "restore_not_enough_disk_space", + free_space=free_space, + needed_space=needed_space, + margin=margin, + ) # # "Actual restore" (reverse step of the backup collect part) # @@ -1107,7 +1170,9 @@ class RestoreManager(): self._restore_system() self._restore_apps() except Exception as e: - raise YunohostError("The following critical error happened during restoration: %s" % e) + raise YunohostError( + "The following critical error happened during restoration: %s" % e + ) finally: self.clean() @@ -1116,30 +1181,30 @@ class RestoreManager(): Apply dirty patch to redirect php5 and php7.0 files to php7.3 """ - backup_csv = os.path.join(self.work_dir, 'backup.csv') + backup_csv = os.path.join(self.work_dir, "backup.csv") if not os.path.isfile(backup_csv): return replaced_something = False with open(backup_csv) as csvfile: - reader = csv.DictReader(csvfile, fieldnames=['source', 'dest']) + reader = csv.DictReader(csvfile, fieldnames=["source", "dest"]) newlines = [] for row in reader: for pattern, replace in LEGACY_PHP_VERSION_REPLACEMENTS: - if pattern in row['source']: + if pattern in row["source"]: replaced_something = True - row['source'] = row['source'].replace(pattern, replace) + row["source"] = row["source"].replace(pattern, replace) newlines.append(row) if not replaced_something: return - with open(backup_csv, 'w') as csvfile: - writer = csv.DictWriter(csvfile, - fieldnames=['source', 'dest'], - quoting=csv.QUOTE_ALL) + 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) @@ -1153,46 +1218,63 @@ class RestoreManager(): return from yunohost.user import user_group_list - from yunohost.permission import permission_create, permission_delete, user_permission_list, permission_sync_to_user + from yunohost.permission import ( + permission_create, + permission_delete, + user_permission_list, + permission_sync_to_user, + ) # Backup old permission for apps # We need to do that because in case of an app is installed we can't remove the permission for this app - old_apps_permission = user_permission_list(ignore_system_perms=True, full=True)["permissions"] + old_apps_permission = user_permission_list(ignore_system_perms=True, full=True)[ + "permissions" + ] # Start register change on system - operation_logger = OperationLogger('backup_restore_system') + operation_logger = OperationLogger("backup_restore_system") operation_logger.start() - logger.debug(m18n.n('restore_running_hooks')) + logger.debug(m18n.n("restore_running_hooks")) env_dict = { - 'YNH_BACKUP_DIR': self.work_dir, - 'YNH_BACKUP_CSV': os.path.join(self.work_dir, "backup.csv") + "YNH_BACKUP_DIR": self.work_dir, + "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"), } - operation_logger.extra['env'] = env_dict + operation_logger.extra["env"] = env_dict operation_logger.flush() - ret = hook_callback('restore', - system_targets, - args=[self.work_dir], - env=env_dict, - chdir=self.work_dir) + ret = hook_callback( + "restore", + system_targets, + args=[self.work_dir], + env=env_dict, + chdir=self.work_dir, + ) - ret_succeed = [hook for hook, infos in ret.items() - if any(result["state"] == "succeed" for result in infos.values())] - ret_failed = [hook for hook, infos in ret.items() - if any(result["state"] == "failed" for result in infos.values())] + ret_succeed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] for part in ret_succeed: self.targets.set_result("system", part, "Success") error_part = [] for part in ret_failed: - logger.error(m18n.n('restore_system_part_failed', part=part)) + logger.error(m18n.n("restore_system_part_failed", part=part)) self.targets.set_result("system", part, "Error") error_part.append(part) if ret_failed: - operation_logger.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: operation_logger.success() @@ -1204,26 +1286,35 @@ class RestoreManager(): # Legacy code if "all_users" not in user_group_list()["groups"].keys(): from yunohost.utils.legacy import SetupGroupPermissions + # Update LDAP schema restart slapd logger.info(m18n.n("migration_0011_update_LDAP_schema")) - regen_conf(names=['slapd'], force=True) + regen_conf(names=["slapd"], force=True) SetupGroupPermissions.migrate_LDAP_db() # Remove all permission for all app which is still in the LDAP - for permission_name in user_permission_list(ignore_system_perms=True)["permissions"].keys(): + for permission_name in user_permission_list(ignore_system_perms=True)[ + "permissions" + ].keys(): permission_delete(permission_name, force=True, sync_perm=False) # Restore permission for the app which is installed for permission_name, permission_infos in old_apps_permission.items(): app_name, perm_name = permission_name.split(".") if _is_installed(app_name): - permission_create(permission_name, allowed=permission_infos["allowed"], - url=permission_infos["url"], - additional_urls=permission_infos['additional_urls'], - auth_header=permission_infos['auth_header'], - label=permission_infos['label'] if perm_name == "main" else permission_infos["sublabel"], - show_tile=permission_infos['show_tile'], - protected=permission_infos["protected"], sync_perm=False) + permission_create( + permission_name, + allowed=permission_infos["allowed"], + url=permission_infos["url"], + additional_urls=permission_infos["additional_urls"], + auth_header=permission_infos["auth_header"], + label=permission_infos["label"] + if perm_name == "main" + else permission_infos["sublabel"], + show_tile=permission_infos["show_tile"], + protected=permission_infos["protected"], + sync_perm=False, + ) permission_sync_to_user() @@ -1259,7 +1350,12 @@ class RestoreManager(): """ from yunohost.user import user_group_list from yunohost.app import app_setting - from yunohost.permission import permission_create, permission_delete, user_permission_list, permission_sync_to_user + from yunohost.permission import ( + permission_create, + permission_delete, + user_permission_list, + permission_sync_to_user, + ) def copytree(src, dst, symlinks=False, ignore=None): for item in os.listdir(src): @@ -1272,22 +1368,21 @@ class RestoreManager(): # 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)) + logger.error(m18n.n("restore_already_installed_app", app=app_instance_name)) self.targets.set_result("apps", app_instance_name, "Error") return # Start register change on system - related_to = [('app', app_instance_name)] - operation_logger = OperationLogger('backup_restore_app', related_to) + related_to = [("app", app_instance_name)] + operation_logger = OperationLogger("backup_restore_app", related_to) operation_logger.start() logger.info(m18n.n("app_start_restore", app=app_instance_name)) - 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') + 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") # Attempt to patch legacy helpers... _patch_legacy_helpers(app_settings_in_archive) @@ -1297,68 +1392,80 @@ class RestoreManager(): _patch_legacy_php_versions_in_settings(app_settings_in_archive) # Delete _common.sh file in backup - common_file = os.path.join(app_backup_in_archive, '_common.sh') + 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') + 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)) + logger.warning(m18n.n("unrestore_app", app=app_instance_name)) self.targets.set_result("apps", app_instance_name, "Warning") return - logger.debug(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/', - app_instance_name) - app_scripts_new_path = os.path.join(app_settings_new_path, 'scripts') + 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, 0o400, 0o400, True) - filesystem.chown(app_scripts_new_path, 'admin', None, 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') + 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, 0o550, 0o550, True) - filesystem.chown(tmp_folder_for_app_restore, 'admin', None, True) - restore_script = os.path.join(tmp_folder_for_app_restore, 'restore') + filesystem.chown(tmp_folder_for_app_restore, "admin", None, True) + restore_script = os.path.join(tmp_folder_for_app_restore, "restore") # Restore permissions - if os.path.isfile('%s/permissions.yml' % app_settings_new_path): + if os.path.isfile("%s/permissions.yml" % app_settings_new_path): - permissions = read_yaml('%s/permissions.yml' % app_settings_new_path) - existing_groups = user_group_list()['groups'] + permissions = read_yaml("%s/permissions.yml" % app_settings_new_path) + existing_groups = user_group_list()["groups"] for permission_name, permission_infos in permissions.items(): if "allowed" not in permission_infos: - logger.warning("'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." % (permission_name, app_instance_name)) + logger.warning( + "'allowed' key corresponding to allowed groups for permission %s not found when restoring app %s … You might have to reconfigure permissions yourself." + % (permission_name, app_instance_name) + ) should_be_allowed = ["all_users"] else: - should_be_allowed = [g for g in permission_infos["allowed"] if g in existing_groups] + should_be_allowed = [ + g + for g in permission_infos["allowed"] + if g in existing_groups + ] perm_name = permission_name.split(".")[1] - permission_create(permission_name, - allowed=should_be_allowed, - url=permission_infos.get("url"), - additional_urls=permission_infos.get("additional_urls"), - auth_header=permission_infos.get("auth_header"), - label=permission_infos.get('label') if perm_name == "main" else permission_infos.get("sublabel"), - show_tile=permission_infos.get("show_tile", True), - protected=permission_infos.get("protected", False), - sync_perm=False) + permission_create( + permission_name, + allowed=should_be_allowed, + url=permission_infos.get("url"), + additional_urls=permission_infos.get("additional_urls"), + auth_header=permission_infos.get("auth_header"), + label=permission_infos.get("label") + if perm_name == "main" + else permission_infos.get("sublabel"), + show_tile=permission_infos.get("show_tile", True), + protected=permission_infos.get("protected", False), + sync_perm=False, + ) permission_sync_to_user() - os.remove('%s/permissions.yml' % app_settings_new_path) + os.remove("%s/permissions.yml" % app_settings_new_path) else: # Otherwise, we need to migrate the legacy permissions of this # app (included in its settings.yml) from yunohost.utils.legacy import SetupGroupPermissions + SetupGroupPermissions.migrate_app_permission(app=app_instance_name) # Migrate old settings @@ -1368,52 +1475,63 @@ class RestoreManager(): "protected_uris", "skipped_regex", "unprotected_regex", - "protected_regex" + "protected_regex", ] - if any(app_setting(app_instance_name, setting) is not None for setting in legacy_permission_settings): + if any( + app_setting(app_instance_name, setting) is not None + for setting in legacy_permission_settings + ): from yunohost.utils.legacy import migrate_legacy_permission_settings + migrate_legacy_permission_settings(app=app_instance_name) # Prepare env. var. to pass to script env_dict = _make_environment_for_app_script(app_instance_name) - env_dict.update({ - 'YNH_BACKUP_DIR': self.work_dir, - 'YNH_BACKUP_CSV': os.path.join(self.work_dir, "backup.csv"), - 'YNH_APP_BACKUP_DIR': os.path.join(self.work_dir, 'apps', app_instance_name, 'backup') - }) + env_dict.update( + { + "YNH_BACKUP_DIR": self.work_dir, + "YNH_BACKUP_CSV": os.path.join(self.work_dir, "backup.csv"), + "YNH_APP_BACKUP_DIR": os.path.join( + self.work_dir, "apps", app_instance_name, "backup" + ), + } + ) - operation_logger.extra['env'] = env_dict + operation_logger.extra["env"] = env_dict operation_logger.flush() # Execute app restore script - hook_exec(restore_script, - chdir=app_backup_in_archive, - raise_on_error=True, - env=env_dict)[0] + hook_exec( + restore_script, + chdir=app_backup_in_archive, + raise_on_error=True, + env=env_dict, + )[0] except: - msg = m18n.n('restore_app_failed', app=app_instance_name) + msg = m18n.n("restore_app_failed", app=app_instance_name) logger.error(msg) operation_logger.error(msg) - if msettings.get('interface') != 'api': + if msettings.get("interface") != "api": dump_app_log_extract_for_debugging(operation_logger) self.targets.set_result("apps", app_instance_name, "Error") - remove_script = os.path.join(app_scripts_in_archive, 'remove') + remove_script = os.path.join(app_scripts_in_archive, "remove") # Setup environment for remove script env_dict_remove = _make_environment_for_app_script(app_instance_name) - operation_logger = OperationLogger('remove_on_failed_restore', - [('app', app_instance_name)], - env=env_dict_remove) + operation_logger = OperationLogger( + "remove_on_failed_restore", + [("app", app_instance_name)], + env=env_dict_remove, + ) operation_logger.start() # Execute remove script - if hook_exec(remove_script, - env=env_dict_remove)[0] != 0: - msg = m18n.n('app_not_properly_removed', app=app_instance_name) + if hook_exec(remove_script, env=env_dict_remove)[0] != 0: + msg = m18n.n("app_not_properly_removed", app=app_instance_name) logger.warning(msg) operation_logger.error(msg) else: @@ -1435,6 +1553,7 @@ class RestoreManager(): # Cleaning temporary scripts directory shutil.rmtree(tmp_folder_for_app_restore, ignore_errors=True) + # # Backup methods # # @@ -1522,7 +1641,7 @@ class BackupMethod(object): @property def method_name(self): """Return the string name of a BackupMethod (eg "tar" or "copy")""" - raise YunohostError('backup_abstract_method') + raise YunohostError("backup_abstract_method") @property def name(self): @@ -1591,7 +1710,7 @@ class BackupMethod(object): """ if self.need_mount(): if not _recursive_umount(self.work_dir): - raise YunohostError('backup_cleaning_failed') + raise YunohostError("backup_cleaning_failed") if self.manager.is_tmp_work_dir: filesystem.rm(self.work_dir, True, True) @@ -1606,9 +1725,13 @@ class BackupMethod(object): 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 YunohostError('not_enough_disk_space', path=self.repo) + logger.debug( + "Not enough space at %s (free: %s / needed: %d)", + self.repo, + free_space, + backup_size, + ) + raise YunohostError("not_enough_disk_space", path=self.repo) def _organize_files(self): """ @@ -1623,7 +1746,7 @@ class BackupMethod(object): """ paths_needed_to_be_copied = [] for path in self.manager.paths_to_backup: - src = path['source'] + src = path["source"] if self.manager is RestoreManager: # TODO Support to run this before a restore (and not only before @@ -1631,7 +1754,7 @@ class BackupMethod(object): # be implemented src = os.path.join(self.unorganized_work_dir, src) - dest = os.path.join(self.work_dir, path['dest']) + dest = os.path.join(self.work_dir, path["dest"]) if dest == src: continue dest_dir = os.path.dirname(dest) @@ -1651,7 +1774,7 @@ class BackupMethod(object): 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') + 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: @@ -1667,7 +1790,7 @@ class BackupMethod(object): if os.stat(src).st_dev == os.stat(dest_dir).st_dev: # 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') + '.' + cron_path = os.path.abspath("/etc/cron") + "." if not os.path.abspath(src).startswith(cron_path): try: os.link(src, dest) @@ -1677,7 +1800,10 @@ class BackupMethod(object): # E.g. this happens when running an encrypted hard drive # where everything is mapped to /dev/mapper/some-stuff # yet there are different devices behind it or idk ... - logger.warning("Could not link %s to %s (%s) ... falling back to regular copy." % (src, dest, str(e))) + logger.warning( + "Could not link %s to %s (%s) ... falling back to regular copy." + % (src, dest, str(e)) + ) else: # Success, go to next file to organize continue @@ -1693,29 +1819,33 @@ class BackupMethod(object): # to mounting error # 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 = sum(disk_usage(path["source"]) 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=str(size))) + i = msignals.prompt( + m18n.n( + "backup_ask_for_copying_if_needed", + answers="y/N", + size=str(size), + ) + ) except NotImplemented: - raise YunohostError('backup_unable_to_organize_files') + raise YunohostError("backup_unable_to_organize_files") else: - if i != 'y' and i != 'Y': - raise YunohostError('backup_unable_to_organize_files') + if i != "y" and i != "Y": + raise YunohostError("backup_unable_to_organize_files") # Copy unbinded path - logger.debug(m18n.n('backup_copying_to_organize_the_archive', - size=str(size))) + 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']) - if os.path.isdir(path['source']): - shutil.copytree(path['source'], 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(path['source'], dest) + shutil.copy(path["source"], dest) class CopyBackupMethod(BackupMethod): @@ -1733,15 +1863,15 @@ class CopyBackupMethod(BackupMethod): 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']) + 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, 0o750, True, uid='admin') + filesystem.mkdir(dest_parent, 0o750, True, uid="admin") if os.path.isdir(source): shutil.copytree(source, dest) @@ -1757,19 +1887,21 @@ class CopyBackupMethod(BackupMethod): super(CopyBackupMethod, self).mount() if not os.path.isdir(self.repo): - raise YunohostError('backup_no_uncompress_archive_dir') + raise YunohostError("backup_no_uncompress_archive_dir") filesystem.mkdir(self.work_dir, parent=True) - ret = subprocess.call(["mount", "-r", "--rbind", self.repo, - self.work_dir]) + ret = subprocess.call(["mount", "-r", "--rbind", self.repo, self.work_dir]) if ret == 0: return - logger.warning("Could not mount the backup in readonly mode with --rbind ... Unmounting") + logger.warning( + "Could not mount the backup in readonly mode with --rbind ... Unmounting" + ) # FIXME : Does this stuff really works ? '&&' is going to be interpreted as an argument for mounpoint here ... Not as a classical '&&' ... - subprocess.call(["mountpoint", "-q", self.work_dir, - "&&", "umount", "-R", self.work_dir]) - raise YunohostError('backup_cant_mount_uncompress_archive') + subprocess.call( + ["mountpoint", "-q", self.work_dir, "&&", "umount", "-R", self.work_dir] + ) + raise YunohostError("backup_cant_mount_uncompress_archive") def copy(self, file, target): shutil.copy(file, target) @@ -1782,10 +1914,12 @@ class TarBackupMethod(BackupMethod): @property def _archive_file(self): - if isinstance(self.manager, BackupManager) and settings_get("backup.compress_tar_archives"): - return os.path.join(self.repo, self.name + '.tar.gz') + if isinstance(self.manager, BackupManager) and settings_get( + "backup.compress_tar_archives" + ): + return os.path.join(self.repo, self.name + ".tar.gz") - f = os.path.join(self.repo, self.name + '.tar') + f = os.path.join(self.repo, self.name + ".tar") if os.path.exists(f + ".gz"): f += ".gz" return f @@ -1799,38 +1933,52 @@ class TarBackupMethod(BackupMethod): """ if not os.path.exists(self.repo): - filesystem.mkdir(self.repo, 0o750, parents=True, uid='admin') + filesystem.mkdir(self.repo, 0o750, 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" if self._archive_file.endswith(".gz") else "w") + tar = tarfile.open( + self._archive_file, + "w:gz" if self._archive_file.endswith(".gz") else "w", + ) except: - logger.debug("unable to open '%s' for writing", - self._archive_file, exc_info=1) - raise YunohostError('backup_archive_open_failed') + logger.debug( + "unable to open '%s' for writing", self._archive_file, exc_info=1 + ) + raise YunohostError("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.add(path["source"], arcname=path["dest"]) except IOError: - logger.error(m18n.n('backup_archive_writing_error', source=path['source'], archive=self._archive_file, dest=path['dest']), exc_info=1) - raise YunohostError('backup_creation_failed') + logger.error( + m18n.n( + "backup_archive_writing_error", + source=path["source"], + archive=self._archive_file, + dest=path["dest"], + ), + exc_info=1, + ) + raise YunohostError("backup_creation_failed") finally: tar.close() # Move info file - shutil.copy(os.path.join(self.work_dir, 'info.json'), - os.path.join(ARCHIVES_PATH, self.name + '.info.json')) + 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') + link = os.path.join(ARCHIVES_PATH, self.name + ".tar") if not os.path.isfile(link): os.symlink(self._archive_file, link) @@ -1843,33 +1991,42 @@ class TarBackupMethod(BackupMethod): # Mount the tarball logger.debug(m18n.n("restore_extracting")) try: - tar = tarfile.open(self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r") + tar = tarfile.open( + self._archive_file, + "r:gz" if self._archive_file.endswith(".gz") else "r", + ) except: - logger.debug("cannot open backup archive '%s'", - self._archive_file, exc_info=1) - raise YunohostError('backup_archive_open_failed') + logger.debug( + "cannot open backup archive '%s'", self._archive_file, exc_info=1 + ) + raise YunohostError("backup_archive_open_failed") try: files_in_archive = tar.getnames() except IOError as e: - raise YunohostError("backup_archive_corrupted", archive=self._archive_file, error=str(e)) + raise YunohostError( + "backup_archive_corrupted", archive=self._archive_file, error=str(e) + ) if "info.json" in tar.getnames(): leading_dot = "" - tar.extract('info.json', path=self.work_dir) + tar.extract("info.json", path=self.work_dir) elif "./info.json" in files_in_archive: leading_dot = "./" - tar.extract('./info.json', path=self.work_dir) + tar.extract("./info.json", path=self.work_dir) else: - logger.debug("unable to retrieve 'info.json' inside the archive", - exc_info=1) + logger.debug( + "unable to retrieve 'info.json' inside the archive", exc_info=1 + ) tar.close() - raise YunohostError('backup_archive_cant_retrieve_info_json', archive=self._archive_file) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=self._archive_file + ) if "backup.csv" in files_in_archive: - tar.extract('backup.csv', path=self.work_dir) + tar.extract("backup.csv", path=self.work_dir) elif "./backup.csv" in files_in_archive: - tar.extract('./backup.csv', path=self.work_dir) + tar.extract("./backup.csv", path=self.work_dir) else: # Old backup archive have no backup.csv file pass @@ -1891,12 +2048,14 @@ class TarBackupMethod(BackupMethod): else: system_part = system_part.replace("_", "/") + "/" subdir_and_files = [ - tarinfo for tarinfo in tar.getmembers() + tarinfo + for tarinfo in tar.getmembers() if tarinfo.name.startswith(leading_dot + system_part) ] tar.extractall(members=subdir_and_files, path=self.work_dir) subdir_and_files = [ - tarinfo for tarinfo in tar.getmembers() + tarinfo + for tarinfo in tar.getmembers() if tarinfo.name.startswith(leading_dot + "hooks/restore/") ] tar.extractall(members=subdir_and_files, path=self.work_dir) @@ -1904,7 +2063,8 @@ class TarBackupMethod(BackupMethod): # Extract apps backup for app in apps_targets: subdir_and_files = [ - tarinfo for tarinfo in tar.getmembers() + tarinfo + for tarinfo in tar.getmembers() if tarinfo.name.startswith(leading_dot + "apps/" + app) ] tar.extractall(members=subdir_and_files, path=self.work_dir) @@ -1912,7 +2072,9 @@ class TarBackupMethod(BackupMethod): tar.close() def copy(self, file, target): - tar = tarfile.open(self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r") + tar = tarfile.open( + self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r" + ) file_to_extract = tar.getmember(file) # Remove the path file_to_extract.name = os.path.basename(file_to_extract.name) @@ -1937,15 +2099,18 @@ class CustomBackupMethod(BackupMethod): self._need_mount = None def need_mount(self): - """Call the backup_method hook to know if we need to organize files - """ + """Call the backup_method hook to know if we need to organize files""" if self._need_mount is not None: return self._need_mount - ret = hook_callback('backup_method', [self.method], - args=self._get_args('need_mount')) - ret_succeed = [hook for hook, infos in ret.items() - if any(result["state"] == "succeed" for result in infos.values())] + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("need_mount") + ) + ret_succeed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] self._need_mount = True if ret_succeed else False return self._need_mount @@ -1954,40 +2119,55 @@ class CustomBackupMethod(BackupMethod): Launch a custom script to backup """ - ret = hook_callback('backup_method', [self.method], - args=self._get_args('backup')) + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("backup") + ) - ret_failed = [hook for hook, infos in ret.items() - if any(result["state"] == "failed" for result in infos.values())] + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] if ret_failed: - raise YunohostError('backup_custom_backup_error') + raise YunohostError("backup_custom_backup_error") def mount(self): """ Launch a custom script to mount the custom archive """ super(CustomBackupMethod, self).mount() - ret = hook_callback('backup_method', [self.method], - args=self._get_args('mount')) + ret = hook_callback( + "backup_method", [self.method], args=self._get_args("mount") + ) - ret_failed = [hook for hook, infos in ret.items() - if any(result["state"] == "failed" for result in infos.values())] + ret_failed = [ + hook + for hook, infos in ret.items() + if any(result["state"] == "failed" for result in infos.values()) + ] if ret_failed: - raise YunohostError('backup_custom_mount_error') + raise YunohostError("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] + 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, - system=[], apps=[]): + +def backup_create( + name=None, description=None, methods=[], output_directory=None, system=[], apps=[] +): """ Create a backup local archive @@ -2007,29 +2187,30 @@ def backup_create(name=None, description=None, methods=[], # # Validate there is no archive with the same name - if name and name in backup_list()['archives']: - raise YunohostError('backup_archive_name_exists') + if name and name in backup_list()["archives"]: + raise YunohostError("backup_archive_name_exists") # By default we backup using the tar method if not methods: - methods = ['tar'] + methods = ["tar"] # 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): - raise YunohostError('backup_output_directory_forbidden') + if output_directory.startswith(ARCHIVES_PATH) or re.match( + r"^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$", + output_directory, + ): + raise YunohostError("backup_output_directory_forbidden") if "copy" in methods: if not output_directory: - raise YunohostError('backup_output_directory_required') + raise YunohostError("backup_output_directory_required") # Check that output directory is empty elif os.path.isdir(output_directory) and os.listdir(output_directory): - raise YunohostError('backup_output_directory_not_empty') + raise YunohostError("backup_output_directory_not_empty") # If no --system or --apps given, backup everything if system is None and apps is None: @@ -2045,7 +2226,9 @@ def backup_create(name=None, description=None, methods=[], # Initialize backup manager - backup_manager = BackupManager(name, description, methods=methods, work_dir=output_directory) + backup_manager = BackupManager( + name, description, methods=methods, work_dir=output_directory + ) # Add backup targets (system and apps) @@ -2063,12 +2246,12 @@ def backup_create(name=None, description=None, methods=[], logger.info(m18n.n("backup_actually_backuping")) backup_manager.backup() - logger.success(m18n.n('backup_created')) + logger.success(m18n.n("backup_created")) return { - 'name': backup_manager.name, - 'size': backup_manager.size, - 'results': backup_manager.targets.results + "name": backup_manager.name, + "size": backup_manager.size, + "results": backup_manager.targets.results, } @@ -2107,20 +2290,23 @@ def backup_restore(name, system=[], apps=[], force=False): # Add validation if restoring system parts on an already-installed system # - if restore_manager.targets.targets["system"] != [] and os.path.isfile('/etc/yunohost/installed'): - logger.warning(m18n.n('yunohost_already_installed')) + if restore_manager.targets.targets["system"] != [] and os.path.isfile( + "/etc/yunohost/installed" + ): + logger.warning(m18n.n("yunohost_already_installed")) if not force: try: # Ask confirmation for restoring - i = msignals.prompt(m18n.n('restore_confirm_yunohost_installed', - answers='y/N')) + i = msignals.prompt( + m18n.n("restore_confirm_yunohost_installed", answers="y/N") + ) except NotImplemented: pass else: - if i == 'y' or i == 'Y': + if i == "y" or i == "Y": force = True if not force: - raise YunohostError('restore_failed') + raise YunohostError("restore_failed") # # Mount the archive then call the restore for each system part / app # @@ -2132,9 +2318,9 @@ def backup_restore(name, system=[], apps=[], force=False): # Check if something has been restored if restore_manager.success: - logger.success(m18n.n('restore_complete')) + logger.success(m18n.n("restore_complete")) else: - raise YunohostError('restore_nothings_done') + raise YunohostError("restore_nothings_done") return restore_manager.targets.results @@ -2157,9 +2343,10 @@ def backup_list(with_info=False, human_readable=False): def remove_extension(f): if f.endswith(".tar.gz"): - return os.path.basename(f)[:-len(".tar.gz")] + return os.path.basename(f)[: -len(".tar.gz")] else: - return os.path.basename(f)[:-len(".tar")] + return os.path.basename(f)[: -len(".tar")] + archives = [remove_extension(f) for f in archives] if with_info: @@ -2171,26 +2358,32 @@ def backup_list(with_info=False, human_readable=False): logger.warning(str(e)) except Exception: import traceback - logger.warning("Could not check infos for archive %s: %s" % (archive, '\n' + traceback.format_exc())) + + logger.warning( + "Could not check infos for archive %s: %s" + % (archive, "\n" + traceback.format_exc()) + ) archives = d - return {'archives': archives} + return {"archives": archives} def backup_download(name): - if msettings.get('interface') != 'api': - logger.error("This option is only meant for the API/webadmin and doesn't make sense for the command line.") + if msettings.get("interface") != "api": + logger.error( + "This option is only meant for the API/webadmin and doesn't make sense for the command line." + ) return - archive_file = '%s/%s.tar' % (ARCHIVES_PATH, name) + archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): archive_file += ".gz" if not os.path.lexists(archive_file): - raise YunohostError('backup_archive_name_unknown', name=name) + raise YunohostError("backup_archive_name_unknown", name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2198,12 +2391,12 @@ def backup_download(name): # Raise exception if link is broken (e.g. on unmounted external storage) if not os.path.exists(archive_file): - raise YunohostError('backup_archive_broken_link', - path=archive_file) + raise YunohostError("backup_archive_broken_link", path=archive_file) # We return a raw bottle HTTPresponse (instead of serializable data like # list/dict, ...), which is gonna be picked and used directly by moulinette from bottle import static_file + archive_folder, archive_file_name = archive_file.rsplit("/", 1) return static_file(archive_file_name, archive_folder, download=archive_file_name) @@ -2218,13 +2411,13 @@ def backup_info(name, with_details=False, human_readable=False): human_readable -- Print sizes in human readable format """ - archive_file = '%s/%s.tar' % (ARCHIVES_PATH, name) + archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): archive_file += ".gz" if not os.path.lexists(archive_file): - raise YunohostError('backup_archive_name_unknown', name=name) + raise YunohostError("backup_archive_name_unknown", name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2232,33 +2425,39 @@ 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 YunohostError('backup_archive_broken_link', - path=archive_file) + raise YunohostError("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" if archive_file.endswith(".gz") else "r") - info_dir = info_file + '.d' + tar = tarfile.open( + archive_file, "r:gz" if archive_file.endswith(".gz") else "r" + ) + info_dir = info_file + ".d" try: files_in_archive = tar.getnames() except IOError as e: - raise YunohostError("backup_archive_corrupted", archive=archive_file, error=str(e)) + raise YunohostError( + "backup_archive_corrupted", archive=archive_file, error=str(e) + ) try: if "info.json" in files_in_archive: - tar.extract('info.json', path=info_dir) + tar.extract("info.json", path=info_dir) elif "./info.json" in files_in_archive: - tar.extract('./info.json', path=info_dir) + tar.extract("./info.json", path=info_dir) else: raise KeyError except KeyError: - logger.debug("unable to retrieve '%s' inside the archive", - info_file, exc_info=1) - raise YunohostError('backup_archive_cant_retrieve_info_json', archive=archive_file) + logger.debug( + "unable to retrieve '%s' inside the archive", info_file, exc_info=1 + ) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=archive_file + ) else: - shutil.move(os.path.join(info_dir, 'info.json'), info_file) + shutil.move(os.path.join(info_dir, "info.json"), info_file) finally: tar.close() os.rmdir(info_dir) @@ -2269,23 +2468,28 @@ def backup_info(name, with_details=False, human_readable=False): info = json.load(f) except: logger.debug("unable to load '%s'", info_file, exc_info=1) - raise YunohostError('backup_archive_cant_retrieve_info_json', archive=archive_file) + raise YunohostError( + "backup_archive_cant_retrieve_info_json", archive=archive_file + ) # Retrieve backup size - size = info.get('size', 0) + size = info.get("size", 0) if not size: - tar = tarfile.open(archive_file, "r:gz" if archive_file.endswith(".gz") else "r") - size = reduce(lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y), - tar.getmembers()) + tar = tarfile.open( + archive_file, "r:gz" if archive_file.endswith(".gz") else "r" + ) + size = reduce( + lambda x, y: getattr(x, "size", x) + getattr(y, "size", y), tar.getmembers() + ) tar.close() if human_readable: - size = binary_to_human(size) + 'B' + size = binary_to_human(size) + "B" result = { - 'path': archive_file, - 'created_at': datetime.utcfromtimestamp(info['created_at']), - 'description': info['description'], - 'size': size, + "path": archive_file, + "created_at": datetime.utcfromtimestamp(info["created_at"]), + "description": info["description"], + "size": size, } if with_details: @@ -2309,7 +2513,7 @@ def backup_info(name, with_details=False, human_readable=False): if name in info["size_details"][category].keys(): key_info["size"] = info["size_details"][category][name] if human_readable: - key_info["size"] = binary_to_human(key_info["size"]) + 'B' + key_info["size"] = binary_to_human(key_info["size"]) + "B" else: key_info["size"] = -1 if human_readable: @@ -2329,14 +2533,13 @@ def backup_delete(name): """ if name not in backup_list()["archives"]: - raise YunohostError('backup_archive_name_unknown', - name=name) + raise YunohostError("backup_archive_name_unknown", name=name) - hook_callback('pre_backup_delete', args=[name]) + hook_callback("pre_backup_delete", args=[name]) - archive_file = '%s/%s.tar' % (ARCHIVES_PATH, name) + archive_file = "%s/%s.tar" % (ARCHIVES_PATH, name) if os.path.exists(archive_file + ".gz"): - archive_file += '.gz' + archive_file += ".gz" info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) files_to_delete = [archive_file, info_file] @@ -2351,11 +2554,12 @@ def backup_delete(name): os.remove(backup_file) except: logger.debug("unable to delete '%s'", backup_file, exc_info=1) - logger.warning(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]) + hook_callback("post_backup_delete", args=[name]) + + logger.success(m18n.n("backup_deleted")) - logger.success(m18n.n('backup_deleted')) # # Misc helpers # @@ -2366,7 +2570,7 @@ def _create_archive_dir(): """ Create the YunoHost archives directory if doesn't exist """ if not os.path.isdir(ARCHIVES_PATH): if os.path.lexists(ARCHIVES_PATH): - raise YunohostError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH) + raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH) # Create the archive folder, with 'admin' as owner, such that # people can scp archives out of the server @@ -2378,9 +2582,9 @@ def _call_for_each_path(self, callback, csv_path=None): 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']) + backup_csv = csv.DictReader(backup_file, fieldnames=["source", "dest"]) for row in backup_csv: - callback(self, row['source'], row['dest']) + callback(self, row["source"], row["dest"]) def _recursive_umount(directory): @@ -2392,16 +2596,18 @@ def _recursive_umount(directory): """ mount_lines = 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)] + points_to_umount = [ + line.split(" ")[2] + for line in mount_lines + if len(line) >= 3 and line.split(" ")[2].startswith(directory) + ] everything_went_fine = True for point in reversed(points_to_umount): ret = subprocess.call(["umount", point]) if ret != 0: everything_went_fine = False - logger.warning(m18n.n('backup_cleaning_failed', point)) + logger.warning(m18n.n("backup_cleaning_failed", point)) continue return everything_went_fine @@ -2416,7 +2622,7 @@ def disk_usage(path): # We don't do this in python with os.stat because we don't want # to follow symlinks - du_output = check_output(['du', '-sb', path], shell=False) + du_output = check_output(["du", "-sb", path], shell=False) return int(du_output.split()[0]) @@ -2427,14 +2633,14 @@ def binary_to_human(n, customary=False): n -- Number to convert customary -- Use customary symbol instead of IEC standard """ - symbols = ('Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi') + symbols = ("Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi") if customary: - symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') + symbols = ("K", "M", "G", "T", "P", "E", "Z", "Y") prefix = {} for i, s in enumerate(symbols): prefix[s] = 1 << (i + 1) * 10 for s in reversed(symbols): if n >= prefix[s]: value = float(n) / prefix[s] - return '%.1f%s' % (value, s) + return "%.1f%s" % (value, s) return "%s" % n diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 23903c92c..566c56df1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -45,7 +45,7 @@ from yunohost.service import _run_service_command from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger -logger = getActionLogger('yunohost.certmanager') +logger = getActionLogger("yunohost.certmanager") CERT_FOLDER = "/etc/yunohost/certs/" TMP_FOLDER = "/tmp/acme-challenge-private/" @@ -54,7 +54,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" -SSL_DIR = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' +SSL_DIR = "/usr/share/yunohost/yunohost-config/ssl/yunoCA" KEY_SIZE = 3072 @@ -83,14 +83,14 @@ def certificate_status(domain_list, full=False): # If no domains given, consider all yunohost domains if domain_list == []: - domain_list = yunohost.domain.domain_list()['domains'] + domain_list = yunohost.domain.domain_list()["domains"] # Else, validate that yunohost knows the domains given else: - yunohost_domains_list = yunohost.domain.domain_list()['domains'] + yunohost_domains_list = yunohost.domain.domain_list()["domains"] for domain in domain_list: # Is it in Yunohost domain list? if domain not in yunohost_domains_list: - raise YunohostError('domain_name_unknown', domain=domain) + raise YunohostError("domain_name_unknown", domain=domain) certificates = {} @@ -116,7 +116,9 @@ def certificate_status(domain_list, full=False): return {"certificates": certificates} -def certificate_install(domain_list, force=False, no_checks=False, self_signed=False, staging=False): +def certificate_install( + domain_list, force=False, no_checks=False, self_signed=False, staging=False +): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -131,21 +133,24 @@ def certificate_install(domain_list, force=False, no_checks=False, self_signed=F if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt( - domain_list, force, no_checks, staging) + _certificate_install_letsencrypt(domain_list, force, no_checks, staging) def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: - operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)], - args={'force': force}) + operation_logger = OperationLogger( + "selfsigned_cert_install", [("domain", domain)], args={"force": force} + ) # Paths of files and folder we'll need date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % ( - CERT_FOLDER, domain, date_tag) + CERT_FOLDER, + domain, + date_tag, + ) conf_template = os.path.join(SSL_DIR, "openssl.cnf") @@ -160,8 +165,10 @@ 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 YunohostError('certmanager_attempt_to_replace_valid_cert', domain=domain) + if status["summary"]["code"] in ("good", "great"): + raise YunohostError( + "certmanager_attempt_to_replace_valid_cert", domain=domain + ) operation_logger.start() @@ -185,13 +192,14 @@ def _certificate_install_selfsigned(domain_list, force=False): for command in commands: p = subprocess.Popen( - command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) out, _ = p.communicate() if p.returncode != 0: logger.warning(out) - raise YunohostError('domain_cert_gen_failed') + raise YunohostError("domain_cert_gen_failed") else: logger.debug(out) @@ -217,17 +225,27 @@ def _certificate_install_selfsigned(domain_list, force=False): # 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: + if ( + status + and status["CA_type"]["code"] == "self-signed" + and status["validity"] > 3648 + ): logger.success( - m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + m18n.n("certmanager_cert_install_success_selfsigned", domain=domain) + ) operation_logger.success() else: - msg = "Installation of self-signed certificate installation for %s failed !" % (domain) + msg = ( + "Installation of self-signed certificate installation for %s failed !" + % (domain) + ) logger.error(msg) operation_logger.error(msg) -def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False, staging=False): +def _certificate_install_letsencrypt( + domain_list, force=False, no_checks=False, staging=False +): import yunohost.domain if not os.path.exists(ACCOUNT_KEY_FILE): @@ -236,7 +254,7 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False, # If no domains given, consider all yunohost domains with self-signed # certificates if domain_list == []: - for domain in yunohost.domain.domain_list()['domains']: + for domain in yunohost.domain.domain_list()["domains"]: status = _get_status(domain) if status["CA_type"]["code"] != "self-signed": @@ -247,18 +265,21 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False, # Else, validate that yunohost knows the domains given else: for domain in domain_list: - yunohost_domains_list = yunohost.domain.domain_list()['domains'] + yunohost_domains_list = yunohost.domain.domain_list()["domains"] if domain not in yunohost_domains_list: - raise YunohostError('domain_name_unknown', domain=domain) + raise YunohostError("domain_name_unknown", domain=domain) # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise YunohostError('certmanager_domain_cert_not_selfsigned', domain=domain) + raise YunohostError( + "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 !") + "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: @@ -270,32 +291,40 @@ def _certificate_install_letsencrypt(domain_list, force=False, no_checks=False, logger.error(e) continue - logger.info( - "Now attempting install of certificate for domain %s!", domain) + logger.info("Now attempting install of certificate for domain %s!", domain) - operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)], - args={'force': force, 'no_checks': no_checks, - 'staging': staging}) + operation_logger = OperationLogger( + "letsencrypt_cert_install", + [("domain", domain)], + args={"force": force, "no_checks": no_checks, "staging": staging}, + ) operation_logger.start() try: _fetch_and_enable_new_certificate(domain, staging, no_checks=no_checks) except Exception as e: - msg = "Certificate installation for %s failed !\nException: %s" % (domain, e) + msg = "Certificate installation for %s failed !\nException: %s" % ( + domain, + e, + ) logger.error(msg) operation_logger.error(msg) if no_checks: - logger.error("Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." % domain) + logger.error( + "Please consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." + % domain + ) else: _install_cron(no_checks=no_checks) - logger.success( - m18n.n("certmanager_cert_install_success", domain=domain)) + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) operation_logger.success() -def certificate_renew(domain_list, force=False, no_checks=False, email=False, staging=False): +def certificate_renew( + domain_list, force=False, no_checks=False, email=False, staging=False +): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -312,7 +341,7 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st # If no domains given, consider all yunohost domains with Let's Encrypt # certificates if domain_list == []: - for domain in yunohost.domain.domain_list()['domains']: + for domain in yunohost.domain.domain_list()["domains"]: # Does it have a Let's Encrypt cert? status = _get_status(domain) @@ -325,8 +354,9 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st # 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)) + logger.warning( + m18n.n("certmanager_acme_not_configured_for_domain", domain=domain) + ) continue domain_list.append(domain) @@ -339,26 +369,33 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st for domain in domain_list: # Is it in Yunohost dmomain list? - if domain not in yunohost.domain.domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + if domain not in yunohost.domain.domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) status = _get_status(domain) # Does it expire soon? if status["validity"] > VALIDITY_LIMIT and not force: - raise YunohostError('certmanager_attempt_to_renew_valid_cert', domain=domain) + raise YunohostError( + "certmanager_attempt_to_renew_valid_cert", domain=domain + ) # Does it have a Let's Encrypt cert? if status["CA_type"]["code"] != "lets-encrypt": - raise YunohostError('certmanager_attempt_to_renew_nonLE_cert', domain=domain) + raise YunohostError( + "certmanager_attempt_to_renew_nonLE_cert", domain=domain + ) # Check ACME challenge configured for given domain if not _check_acme_challenge_configuration(domain): - raise YunohostError('certmanager_acme_not_configured_for_domain', domain=domain) + raise YunohostError( + "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 !") + "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: @@ -373,12 +410,18 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st _email_renewing_failed(domain, e) continue - logger.info( - "Now attempting renewing of certificate for domain %s !", domain) + logger.info("Now attempting renewing of certificate for domain %s !", domain) - operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)], - args={'force': force, 'no_checks': no_checks, - 'staging': staging, 'email': email}) + operation_logger = OperationLogger( + "letsencrypt_cert_renew", + [("domain", domain)], + args={ + "force": force, + "no_checks": no_checks, + "staging": staging, + "email": email, + }, + ) operation_logger.start() try: @@ -386,11 +429,15 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st except Exception as e: import traceback from io import StringIO + stack = StringIO() traceback.print_exc(file=stack) msg = "Certificate renewing for %s failed !" % (domain) if no_checks: - msg += "\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." % domain + msg += ( + "\nPlease consider checking the 'DNS records' (basic) and 'Web' categories of the diagnosis to check for possible issues that may prevent installing a Let's Encrypt certificate on domain %s." + % domain + ) logger.error(msg) operation_logger.error(msg) logger.error(stack.getvalue()) @@ -400,10 +447,10 @@ def certificate_renew(domain_list, force=False, no_checks=False, email=False, st logger.error("Sending email with details to root ...") _email_renewing_failed(domain, msg + "\n" + e, stack.getvalue()) else: - logger.success( - m18n.n("certmanager_cert_renew_success", domain=domain)) + logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) operation_logger.success() + # # Back-end stuff # # @@ -454,7 +501,12 @@ investigate : -- Certificate Manager -""" % (domain, exception_message, stack, logs) +""" % ( + domain, + exception_message, + stack, + logs, + ) message = """\ From: %s @@ -462,9 +514,15 @@ To: %s Subject: %s %s -""" % (from_, to_, subject_, text) +""" % ( + from_, + to_, + subject_, + text, + ) import smtplib + smtp = smtplib.SMTP("localhost") smtp.sendmail(from_, [to_], message) smtp.quit() @@ -503,8 +561,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): _regen_dnsmasq_if_needed() # Prepare certificate signing request - logger.debug( - "Prepare key and certificate signing request (CSR) for %s...", domain) + logger.debug("Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) @@ -523,23 +580,25 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY try: - signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, - domain_csr_file, - WEBROOT_FOLDER, - log=logger, - disable_check=no_checks, - CA=certification_authority) + signed_certificate = sign_certificate( + ACCOUNT_KEY_FILE, + domain_csr_file, + WEBROOT_FOLDER, + log=logger, + disable_check=no_checks, + CA=certification_authority, + ) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): - raise YunohostError('certmanager_hit_rate_limit', domain=domain) + raise YunohostError("certmanager_hit_rate_limit", domain=domain) else: logger.error(str(e)) - raise YunohostError('certmanager_cert_signing_failed') + raise YunohostError("certmanager_cert_signing_failed") except Exception as e: logger.error(str(e)) - raise YunohostError('certmanager_cert_signing_failed') + raise YunohostError("certmanager_cert_signing_failed") # Now save the key and signed certificate logger.debug("Saving the key and signed certificate...") @@ -553,7 +612,11 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): folder_flag = "letsencrypt" new_cert_folder = "%s/%s-history/%s-%s" % ( - CERT_FOLDER, domain, date_tag, folder_flag) + CERT_FOLDER, + domain, + date_tag, + folder_flag, + ) os.makedirs(new_cert_folder) @@ -581,11 +644,14 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): status_summary = _get_status(domain)["summary"] if status_summary["code"] != "great": - raise YunohostError('certmanager_certificate_fetching_or_enabling_failed', domain=domain) + raise YunohostError( + "certmanager_certificate_fetching_or_enabling_failed", domain=domain + ) 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() @@ -593,17 +659,37 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): csr.get_subject().CN = domain from yunohost.domain import domain_list + # For "parent" domains, include xmpp-upload subdomain in subject alternate names if domain in domain_list(exclude_subdomains=True)["domains"]: subdomain = "xmpp-upload." + domain - xmpp_records = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "xmpp"}).get("data") or {} + xmpp_records = ( + Diagnoser.get_cached_report( + "dnsrecords", item={"domain": domain, "category": "xmpp"} + ).get("data") + or {} + ) if xmpp_records.get("CNAME:xmpp-upload") == "OK": - csr.add_extensions([crypto.X509Extension("subjectAltName".encode('utf8'), False, ("DNS:" + subdomain).encode('utf8'))]) + csr.add_extensions( + [ + crypto.X509Extension( + "subjectAltName".encode("utf8"), + False, + ("DNS:" + subdomain).encode("utf8"), + ) + ] + ) else: - logger.warning(m18n.n('certmanager_warning_subdomain_dns_record', subdomain=subdomain, domain=domain)) + logger.warning( + m18n.n( + "certmanager_warning_subdomain_dns_record", + subdomain=subdomain, + domain=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) @@ -624,24 +710,32 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - raise YunohostError('certmanager_no_cert_file', domain=domain, file=cert_file) + raise YunohostError("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()) + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) except Exception as exception: import traceback + traceback.print_exc(file=sys.stdout) - raise YunohostError('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception) + raise YunohostError( + "certmanager_cannot_read_cert", + domain=domain, + file=cert_file, + reason=exception, + ) cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN organization_name = cert.get_issuer().O - valid_up_to = datetime.strptime(cert.get_notAfter().decode('utf-8'), "%Y%m%d%H%M%SZ") + valid_up_to = datetime.strptime( + cert.get_notAfter().decode("utf-8"), "%Y%m%d%H%M%SZ" + ) days_remaining = (valid_up_to - datetime.utcnow()).days - if cert_issuer == 'yunohost.org' or cert_issuer == _name_self_CA(): + if cert_issuer == "yunohost.org" or cert_issuer == _name_self_CA(): CA_type = { "code": "self-signed", "verbose": "Self-signed", @@ -710,6 +804,7 @@ def _get_status(domain): "summary": status_summary, } + # # Misc small stuff ... # # @@ -723,12 +818,14 @@ 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) with open(destination_path, "wb") 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 @@ -760,15 +857,16 @@ def _enable_certificate(domain, new_cert_folder): for service in ("postfix", "dovecot", "metronome"): _run_service_command("restart", service) - if os.path.isfile('/etc/yunohost/installed'): + if os.path.isfile("/etc/yunohost/installed"): # regen nginx conf to be sure it integrates OCSP Stapling # (We don't do this yet if postinstall is not finished yet) - regen_conf(names=['nginx']) + regen_conf(names=["nginx"]) _run_service_command("reload", "nginx") from yunohost.hook import hook_callback - hook_callback('post_cert_update', args=[domain]) + + hook_callback("post_cert_update", args=[domain]) def _backup_current_cert(domain): @@ -784,19 +882,36 @@ def _backup_current_cert(domain): def _check_domain_is_ready_for_ACME(domain): - dnsrecords = Diagnoser.get_cached_report("dnsrecords", item={"domain": domain, "category": "basic"}, warn_if_no_cache=False) or {} - httpreachable = Diagnoser.get_cached_report("web", item={"domain": domain}, warn_if_no_cache=False) or {} + dnsrecords = ( + Diagnoser.get_cached_report( + "dnsrecords", + item={"domain": domain, "category": "basic"}, + warn_if_no_cache=False, + ) + or {} + ) + httpreachable = ( + Diagnoser.get_cached_report( + "web", item={"domain": domain}, warn_if_no_cache=False + ) + or {} + ) if not dnsrecords or not httpreachable: - raise YunohostError('certmanager_domain_not_diagnosed_yet', domain=domain) + raise YunohostError("certmanager_domain_not_diagnosed_yet", domain=domain) # Check if IP from DNS matches public IP - if not dnsrecords.get("status") in ["SUCCESS", "WARNING"]: # Warning is for missing IPv6 record which ain't critical for ACME - raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain) + if not dnsrecords.get("status") in [ + "SUCCESS", + "WARNING", + ]: # Warning is for missing IPv6 record which ain't critical for ACME + raise YunohostError( + "certmanager_domain_dns_ip_differs_from_public_ip", domain=domain + ) # Check if domain seems to be accessible through HTTP? if not httpreachable.get("status") == "SUCCESS": - raise YunohostError('certmanager_domain_http_not_working', domain=domain) + raise YunohostError("certmanager_domain_http_not_working", domain=domain) # FIXME / TODO : ideally this should not be needed. There should be a proper @@ -840,7 +955,7 @@ def _name_self_CA(): ca_conf = os.path.join(SSL_DIR, "openssl.ca.cnf") if not os.path.exists(ca_conf): - logger.warning(m18n.n('certmanager_self_ca_conf_file_not_found', file=ca_conf)) + logger.warning(m18n.n("certmanager_self_ca_conf_file_not_found", file=ca_conf)) return "" with open(ca_conf) as f: @@ -850,7 +965,7 @@ def _name_self_CA(): if line.startswith("commonName_default"): return line.split()[2] - logger.warning(m18n.n('certmanager_unable_to_parse_self_CA_name', file=ca_conf)) + logger.warning(m18n.n("certmanager_unable_to_parse_self_CA_name", file=ca_conf)) return "" diff --git a/src/yunohost/data_migrations/0015_migrate_to_buster.py b/src/yunohost/data_migrations/0015_migrate_to_buster.py index 638f519ae..549d0088c 100644 --- a/src/yunohost/data_migrations/0015_migrate_to_buster.py +++ b/src/yunohost/data_migrations/0015_migrate_to_buster.py @@ -1,4 +1,3 @@ - import glob import os @@ -12,9 +11,12 @@ from yunohost.tools import Migration, tools_update, tools_upgrade from yunohost.app import unstable_apps from yunohost.regenconf import manually_modified_files from yunohost.utils.filesystem import free_space_in_directory -from yunohost.utils.packages import get_ynh_package_version, _list_upgradable_apt_packages +from yunohost.utils.packages import ( + get_ynh_package_version, + _list_upgradable_apt_packages, +) -logger = getActionLogger('yunohost.migration') +logger = getActionLogger("yunohost.migration") class MyMigration(Migration): @@ -44,10 +46,14 @@ class MyMigration(Migration): tools_update(system=True) # Tell libc6 it's okay to restart system stuff during the upgrade - os.system("echo 'libc6 libraries/restart-without-asking boolean true' | debconf-set-selections") + 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") + os.system( + "echo 'postgresql-common postgresql-common/obsolete-major seen true' | debconf-set-selections" + ) # # Specific packages upgrades @@ -56,16 +62,22 @@ class MyMigration(Migration): # Update unscd independently, was 0.53-1+yunohost on stretch (custom build of ours) but now it's 0.53-1+b1 on vanilla buster, # which for apt appears as a lower version (hence the --allow-downgrades and the hardcoded version number) - unscd_version = check_output('dpkg -s unscd | grep "^Version: " | cut -d " " -f 2') + unscd_version = check_output( + 'dpkg -s unscd | grep "^Version: " | cut -d " " -f 2' + ) if "yunohost" in unscd_version: - new_version = check_output("LC_ALL=C apt policy unscd 2>/dev/null | grep -v '\\*\\*\\*' | grep http -B1 | head -n 1 | awk '{print $1}'").strip() + new_version = check_output( + "LC_ALL=C apt policy unscd 2>/dev/null | grep -v '\\*\\*\\*' | grep http -B1 | head -n 1 | awk '{print $1}'" + ).strip() if new_version: - self.apt_install('unscd=%s --allow-downgrades' % new_version) + self.apt_install("unscd=%s --allow-downgrades" % new_version) else: logger.warning("Could not identify which version of unscd to install") # Upgrade libpam-modules independently, small issue related to willing to overwrite a file previously provided by Yunohost - libpammodules_version = check_output('dpkg -s libpam-modules | grep "^Version: " | cut -d " " -f 2') + libpammodules_version = check_output( + 'dpkg -s libpam-modules | grep "^Version: " | cut -d " " -f 2' + ) if not libpammodules_version.startswith("1.3"): self.apt_install('libpam-modules -o Dpkg::Options::="--force-overwrite"') @@ -100,10 +112,14 @@ class MyMigration(Migration): # 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 | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2")) + return int( + check_output( + "grep VERSION_ID /etc/os-release | head -n 1 | tr '\"' ' ' | cut -d ' ' -f2" + ) + ) def yunohost_major_version(self): - return int(get_ynh_package_version("yunohost")["version"].split('.')[0]) + return int(get_ynh_package_version("yunohost")["version"].split(".")[0]) def check_assertions(self): @@ -111,12 +127,14 @@ 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 3.x... - if not self.debian_major_version() == 9 \ - and not self.yunohost_major_version() == 3: + if ( + not self.debian_major_version() == 9 + and not self.yunohost_major_version() == 3 + ): raise YunohostError("migration_0015_not_stretch") # Have > 1 Go free space on /var/ ? - if free_space_in_directory("/var/") / (1024**3) < 1.0: + if free_space_in_directory("/var/") / (1024 ** 3) < 1.0: raise YunohostError("migration_0015_not_enough_free_space") # Check system is up to date @@ -136,8 +154,10 @@ class MyMigration(Migration): # NB : we do both check to cover situations where the upgrade crashed # in the middle and debian version could be >= 10.x but yunohost package # would still be in 3.x... - if not self.debian_major_version() == 9 \ - and not self.yunohost_major_version() == 3: + if ( + not self.debian_major_version() == 9 + and not self.yunohost_major_version() == 3 + ): return None # Get list of problematic apps ? I.e. not official or community+working @@ -150,13 +170,21 @@ class MyMigration(Migration): message = m18n.n("migration_0015_general_warning") - message = "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" + message + message = ( + "N.B.: This migration has been tested by the community over the last few months but has only been declared stable recently. If your server hosts critical services and if you are not too confident with debugging possible issues, we recommend you to wait a little bit more while we gather more feedback and polish things up. If on the other hand you are relatively confident with debugging small issues that may arise, you are encouraged to run this migration ;)! You can read about remaining known issues and feedback from the community here: https://forum.yunohost.org/t/12195\n\n" + + message + ) if problematic_apps: - message += "\n\n" + m18n.n("migration_0015_problematic_apps_warning", problematic_apps=problematic_apps) + message += "\n\n" + m18n.n( + "migration_0015_problematic_apps_warning", + problematic_apps=problematic_apps, + ) if modified_files: - message += "\n\n" + m18n.n("migration_0015_modified_files", manually_modified_files=modified_files) + message += "\n\n" + m18n.n( + "migration_0015_modified_files", manually_modified_files=modified_files + ) return message @@ -170,23 +198,27 @@ class MyMigration(Migration): # - comments lines containing "backports" # - replace 'stretch/updates' by 'strech/updates' (or same with -) for f in sources_list: - command = "sed -i -e 's@ stretch @ buster @g' " \ - "-e '/backports/ s@^#*@#@' " \ - "-e 's@ stretch/updates @ buster/updates @g' " \ - "-e 's@ stretch-@ buster-@g' " \ - "{}".format(f) + command = ( + "sed -i -e 's@ stretch @ buster @g' " + "-e '/backports/ s@^#*@#@' " + "-e 's@ stretch/updates @ buster/updates @g' " + "-e 's@ stretch-@ buster-@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; }" + command = ( + "dpkg --get-selections" + " | grep -v deinstall" + " | awk '{print $1}'" + " | { grep 'ynh-deps$' || true; }" + ) output = check_output(command) - return output.split('\n') if output else [] + return output.split("\n") if output else [] def hold(self, packages): for package in packages: @@ -197,16 +229,20 @@ class MyMigration(Migration): os.system("apt-mark unhold {}".format(package)) def apt_install(self, cmd): - def is_relevant(l): return "Reading database ..." not in l.rstrip() callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") if is_relevant(l) else logger.debug(l.rstrip() + "\r"), + lambda l: logger.info("+ " + l.rstrip() + "\r") + if is_relevant(l) + else logger.debug(l.rstrip() + "\r"), lambda l: logger.warning(l.rstrip()), ) - cmd = "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " + cmd + cmd = ( + "LC_ALL=C DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt install --quiet -o=Dpkg::Use-Pty=0 --fix-broken --assume-yes " + + cmd + ) logger.debug("Running: %s" % cmd) @@ -214,15 +250,24 @@ class MyMigration(Migration): def validate_and_upgrade_cert_if_necessary(self): - active_certs = set(check_output("grep -roh '/.*crt.pem' /etc/nginx/").split("\n")) + active_certs = set( + check_output("grep -roh '/.*crt.pem' /etc/nginx/").split("\n") + ) cmd = "LC_ALL=C openssl x509 -in %s -text -noout | grep -i 'Signature Algorithm:' | awk '{print $3}' | uniq" - default_crt = '/etc/yunohost/certs/yunohost.org/crt.pem' - default_key = '/etc/yunohost/certs/yunohost.org/key.pem' - default_signature = check_output(cmd % default_crt) if default_crt in active_certs else None - if default_signature is not None and (default_signature.startswith("md5") or default_signature.startswith("sha1")): - logger.warning("%s is using a pretty old certificate incompatible with newer versions of nginx ... attempting to regenerate a fresh one" % default_crt) + default_crt = "/etc/yunohost/certs/yunohost.org/crt.pem" + default_key = "/etc/yunohost/certs/yunohost.org/key.pem" + default_signature = ( + check_output(cmd % default_crt) if default_crt in active_certs else None + ) + if default_signature is not None and ( + default_signature.startswith("md5") or default_signature.startswith("sha1") + ): + logger.warning( + "%s is using a pretty old certificate incompatible with newer versions of nginx ... attempting to regenerate a fresh one" + % default_crt + ) os.system("mv %s %s.old" % (default_crt, default_crt)) os.system("mv %s %s.old" % (default_key, default_key)) @@ -241,4 +286,6 @@ class MyMigration(Migration): weak_certs = [cert for cert in signatures.keys() if cert_is_weak(cert)] if weak_certs: - raise YunohostError("migration_0015_weak_certs", certs=", ".join(weak_certs)) + raise YunohostError( + "migration_0015_weak_certs", certs=", ".join(weak_certs) + ) diff --git a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py index 73875d359..6b424f211 100644 --- a/src/yunohost/data_migrations/0016_php70_to_php73_pools.py +++ b/src/yunohost/data_migrations/0016_php70_to_php73_pools.py @@ -8,7 +8,7 @@ from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings from yunohost.tools import Migration from yunohost.service import _run_service_command -logger = getActionLogger('yunohost.migration') +logger = getActionLogger("yunohost.migration") PHP70_POOLS = "/etc/php/7.0/fpm/pool.d" PHP73_POOLS = "/etc/php/7.3/fpm/pool.d" @@ -16,7 +16,9 @@ PHP73_POOLS = "/etc/php/7.3/fpm/pool.d" PHP70_SOCKETS_PREFIX = "/run/php/php7.0-fpm" PHP73_SOCKETS_PREFIX = "/run/php/php7.3-fpm" -MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {}".format(PHP70_POOLS) +MIGRATION_COMMENT = ( + "; YunoHost note : this file was automatically moved from {}".format(PHP70_POOLS) +) class MyMigration(Migration): @@ -43,7 +45,9 @@ class MyMigration(Migration): copy2(src, dest) # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format(PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, dest) + c = "sed -i -e 's@{}@{}@g' {}".format( + PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, dest + ) os.system(c) # Also add a comment that it was automatically moved from php7.0 @@ -51,17 +55,23 @@ class MyMigration(Migration): c = "sed -i '1i {}' {}".format(MIGRATION_COMMENT, dest) os.system(c) - app_id = os.path.basename(f)[:-len(".conf")] + app_id = os.path.basename(f)[: -len(".conf")] if _is_installed(app_id): - _patch_legacy_php_versions_in_settings("/etc/yunohost/apps/%s/" % app_id) + _patch_legacy_php_versions_in_settings( + "/etc/yunohost/apps/%s/" % app_id + ) nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/%s.conf" % app_id) for f in nginx_conf_files: # Replace the socket prefix if it's found - c = "sed -i -e 's@{}@{}@g' {}".format(PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, f) + c = "sed -i -e 's@{}@{}@g' {}".format( + PHP70_SOCKETS_PREFIX, PHP73_SOCKETS_PREFIX, f + ) os.system(c) - os.system("rm /etc/logrotate.d/php7.0-fpm") # We remove this otherwise the logrotate cron will be unhappy + os.system( + "rm /etc/logrotate.d/php7.0-fpm" + ) # We remove this otherwise the logrotate cron will be unhappy # Reload/restart the php pools _run_service_command("restart", "php7.3-fpm") diff --git a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py index 2f277443e..728ae443f 100644 --- a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py +++ b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py @@ -7,7 +7,7 @@ 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') +logger = getActionLogger("yunohost.migration") class MyMigration(Migration): @@ -29,37 +29,54 @@ class MyMigration(Migration): try: self.runcmd("pg_lsclusters | grep -q '^9.6 '") except Exception: - logger.warning("It looks like there's not active 9.6 cluster, so probably don't need to run this migration") + logger.warning( + "It looks like there's not active 9.6 cluster, so probably don't need to run this migration" + ) return - if not space_used_by_directory("/var/lib/postgresql/9.6") > free_space_in_directory("/var/lib/postgresql"): - raise YunohostError("migration_0017_not_enough_space", path="/var/lib/postgresql/") + if not space_used_by_directory( + "/var/lib/postgresql/9.6" + ) > free_space_in_directory("/var/lib/postgresql"): + raise YunohostError( + "migration_0017_not_enough_space", path="/var/lib/postgresql/" + ) self.runcmd("systemctl stop postgresql") - self.runcmd("LC_ALL=C pg_dropcluster --stop 11 main || true") # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster) + self.runcmd( + "LC_ALL=C pg_dropcluster --stop 11 main || true" + ) # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster) self.runcmd("LC_ALL=C pg_upgradecluster -m upgrade 9.6 main") self.runcmd("LC_ALL=C pg_dropcluster --stop 9.6 main") self.runcmd("systemctl start postgresql") def package_is_installed(self, package_name): - (returncode, out, err) = self.runcmd("dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), raise_on_errors=False) + (returncode, out, err) = self.runcmd( + "dpkg --list | grep '^ii ' | grep -q -w {}".format(package_name), + raise_on_errors=False, + ) return returncode == 0 def runcmd(self, cmd, raise_on_errors=True): logger.debug("Running command: " + cmd) - p = subprocess.Popen(cmd, - shell=True, - executable='/bin/bash', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + cmd, + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out, err = p.communicate() returncode = p.returncode if raise_on_errors and returncode != 0: - raise YunohostError("Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(cmd, returncode, out, err)) + raise YunohostError( + "Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format( + cmd, returncode, out, err + ) + ) out = out.strip().split("\n") return (returncode, out, err) diff --git a/src/yunohost/data_migrations/0018_xtable_to_nftable.py b/src/yunohost/data_migrations/0018_xtable_to_nftable.py index 8a7e11412..af5d11e43 100644 --- a/src/yunohost/data_migrations/0018_xtable_to_nftable.py +++ b/src/yunohost/data_migrations/0018_xtable_to_nftable.py @@ -9,7 +9,7 @@ from yunohost.firewall import firewall_reload from yunohost.service import service_restart from yunohost.tools import Migration -logger = getActionLogger('yunohost.migration') +logger = getActionLogger("yunohost.migration") class MyMigration(Migration): @@ -24,9 +24,9 @@ class MyMigration(Migration): self.do_ipv6 = os.system("ip6tables -w -L >/dev/null") == 0 if not self.do_ipv4: - logger.warning(m18n.n('iptables_unavailable')) + logger.warning(m18n.n("iptables_unavailable")) if not self.do_ipv6: - logger.warning(m18n.n('ip6tables_unavailable')) + logger.warning(m18n.n("ip6tables_unavailable")) backup_folder = "/home/yunohost.backup/premigration/xtable_to_nftable/" if not os.path.exists(backup_folder): @@ -36,13 +36,21 @@ class MyMigration(Migration): # Backup existing legacy rules to be able to rollback if self.do_ipv4 and not os.path.exists(self.backup_rules_ipv4): - self.runcmd("iptables-legacy -L >/dev/null") # For some reason if we don't do this, iptables-legacy-save is empty ? + self.runcmd( + "iptables-legacy -L >/dev/null" + ) # For some reason if we don't do this, iptables-legacy-save is empty ? self.runcmd("iptables-legacy-save > %s" % self.backup_rules_ipv4) - assert open(self.backup_rules_ipv4).read().strip(), "Uhoh backup of legacy ipv4 rules is empty !?" + assert ( + open(self.backup_rules_ipv4).read().strip() + ), "Uhoh backup of legacy ipv4 rules is empty !?" if self.do_ipv6 and not os.path.exists(self.backup_rules_ipv6): - self.runcmd("ip6tables-legacy -L >/dev/null") # For some reason if we don't do this, iptables-legacy-save is empty ? + self.runcmd( + "ip6tables-legacy -L >/dev/null" + ) # For some reason if we don't do this, iptables-legacy-save is empty ? self.runcmd("ip6tables-legacy-save > %s" % self.backup_rules_ipv6) - assert open(self.backup_rules_ipv6).read().strip(), "Uhoh backup of legacy ipv6 rules is empty !?" + assert ( + open(self.backup_rules_ipv6).read().strip() + ), "Uhoh backup of legacy ipv6 rules is empty !?" # We inject the legacy rules (iptables-legacy) into the new iptable (just "iptables") try: @@ -52,23 +60,27 @@ class MyMigration(Migration): self.runcmd("ip6tables-legacy-save | ip6tables-restore") except Exception as e: self.rollback() - raise YunohostError("migration_0018_failed_to_migrate_iptables_rules", error=e) + raise YunohostError( + "migration_0018_failed_to_migrate_iptables_rules", error=e + ) # Reset everything in iptables-legacy # Stolen from https://serverfault.com/a/200642 try: if self.do_ipv4: self.runcmd( - "iptables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat - " /^:[A-Z]+ [^-]/ { print $1 \" ACCEPT\" ; }" # Turn all policies to accept - " /COMMIT/ { print $0; }'" # Keep the line COMMIT - " | iptables-legacy-restore") + "iptables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat + ' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept + " /COMMIT/ { print $0; }'" # Keep the line COMMIT + " | iptables-legacy-restore" + ) if self.do_ipv6: self.runcmd( - "ip6tables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat - " /^:[A-Z]+ [^-]/ { print $1 \" ACCEPT\" ; }" # Turn all policies to accept - " /COMMIT/ { print $0; }'" # Keep the line COMMIT - " | ip6tables-legacy-restore") + "ip6tables-legacy-save | awk '/^[*]/ { print $1 }" # Keep lines like *raw, *filter and *nat + ' /^:[A-Z]+ [^-]/ { print $1 " ACCEPT" ; }' # Turn all policies to accept + " /COMMIT/ { print $0; }'" # Keep the line COMMIT + " | ip6tables-legacy-restore" + ) except Exception as e: self.rollback() raise YunohostError("migration_0018_failed_to_reset_legacy_rules", error=e) @@ -93,16 +105,22 @@ class MyMigration(Migration): logger.debug("Running command: " + cmd) - p = subprocess.Popen(cmd, - shell=True, - executable='/bin/bash', - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + p = subprocess.Popen( + cmd, + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) out, err = p.communicate() returncode = p.returncode if raise_on_errors and returncode != 0: - raise YunohostError("Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format(cmd, returncode, out, err)) + raise YunohostError( + "Failed to run command '{}'.\nreturncode: {}\nstdout:\n{}\nstderr:\n{}\n".format( + cmd, returncode, out, err + ) + ) out = out.strip().split("\n") return (returncode, out, err) diff --git a/src/yunohost/data_migrations/0019_extend_permissions_features.py b/src/yunohost/data_migrations/0019_extend_permissions_features.py index 6c292e014..07f740a2b 100644 --- a/src/yunohost/data_migrations/0019_extend_permissions_features.py +++ b/src/yunohost/data_migrations/0019_extend_permissions_features.py @@ -9,12 +9,12 @@ from yunohost.tools import Migration from yunohost.permission import user_permission_list from yunohost.utils.legacy import migrate_legacy_permission_settings -logger = getActionLogger('yunohost.migration') +logger = getActionLogger("yunohost.migration") class MyMigration(Migration): """ - Add protected attribute in LDAP permission + Add protected attribute in LDAP permission """ required = True @@ -25,14 +25,19 @@ class MyMigration(Migration): from yunohost.regenconf import regen_conf, BACKUP_CONF_DIR # Check if the migration can be processed - ldap_regen_conf_status = regen_conf(names=['slapd'], dry_run=True) + ldap_regen_conf_status = regen_conf(names=["slapd"], dry_run=True) # By this we check if the have been customized - if ldap_regen_conf_status and ldap_regen_conf_status['slapd']['pending']: - logger.warning(m18n.n("migration_0019_slapd_config_will_be_overwritten", conf_backup_folder=BACKUP_CONF_DIR)) + if ldap_regen_conf_status and ldap_regen_conf_status["slapd"]["pending"]: + logger.warning( + m18n.n( + "migration_0019_slapd_config_will_be_overwritten", + conf_backup_folder=BACKUP_CONF_DIR, + ) + ) # Update LDAP schema restart slapd logger.info(m18n.n("migration_0011_update_LDAP_schema")) - regen_conf(names=['slapd'], force=True) + regen_conf(names=["slapd"], force=True) logger.info(m18n.n("migration_0019_add_new_attributes_in_ldap")) ldap = _get_ldap_interface() @@ -43,33 +48,35 @@ class MyMigration(Migration): "mail": "E-mail", "xmpp": "XMPP", "ssh": "SSH", - "sftp": "STFP" + "sftp": "STFP", } - if permission.split('.')[0] in system_perms: + if permission.split(".")[0] in system_perms: update = { - 'authHeader': ["FALSE"], - 'label': [system_perms[permission.split('.')[0]]], - 'showTile': ["FALSE"], - 'isProtected': ["TRUE"], + "authHeader": ["FALSE"], + "label": [system_perms[permission.split(".")[0]]], + "showTile": ["FALSE"], + "isProtected": ["TRUE"], } else: - app, subperm_name = permission.split('.') + app, subperm_name = permission.split(".") if permission.endswith(".main"): update = { - 'authHeader': ["TRUE"], - 'label': [app], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists - 'showTile': ["TRUE"], - 'isProtected': ["FALSE"] + "authHeader": ["TRUE"], + "label": [ + app + ], # Note that this is later re-changed during the call to migrate_legacy_permission_settings() if a 'label' setting exists + "showTile": ["TRUE"], + "isProtected": ["FALSE"], } else: update = { - 'authHeader': ["TRUE"], - 'label': [subperm_name.title()], - 'showTile': ["FALSE"], - 'isProtected': ["TRUE"] + "authHeader": ["TRUE"], + "label": [subperm_name.title()], + "showTile": ["FALSE"], + "isProtected": ["TRUE"], } - ldap.update('cn=%s,ou=permission' % permission, update) + ldap.update("cn=%s,ou=permission" % permission, update) def run(self): @@ -80,14 +87,20 @@ class MyMigration(Migration): # Backup LDAP and the apps settings before to do the migration logger.info(m18n.n("migration_0019_backup_before_migration")) try: - backup_folder = "/home/yunohost.backup/premigration/" + time.strftime('%Y%m%d-%H%M%S', time.gmtime()) + backup_folder = "/home/yunohost.backup/premigration/" + time.strftime( + "%Y%m%d-%H%M%S", time.gmtime() + ) os.makedirs(backup_folder, 0o750) os.system("systemctl stop slapd") os.system("cp -r --preserve /etc/ldap %s/ldap_config" % backup_folder) os.system("cp -r --preserve /var/lib/ldap %s/ldap_db" % backup_folder) - os.system("cp -r --preserve /etc/yunohost/apps %s/apps_settings" % backup_folder) + os.system( + "cp -r --preserve /etc/yunohost/apps %s/apps_settings" % backup_folder + ) except Exception as e: - raise YunohostError("migration_0019_can_not_backup_before_migration", error=e) + raise YunohostError( + "migration_0019_can_not_backup_before_migration", error=e + ) finally: os.system("systemctl start slapd") @@ -101,10 +114,15 @@ class MyMigration(Migration): except Exception: logger.warn(m18n.n("migration_0019_migration_failed_trying_to_rollback")) os.system("systemctl stop slapd") - os.system("rm -r /etc/ldap/slapd.d") # To be sure that we don't keep some part of the old config + os.system( + "rm -r /etc/ldap/slapd.d" + ) # To be sure that we don't keep some part of the old config os.system("cp -r --preserve %s/ldap_config/. /etc/ldap/" % backup_folder) os.system("cp -r --preserve %s/ldap_db/. /var/lib/ldap/" % backup_folder) - os.system("cp -r --preserve %s/apps_settings/. /etc/yunohost/apps/" % backup_folder) + os.system( + "cp -r --preserve %s/apps_settings/. /etc/yunohost/apps/" + % backup_folder + ) os.system("systemctl start slapd") os.system("rm -r " + backup_folder) logger.info(m18n.n("migration_0019_rollback_success")) diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py index 93ece21fc..b3e8b8636 100644 --- a/src/yunohost/diagnosis.py +++ b/src/yunohost/diagnosis.py @@ -30,15 +30,20 @@ import time from moulinette import m18n, msettings from moulinette.utils import log -from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml +from moulinette.utils.filesystem import ( + read_json, + write_to_json, + read_yaml, + write_to_yaml, +) from yunohost.utils.error import YunohostError from yunohost.hook import hook_list, hook_exec -logger = log.getActionLogger('yunohost.diagnosis') +logger = log.getActionLogger("yunohost.diagnosis") DIAGNOSIS_CACHE = "/var/cache/yunohost/diagnosis/" -DIAGNOSIS_CONFIG_FILE = '/etc/yunohost/diagnosis.yml' +DIAGNOSIS_CONFIG_FILE = "/etc/yunohost/diagnosis.yml" DIAGNOSIS_SERVER = "diagnosis.yunohost.org" @@ -54,11 +59,13 @@ def diagnosis_get(category, item): all_categories_names = [c for c, _ in all_categories] if category not in all_categories_names: - raise YunohostError('diagnosis_unknown_categories', categories=category) + raise YunohostError("diagnosis_unknown_categories", categories=category) if isinstance(item, list): if any("=" not in criteria for criteria in item): - raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)") + raise YunohostError( + "Criterias should be of the form key=value (e.g. domain=yolo.test)" + ) # Convert the provided criteria into a nice dict item = {c.split("=")[0]: c.split("=")[1] for c in item} @@ -66,7 +73,9 @@ def diagnosis_get(category, item): return Diagnoser.get_cached_report(category, item=item) -def diagnosis_show(categories=[], issues=False, full=False, share=False, human_readable=False): +def diagnosis_show( + categories=[], issues=False, full=False, share=False, human_readable=False +): if not os.path.exists(DIAGNOSIS_CACHE): logger.warning(m18n.n("diagnosis_never_ran_yet")) @@ -82,7 +91,9 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r else: unknown_categories = [c for c in categories if c not in all_categories_names] if unknown_categories: - raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories)) + raise YunohostError( + "diagnosis_unknown_categories", categories=", ".join(unknown_categories) + ) # Fetch all reports all_reports = [] @@ -107,7 +118,11 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r if "data" in item: del item["data"] if issues: - report["items"] = [item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]] + report["items"] = [ + item + for item in report["items"] + if item["status"] in ["WARNING", "ERROR"] + ] # Ignore this category if no issue was found if not report["items"]: continue @@ -116,11 +131,12 @@ def diagnosis_show(categories=[], issues=False, full=False, share=False, human_r if share: from yunohost.utils.yunopaste import yunopaste + content = _dump_human_readable_reports(all_reports) url = yunopaste(content) logger.info(m18n.n("log_available_on_yunopaste", url=url)) - if msettings.get('interface') == 'api': + if msettings.get("interface") == "api": return {"url": url} else: return @@ -145,10 +161,12 @@ def _dump_human_readable_reports(reports): output += "\n" output += "\n\n" - return(output) + return output -def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, email=False): +def diagnosis_run( + categories=[], force=False, except_if_never_ran_yet=False, email=False +): if (email or except_if_never_ran_yet) and not os.path.exists(DIAGNOSIS_CACHE): return @@ -163,7 +181,9 @@ def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, ema else: unknown_categories = [c for c in categories if c not in all_categories_names] if unknown_categories: - raise YunohostError('diagnosis_unknown_categories', categories=", ".join(unknown_categories)) + raise YunohostError( + "diagnosis_unknown_categories", categories=", ".join(unknown_categories) + ) issues = [] # Call the hook ... @@ -176,11 +196,24 @@ def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, ema code, report = hook_exec(path, args={"force": force}, env=None) except Exception: import traceback - logger.error(m18n.n("diagnosis_failed_for_category", category=category, error='\n' + traceback.format_exc())) + + logger.error( + m18n.n( + "diagnosis_failed_for_category", + category=category, + error="\n" + traceback.format_exc(), + ) + ) else: diagnosed_categories.append(category) if report != {}: - issues.extend([item for item in report["items"] if item["status"] in ["WARNING", "ERROR"]]) + issues.extend( + [ + item + for item in report["items"] + if item["status"] in ["WARNING", "ERROR"] + ] + ) if email: _email_diagnosis_issues() @@ -237,12 +270,16 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False): # Sanity checks for the provided arguments if len(filter_) == 0: - raise YunohostError("You should provide at least one criteria being the diagnosis category to ignore") + raise YunohostError( + "You should provide at least one criteria being the diagnosis category to ignore" + ) category = filter_[0] if category not in all_categories_names: raise YunohostError("%s is not a diagnosis category" % category) if any("=" not in criteria for criteria in filter_[1:]): - raise YunohostError("Criterias should be of the form key=value (e.g. domain=yolo.test)") + raise YunohostError( + "Criterias should be of the form key=value (e.g. domain=yolo.test)" + ) # Convert the provided criteria into a nice dict criterias = {c.split("=")[0]: c.split("=")[1] for c in filter_[1:]} @@ -254,11 +291,18 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False): category, criterias = validate_filter_criterias(add_filter) # Fetch current issues for the requested category - current_issues_for_this_category = diagnosis_show(categories=[category], issues=True, full=True) - current_issues_for_this_category = current_issues_for_this_category["reports"][0].get("items", {}) + current_issues_for_this_category = diagnosis_show( + categories=[category], issues=True, full=True + ) + current_issues_for_this_category = current_issues_for_this_category["reports"][ + 0 + ].get("items", {}) # Accept the given filter only if the criteria effectively match an existing issue - if not any(issue_matches_criterias(i, criterias) for i in current_issues_for_this_category): + if not any( + issue_matches_criterias(i, criterias) + for i in current_issues_for_this_category + ): raise YunohostError("No issues was found matching the given criteria.") # Make sure the subdicts/lists exists @@ -332,7 +376,9 @@ def add_ignore_flag_to_issues(report): every item in the report """ - ignore_filters = _diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], []) + ignore_filters = ( + _diagnosis_read_configuration().get("ignore_filters", {}).get(report["id"], []) + ) for report_item in report["items"]: report_item["ignored"] = False @@ -347,8 +393,7 @@ def add_ignore_flag_to_issues(report): ############################################################ -class Diagnoser(): - +class Diagnoser: def __init__(self, args, env, loggers): # FIXME ? That stuff with custom loggers is weird ... (mainly inherited from the bash hooks, idk) @@ -371,9 +416,14 @@ class Diagnoser(): def diagnose(self): - if not self.args.get("force", False) and self.cached_time_ago() < self.cache_duration: + if ( + not self.args.get("force", False) + and self.cached_time_ago() < self.cache_duration + ): self.logger_debug("Cache still valid : %s" % self.cache_file) - logger.info(m18n.n("diagnosis_cache_still_valid", category=self.description)) + logger.info( + m18n.n("diagnosis_cache_still_valid", category=self.description) + ) return 0, {} for dependency in self.dependencies: @@ -382,10 +432,18 @@ class Diagnoser(): if dep_report["timestamp"] == -1: # No cache yet for this dep dep_errors = True else: - dep_errors = [item for item in dep_report["items"] if item["status"] == "ERROR"] + dep_errors = [ + item for item in dep_report["items"] if item["status"] == "ERROR" + ] if dep_errors: - logger.error(m18n.n("diagnosis_cant_run_because_of_dep", category=self.description, dep=Diagnoser.get_description(dependency))) + logger.error( + m18n.n( + "diagnosis_cant_run_because_of_dep", + category=self.description, + dep=Diagnoser.get_description(dependency), + ) + ) return 1, {} items = list(self.run()) @@ -394,29 +452,76 @@ class Diagnoser(): if "details" in item and not item["details"]: del item["details"] - new_report = {"id": self.id_, - "cached_for": self.cache_duration, - "items": items} + new_report = {"id": self.id_, "cached_for": self.cache_duration, "items": items} self.logger_debug("Updating cache %s" % self.cache_file) self.write_cache(new_report) Diagnoser.i18n(new_report) add_ignore_flag_to_issues(new_report) - errors = [item for item in new_report["items"] if item["status"] == "ERROR" and not item["ignored"]] - warnings = [item for item in new_report["items"] if item["status"] == "WARNING" and not item["ignored"]] - errors_ignored = [item for item in new_report["items"] if item["status"] == "ERROR" and item["ignored"]] - warning_ignored = [item for item in new_report["items"] if item["status"] == "WARNING" and item["ignored"]] - ignored_msg = " " + m18n.n("diagnosis_ignored_issues", nb_ignored=len(errors_ignored + warning_ignored)) if errors_ignored or warning_ignored else "" + errors = [ + item + for item in new_report["items"] + if item["status"] == "ERROR" and not item["ignored"] + ] + warnings = [ + item + for item in new_report["items"] + if item["status"] == "WARNING" and not item["ignored"] + ] + errors_ignored = [ + item + for item in new_report["items"] + if item["status"] == "ERROR" and item["ignored"] + ] + warning_ignored = [ + item + for item in new_report["items"] + if item["status"] == "WARNING" and item["ignored"] + ] + ignored_msg = ( + " " + + m18n.n( + "diagnosis_ignored_issues", + nb_ignored=len(errors_ignored + warning_ignored), + ) + if errors_ignored or warning_ignored + else "" + ) if errors and warnings: - logger.error(m18n.n("diagnosis_found_errors_and_warnings", errors=len(errors), warnings=len(warnings), category=new_report["description"]) + ignored_msg) + logger.error( + m18n.n( + "diagnosis_found_errors_and_warnings", + errors=len(errors), + warnings=len(warnings), + category=new_report["description"], + ) + + ignored_msg + ) elif errors: - logger.error(m18n.n("diagnosis_found_errors", errors=len(errors), category=new_report["description"]) + ignored_msg) + logger.error( + m18n.n( + "diagnosis_found_errors", + errors=len(errors), + category=new_report["description"], + ) + + ignored_msg + ) elif warnings: - logger.warning(m18n.n("diagnosis_found_warnings", warnings=len(warnings), category=new_report["description"]) + ignored_msg) + logger.warning( + m18n.n( + "diagnosis_found_warnings", + warnings=len(warnings), + category=new_report["description"], + ) + + ignored_msg + ) else: - logger.success(m18n.n("diagnosis_everything_ok", category=new_report["description"]) + ignored_msg) + logger.success( + m18n.n("diagnosis_everything_ok", category=new_report["description"]) + + ignored_msg + ) return 0, new_report @@ -430,10 +535,7 @@ class Diagnoser(): if not os.path.exists(cache_file): if warn_if_no_cache: logger.warning(m18n.n("diagnosis_no_cache", category=id_)) - report = {"id": id_, - "cached_for": -1, - "timestamp": -1, - "items": []} + report = {"id": id_, "cached_for": -1, "timestamp": -1, "items": []} else: report = read_json(cache_file) report["timestamp"] = int(os.path.getmtime(cache_file)) @@ -476,7 +578,7 @@ class Diagnoser(): meta_data = item.get("meta", {}).copy() meta_data.update(item.get("data", {})) - html_tags = re.compile(r'<[^>]+>') + html_tags = re.compile(r"<[^>]+>") def m18n_(info): if not isinstance(info, tuple) and not isinstance(info, list): @@ -486,11 +588,15 @@ class Diagnoser(): # In cli, we remove the html tags if msettings.get("interface") != "api" or force_remove_html_tags: s = s.replace("", "'").replace("", "'") - s = html_tags.sub('', s.replace("
", "\n")) + s = html_tags.sub("", s.replace("
", "\n")) else: - s = s.replace("", "").replace("", "") + s = s.replace("", "").replace( + "", "" + ) # Make it so that links open in new tabs - s = s.replace("URL: %s
Status code: %s" % (url, r.status_code)) + raise Exception( + "The remote diagnosis server failed miserably while trying to diagnose your server. This is most likely an error on Yunohost's infrastructure and not on your side. Please contact the YunoHost team an provide them with the following information.
URL: %s
Status code: %s" + % (url, r.status_code) + ) if r.status_code == 400: raise Exception("Diagnosis request was refused: %s" % r.content) try: r = r.json() except Exception as e: - raise Exception("Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" % (e, r.content)) + raise Exception( + "Failed to parse json from diagnosis server response.\nError: %s\nOriginal content: %s" + % (e, r.content) + ) return r @@ -558,6 +668,7 @@ def _list_diagnosis_categories(): def _email_diagnosis_issues(): from yunohost.domain import _get_maindomain + maindomain = _get_maindomain() from_ = "diagnosis@%s (Automatic diagnosis on %s)" % (maindomain, maindomain) to_ = "root" @@ -581,9 +692,16 @@ Subject: %s --- %s -""" % (from_, to_, subject_, disclaimer, content) +""" % ( + from_, + to_, + subject_, + disclaimer, + content, + ) import smtplib + smtp = smtplib.SMTP("localhost") smtp.sendmail(from_, [to_], message) smtp.quit() diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 6477b3943..920000ab0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -32,13 +32,18 @@ from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file -from yunohost.app import app_ssowatconf, _installed_apps, _get_app_settings, _get_conflicting_apps +from yunohost.app import ( + app_ssowatconf, + _installed_apps, + _get_app_settings, + _get_conflicting_apps, +) from yunohost.regenconf import regen_conf, _force_clear_hashes, _process_regen_conf from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation from yunohost.hook import hook_callback -logger = getActionLogger('yunohost.domain') +logger = getActionLogger("yunohost.domain") def domain_list(exclude_subdomains=False): @@ -52,7 +57,12 @@ def domain_list(exclude_subdomains=False): from yunohost.utils.ldap import _get_ldap_interface ldap = _get_ldap_interface() - result = [entry['virtualdomain'][0] for entry in ldap.search('ou=domains,dc=yunohost,dc=org', 'virtualdomain=*', ['virtualdomain'])] + result = [ + entry["virtualdomain"][0] + for entry in ldap.search( + "ou=domains,dc=yunohost,dc=org", "virtualdomain=*", ["virtualdomain"] + ) + ] result_list = [] for domain in result: @@ -66,17 +76,14 @@ def domain_list(exclude_subdomains=False): def cmp_domain(domain): # Keep the main part of the domain and the extension together # eg: this.is.an.example.com -> ['example.com', 'an', 'is', 'this'] - domain = domain.split('.') + domain = domain.split(".") domain[-1] = domain[-2] + domain.pop() domain = list(reversed(domain)) return domain result_list = sorted(result_list, key=cmp_domain) - return { - 'domains': result_list, - 'main': _get_maindomain() - } + return {"domains": result_list, "main": _get_maindomain()} @is_unit_operation() @@ -99,9 +106,9 @@ def domain_add(operation_logger, domain, dyndns=False): ldap = _get_ldap_interface() try: - ldap.validate_uniqueness({'virtualdomain': domain}) + ldap.validate_uniqueness({"virtualdomain": domain}) except MoulinetteError: - raise YunohostError('domain_exists') + raise YunohostError("domain_exists") operation_logger.start() @@ -113,35 +120,36 @@ def domain_add(operation_logger, domain, dyndns=False): if dyndns: # Do not allow to subscribe to multiple dyndns domains... - if os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise YunohostError('domain_dyndns_already_subscribed') + if os.path.exists("/etc/cron.d/yunohost-dyndns"): + raise YunohostError("domain_dyndns_already_subscribed") from yunohost.dyndns import dyndns_subscribe, _dyndns_provides # 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 YunohostError('domain_dyndns_root_unknown') + raise YunohostError("domain_dyndns_root_unknown") # Actually subscribe dyndns_subscribe(domain=domain) try: import yunohost.certificate + yunohost.certificate._certificate_install_selfsigned([domain], False) attr_dict = { - 'objectClass': ['mailDomain', 'top'], - 'virtualdomain': domain, + "objectClass": ["mailDomain", "top"], + "virtualdomain": domain, } try: - ldap.add('virtualdomain=%s,ou=domains' % domain, attr_dict) + ldap.add("virtualdomain=%s,ou=domains" % domain, attr_dict) except Exception as e: - raise YunohostError('domain_creation_failed', domain=domain, error=e) + raise YunohostError("domain_creation_failed", domain=domain, error=e) # Don't regen these conf if we're still in postinstall - if os.path.exists('/etc/yunohost/installed'): + if os.path.exists("/etc/yunohost/installed"): # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... # There are a few ideas why this happens (like backup/restore nginx @@ -153,7 +161,7 @@ def domain_add(operation_logger, domain, dyndns=False): # because it's one of the major service, but in the long term we # should identify the root of this bug... _force_clear_hashes(["/etc/nginx/conf.d/%s.conf" % domain]) - regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd']) + regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix", "rspamd"]) app_ssowatconf() except Exception: @@ -164,9 +172,9 @@ def domain_add(operation_logger, domain, dyndns=False): pass raise - hook_callback('post_domain_add', args=[domain]) + hook_callback("post_domain_add", args=[domain]) - logger.success(m18n.n('domain_created')) + logger.success(m18n.n("domain_created")) @is_unit_operation() @@ -183,8 +191,8 @@ def domain_remove(operation_logger, domain, force=False): from yunohost.app import app_ssowatconf, app_info from yunohost.utils.ldap import _get_ldap_interface - if not force and domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + if not force and domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) # Check domain is not the main domain if domain == _get_maindomain(): @@ -192,10 +200,13 @@ def domain_remove(operation_logger, domain, force=False): other_domains.remove(domain) if other_domains: - raise YunohostError('domain_cannot_remove_main', - domain=domain, other_domains="\n * " + ("\n * ".join(other_domains))) + raise YunohostError( + "domain_cannot_remove_main", + domain=domain, + other_domains="\n * " + ("\n * ".join(other_domains)), + ) else: - raise YunohostError('domain_cannot_remove_main_add_new_one', domain=domain) + raise YunohostError("domain_cannot_remove_main_add_new_one", domain=domain) # Check if apps are installed on the domain apps_on_that_domain = [] @@ -204,19 +215,25 @@ def domain_remove(operation_logger, domain, force=False): settings = _get_app_settings(app) label = app_info(app)["name"] if settings.get("domain") == domain: - apps_on_that_domain.append(" - %s \"%s\" on https://%s%s" % (app, label, domain, settings["path"]) if "path" in settings else app) + apps_on_that_domain.append( + ' - %s "%s" on https://%s%s' % (app, label, domain, settings["path"]) + if "path" in settings + else app + ) if apps_on_that_domain: - raise YunohostError('domain_uninstall_app_first', apps="\n".join(apps_on_that_domain)) + raise YunohostError( + "domain_uninstall_app_first", apps="\n".join(apps_on_that_domain) + ) operation_logger.start() ldap = _get_ldap_interface() try: - ldap.remove('virtualdomain=' + domain + ',ou=domains') + ldap.remove("virtualdomain=" + domain + ",ou=domains") except Exception as e: - raise YunohostError('domain_deletion_failed', domain=domain, error=e) + raise YunohostError("domain_deletion_failed", domain=domain, error=e) - os.system('rm -rf /etc/yunohost/certs/%s' % domain) + os.system("rm -rf /etc/yunohost/certs/%s" % domain) # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... @@ -235,14 +252,16 @@ def domain_remove(operation_logger, domain, force=False): # catastrophic consequences of nginx breaking because it can't load the # cert file which disappeared etc.. if os.path.exists("/etc/nginx/conf.d/%s.conf" % domain): - _process_regen_conf("/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True) + _process_regen_conf( + "/etc/nginx/conf.d/%s.conf" % domain, new_conf=None, save=True + ) - regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) + regen_conf(names=["nginx", "metronome", "dnsmasq", "postfix"]) app_ssowatconf() - hook_callback('post_domain_remove', args=[domain]) + hook_callback("post_domain_remove", args=[domain]) - logger.success(m18n.n('domain_deleted')) + logger.success(m18n.n("domain_deleted")) def domain_dns_conf(domain, ttl=None): @@ -255,8 +274,8 @@ def domain_dns_conf(domain, ttl=None): """ - if domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + if domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) ttl = 3600 if ttl is None else ttl @@ -290,7 +309,7 @@ def domain_dns_conf(domain, ttl=None): for record in record_list: result += "\n{name} {ttl} IN {type} {value}".format(**record) - if msettings.get('interface') == 'cli': + if msettings.get("interface") == "cli": logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) return result @@ -309,47 +328,58 @@ def domain_main_domain(operation_logger, new_main_domain=None): # If no new domain specified, we return the current main domain if not new_main_domain: - return {'current_main_domain': _get_maindomain()} + return {"current_main_domain": _get_maindomain()} # Check domain exists - if new_main_domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=new_main_domain) + if new_main_domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=new_main_domain) - operation_logger.related_to.append(('domain', new_main_domain)) + operation_logger.related_to.append(("domain", new_main_domain)) operation_logger.start() # Apply changes to ssl certs try: - write_to_file('/etc/yunohost/current_host', new_main_domain) + write_to_file("/etc/yunohost/current_host", new_main_domain) _set_hostname(new_main_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) - raise YunohostError('main_domain_change_failed') + raise YunohostError("main_domain_change_failed") # Generate SSOwat configuration file app_ssowatconf() # Regen configurations - if os.path.exists('/etc/yunohost/installed'): + if os.path.exists("/etc/yunohost/installed"): regen_conf() - logger.success(m18n.n('main_domain_changed')) + logger.success(m18n.n("main_domain_changed")) def domain_cert_status(domain_list, full=False): import yunohost.certificate + return yunohost.certificate.certificate_status(domain_list, full) -def domain_cert_install(domain_list, force=False, no_checks=False, self_signed=False, staging=False): +def domain_cert_install( + domain_list, force=False, no_checks=False, self_signed=False, staging=False +): import yunohost.certificate - return yunohost.certificate.certificate_install(domain_list, force, no_checks, self_signed, staging) + + return yunohost.certificate.certificate_install( + domain_list, force, no_checks, self_signed, staging + ) -def domain_cert_renew(domain_list, force=False, no_checks=False, email=False, staging=False): +def domain_cert_renew( + domain_list, force=False, no_checks=False, email=False, staging=False +): import yunohost.certificate - return yunohost.certificate.certificate_renew(domain_list, force, no_checks, email, staging) + + return yunohost.certificate.certificate_renew( + domain_list, force, no_checks, email, staging + ) def domain_url_available(domain, path): @@ -365,7 +395,7 @@ def domain_url_available(domain, path): def _get_maindomain(): - with open('/etc/yunohost/current_host', 'r') as f: + with open("/etc/yunohost/current_host", "r") as f: maindomain = f.readline().rstrip() return maindomain @@ -478,10 +508,22 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): #################### records = { - "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], - "extra": [{"name": name, "ttl": ttl_, "type": type_, "value": value} for name, ttl_, type_, value in extra], + "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 + ], + "extra": [ + {"name": name, "ttl": ttl_, "type": type_, "value": value} + for name, ttl_, type_, value in extra + ], } ################## @@ -490,7 +532,7 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): # Defined by custom hooks ships in apps for example ... - hook_results = hook_callback('custom_dns_rules', args=[domain]) + hook_results = hook_callback("custom_dns_rules", args=[domain]) for hook_name, results in hook_results.items(): # # There can be multiple results per hook name, so results look like @@ -506,18 +548,28 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): # [...] # # Loop over the sub-results - custom_records = [v['stdreturn'] for v in results.values() - if v and v['stdreturn']] + custom_records = [ + v["stdreturn"] for v in results.values() if v and v["stdreturn"] + ] records[hook_name] = [] for record_list in custom_records: # Check that record_list is indeed a list of dict # with the required keys - if not isinstance(record_list, list) \ - or any(not isinstance(record, dict) for record in record_list) \ - or any(key not in record for record in record_list for key in ["name", "ttl", "type", "value"]): + if ( + not isinstance(record_list, list) + or any(not isinstance(record, dict) for record in record_list) + or any( + key not in record + for record in record_list + for key in ["name", "ttl", "type", "value"] + ) + ): # Display an error, mainly for app packagers trying to implement a hook - logger.warning("Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" % (hook_name, record_list)) + logger.warning( + "Ignored custom record from hook '%s' because the data is not a *list* of dict with keys name, ttl, type and value. Raw data : %s" + % (hook_name, record_list) + ) continue records[hook_name].extend(record_list) @@ -526,7 +578,7 @@ def _build_dns_conf(domain, ttl=3600, include_empty_AAAA_if_no_ipv6=False): def _get_DKIM(domain): - DKIM_file = '/etc/dkim/{domain}.mail.txt'.format(domain=domain) + DKIM_file = "/etc/dkim/{domain}.mail.txt".format(domain=domain) if not os.path.isfile(DKIM_file): return (None, None) @@ -552,19 +604,27 @@ def _get_DKIM(domain): # Legacy DKIM format if is_legacy_format: - dkim = re.match(( - r'^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+' - '[^"]*"v=(?P[^";]+);' - r'[\s"]*k=(?P[^";]+);' - '[\s"]*p=(?P

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

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

[^";]+)' + ), + dkim_content, + re.M | re.S, ) if not dkim: @@ -572,16 +632,18 @@ def _get_DKIM(domain): 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')) + 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("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"), + ), ) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 44bdb0706..50fa8cce1 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -41,16 +41,14 @@ from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip, dig from yunohost.log import is_unit_operation -logger = getActionLogger('yunohost.dyndns') +logger = getActionLogger("yunohost.dyndns") -DYNDNS_ZONE = '/etc/yunohost/dyndns/zone' +DYNDNS_ZONE = "/etc/yunohost/dyndns/zone" -RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( - r'.*/K(?P[^\s\+]+)\.\+157.+\.private$' -) +RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(r".*/K(?P[^\s\+]+)\.\+157.+\.private$") RE_DYNDNS_PRIVATE_KEY_SHA512 = re.compile( - r'.*/K(?P[^\s\+]+)\.\+165.+\.private$' + r".*/K(?P[^\s\+]+)\.\+165.+\.private$" ) @@ -71,13 +69,15 @@ def _dyndns_provides(provider, domain): 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) + dyndomains = download_json("https://%s/domains" % provider, timeout=30) except MoulinetteError as e: logger.error(str(e)) - raise YunohostError('dyndns_could_not_check_provide', domain=domain, provider=provider) + raise YunohostError( + "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:]) + dyndomain = ".".join(domain.split(".")[1:]) return dyndomain in dyndomains @@ -93,22 +93,25 @@ def _dyndns_available(provider, domain): Returns: True if the domain is available, False otherwise. """ - logger.debug("Checking if domain %s is available on %s ..." - % (domain, provider)) + 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) + r = download_json( + "https://%s/test/%s" % (provider, domain), expected_status_code=None + ) except MoulinetteError as e: logger.error(str(e)) - raise YunohostError('dyndns_could_not_check_available', - domain=domain, provider=provider) + raise YunohostError( + "dyndns_could_not_check_available", domain=domain, provider=provider + ) return r == "Domain %s is available" % domain @is_unit_operation() -def dyndns_subscribe(operation_logger, 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 @@ -118,64 +121,87 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom subscribe_host -- Dynette HTTP API to subscribe to """ - if len(glob.glob('/etc/yunohost/dyndns/*.key')) != 0 or os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise YunohostError('domain_dyndns_already_subscribed') + if len(glob.glob("/etc/yunohost/dyndns/*.key")) != 0 or os.path.exists( + "/etc/cron.d/yunohost-dyndns" + ): + raise YunohostError("domain_dyndns_already_subscribed") if domain is None: domain = _get_maindomain() - operation_logger.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): - raise YunohostError('dyndns_domain_not_provided', domain=domain, provider=subscribe_host) + raise YunohostError( + "dyndns_domain_not_provided", domain=domain, provider=subscribe_host + ) # Verify if domain is available if not _dyndns_available(subscribe_host, domain): - raise YunohostError('dyndns_unavailable', domain=domain) + raise YunohostError("dyndns_unavailable", domain=domain) operation_logger.start() if key is None: - if len(glob.glob('/etc/yunohost/dyndns/*.key')) == 0: - if not os.path.exists('/etc/yunohost/dyndns'): - os.makedirs('/etc/yunohost/dyndns') + if len(glob.glob("/etc/yunohost/dyndns/*.key")) == 0: + if not os.path.exists("/etc/yunohost/dyndns"): + os.makedirs("/etc/yunohost/dyndns") - logger.debug(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) - os.system('chmod 600 /etc/yunohost/dyndns/*.key /etc/yunohost/dyndns/*.private') + 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" + ) - private_file = glob.glob('/etc/yunohost/dyndns/*%s*.private' % domain)[0] - key_file = glob.glob('/etc/yunohost/dyndns/*%s*.key' % domain)[0] + private_file = glob.glob("/etc/yunohost/dyndns/*%s*.private" % domain)[0] + key_file = glob.glob("/etc/yunohost/dyndns/*%s*.key" % domain)[0] with open(key_file) as f: - key = f.readline().strip().split(' ', 6)[-1] + 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) + r = requests.post( + "https://%s/key/%s?key_algo=hmac-sha512" + % (subscribe_host, base64.b64encode(key)), + data={"subdomain": domain}, + timeout=30, + ) except Exception as e: os.system("rm -f %s" % private_file) os.system("rm -f %s" % key_file) - raise YunohostError('dyndns_registration_failed', error=str(e)) + raise YunohostError("dyndns_registration_failed", error=str(e)) if r.status_code != 201: os.system("rm -f %s" % private_file) os.system("rm -f %s" % key_file) try: - error = json.loads(r.text)['error'] + error = json.loads(r.text)["error"] except: - error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text) - raise YunohostError('dyndns_registration_failed', error=error) + error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text) + raise YunohostError("dyndns_registration_failed", error=error) - logger.success(m18n.n('dyndns_registered')) + logger.success(m18n.n("dyndns_registered")) dyndns_installcron() @is_unit_operation() -def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key=None, - ipv4=None, ipv6=None, force=False, dry_run=False): +def dyndns_update( + operation_logger, + dyn_host="dyndns.yunohost.org", + domain=None, + key=None, + ipv4=None, + ipv6=None, + force=False, + dry_run=False, +): """ Update IP on DynDNS platform @@ -197,25 +223,24 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, # If key is not given, pick the first file we find with the domain given else: if key is None: - keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) + keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) if not keys: - raise YunohostError('dyndns_key_not_found') + raise YunohostError("dyndns_key_not_found") key = keys[0] # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' - host = domain.split('.')[1:] - host = '.'.join(host) + host = domain.split(".")[1:] + host = ".".join(host) logger.debug("Building zone update file ...") lines = [ - 'server %s' % dyn_host, - 'zone %s' % host, + "server %s" % dyn_host, + "zone %s" % host, ] - def resolve_domain(domain, rdtype): # FIXME make this work for IPv6-only hosts too.. @@ -223,12 +248,15 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, dyn_host_ip = result[0] if ok == "ok" and len(result) else None if not dyn_host_ip: raise YunohostError("Failed to resolve %s" % dyn_host) - + ok, result = dig(domain, rdtype, resolvers=[dyn_host_ip]) if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": - logger.debug("Timed-out while trying to resolve %s record for %s using %s" % (rdtype, domain, dyn_host)) + logger.debug( + "Timed-out while trying to resolve %s record for %s using %s" + % (rdtype, domain, dyn_host) + ) else: return None @@ -237,11 +265,16 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, if ok == "ok": return result[0] if len(result) else None elif result[0] == "Timeout": - logger.debug("Timed-out while trying to resolve %s record for %s using external resolvers : %s" % (rdtype, domain, result)) + logger.debug( + "Timed-out while trying to resolve %s record for %s using external resolvers : %s" + % (rdtype, domain, result) + ) else: return None - raise YunohostError("Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True) + raise YunohostError( + "Failed to resolve %s for %s" % (rdtype, domain), raw_msg=True + ) old_ipv4 = resolve_domain(domain, "A") old_ipv6 = resolve_domain(domain, "AAAA") @@ -264,7 +297,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, logger.info("No updated needed.") return else: - operation_logger.related_to.append(('domain', domain)) + operation_logger.related_to.append(("domain", domain)) operation_logger.start() logger.info("Updated needed, going on...") @@ -297,18 +330,17 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, record["value"] = domain record["value"] = record["value"].replace(";", r"\;") - action = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record) + action = "update add {name}.{domain}. {ttl} {type} {value}".format( + domain=domain, **record + ) action = action.replace(" @.", " ") lines.append(action) - lines += [ - 'show', - 'send' - ] + lines += ["show", "send"] # 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)) + write_to_file(DYNDNS_ZONE, "\n".join(lines)) logger.debug("Now pushing new conf to DynDNS host...") @@ -317,13 +349,15 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] subprocess.check_call(command) except subprocess.CalledProcessError: - raise YunohostError('dyndns_ip_update_failed') + raise YunohostError("dyndns_ip_update_failed") - logger.success(m18n.n('dyndns_ip_updated')) + logger.success(m18n.n("dyndns_ip_updated")) else: print(read_file(DYNDNS_ZONE)) print("") - print("Warning: dry run, this is only the generated config, it won't be applied") + print( + "Warning: dry run, this is only the generated config, it won't be applied" + ) def dyndns_installcron(): @@ -332,10 +366,10 @@ def dyndns_installcron(): """ - with open('/etc/cron.d/yunohost-dyndns', 'w+') as f: - f.write('*/2 * * * * root yunohost dyndns update >> /dev/null\n') + with open("/etc/cron.d/yunohost-dyndns", "w+") as f: + f.write("*/2 * * * * root yunohost dyndns update >> /dev/null\n") - logger.success(m18n.n('dyndns_cron_installed')) + logger.success(m18n.n("dyndns_cron_installed")) def dyndns_removecron(): @@ -347,9 +381,9 @@ def dyndns_removecron(): try: os.remove("/etc/cron.d/yunohost-dyndns") except Exception as e: - raise YunohostError('dyndns_cron_remove_failed', error=e) + raise YunohostError("dyndns_cron_remove_failed", error=e) - logger.success(m18n.n('dyndns_cron_removed')) + logger.success(m18n.n("dyndns_cron_removed")) def _guess_current_dyndns_domain(dyn_host): @@ -362,14 +396,14 @@ def _guess_current_dyndns_domain(dyn_host): """ # Retrieve the first registered domain - paths = list(glob.iglob('/etc/yunohost/dyndns/K*.private')) + paths = list(glob.iglob("/etc/yunohost/dyndns/K*.private")) for path in paths: match = RE_DYNDNS_PRIVATE_KEY_MD5.match(path) if not match: match = RE_DYNDNS_PRIVATE_KEY_SHA512.match(path) if not match: continue - _domain = match.group('domain') + _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..) @@ -380,4 +414,4 @@ def _guess_current_dyndns_domain(dyn_host): else: return (_domain, path) - raise YunohostError('dyndns_no_domain_registered') + raise YunohostError("dyndns_no_domain_registered") diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 8c5272b69..f9b872c10 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -33,14 +33,15 @@ 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') +logger = getActionLogger("yunohost.firewall") -def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, - no_upnp=False, no_reload=False): +def firewall_allow( + protocol, port, ipv4_only=False, ipv6_only=False, no_upnp=False, no_reload=False +): """ Allow connections on a port @@ -56,20 +57,26 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, firewall = firewall_list(raw=True) # Validate port - if not isinstance(port, int) and ':' not in port: + if not isinstance(port, int) and ":" not in port: port = int(port) # Validate protocols - protocols = ['TCP', 'UDP'] - if protocol != 'Both' and protocol in protocols: - protocols = [protocol, ] + protocols = ["TCP", "UDP"] + if protocol != "Both" and protocol in protocols: + protocols = [ + protocol, + ] # Validate IP versions - ipvs = ['ipv4', 'ipv6'] + 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 @@ -78,12 +85,15 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, firewall[i][p].append(port) else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n('port_already_opened', port=port, ip_version=ipv)) + logger.warning(m18n.n("port_already_opened", port=port, ip_version=ipv)) # Add port forwarding with UPnP - if not no_upnp and port not in firewall['uPnP'][p]: - firewall['uPnP'][p].append(port) - if firewall['uPnP'][p + "_TO_CLOSE"] and port in firewall['uPnP'][p + "_TO_CLOSE"]: - firewall['uPnP'][p + "_TO_CLOSE"].remove(port) + if not no_upnp and port not in firewall["uPnP"][p]: + firewall["uPnP"][p].append(port) + if ( + firewall["uPnP"][p + "_TO_CLOSE"] + and port in firewall["uPnP"][p + "_TO_CLOSE"] + ): + firewall["uPnP"][p + "_TO_CLOSE"].remove(port) # Update and reload firewall _update_firewall_file(firewall) @@ -91,8 +101,9 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, return firewall_reload() -def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, - upnp_only=False, no_reload=False): +def firewall_disallow( + protocol, port, ipv4_only=False, ipv6_only=False, upnp_only=False, no_reload=False +): """ Disallow connections on a port @@ -108,24 +119,30 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, firewall = firewall_list(raw=True) # Validate port - if not isinstance(port, int) and ':' not in port: + if not isinstance(port, int) and ":" not in port: port = int(port) # Validate protocols - protocols = ['TCP', 'UDP'] - if protocol != 'Both' and protocol in protocols: - protocols = [protocol, ] + protocols = ["TCP", "UDP"] + if protocol != "Both" and protocol in protocols: + protocols = [ + protocol, + ] # Validate IP versions and UPnP - ipvs = ['ipv4', 'ipv6'] + ipvs = ["ipv4", "ipv6"] upnp = True if ipv4_only and ipv6_only: 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 = [] @@ -137,13 +154,13 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, firewall[i][p].remove(port) else: ipv = "IPv%s" % i[3] - logger.warning(m18n.n('port_already_closed', port=port, ip_version=ipv)) + logger.warning(m18n.n("port_already_closed", port=port, ip_version=ipv)) # Remove port forwarding with UPnP - if upnp and port in firewall['uPnP'][p]: - firewall['uPnP'][p].remove(port) - if not firewall['uPnP'][p + "_TO_CLOSE"]: - firewall['uPnP'][p + "_TO_CLOSE"] = [] - firewall['uPnP'][p + "_TO_CLOSE"].append(port) + if upnp and port in firewall["uPnP"][p]: + firewall["uPnP"][p].remove(port) + if not firewall["uPnP"][p + "_TO_CLOSE"]: + firewall["uPnP"][p + "_TO_CLOSE"] = [] + firewall["uPnP"][p + "_TO_CLOSE"].append(port) # Update and reload firewall _update_firewall_file(firewall) @@ -168,21 +185,22 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False): # Retrieve all ports for IPv4 and IPv6 ports = {} - for i in ['ipv4', 'ipv6']: + for i in ["ipv4", "ipv6"]: f = firewall[i] # Combine TCP and UDP ports - ports[i] = sorted(set(f['TCP']) | set(f['UDP'])) + ports[i] = sorted(set(f["TCP"]) | set(f["UDP"])) if not by_ip_version: # Combine IPv4 and IPv6 ports - ports = sorted(set(ports['ipv4']) | set(ports['ipv6'])) + ports = sorted(set(ports["ipv4"]) | set(ports["ipv6"])) # Format returned dict ret = {"opened_ports": ports} if list_forwarded: # Combine TCP and UDP forwarded ports - ret['forwarded_ports'] = sorted( - set(firewall['uPnP']['TCP']) | set(firewall['uPnP']['UDP'])) + ret["forwarded_ports"] = sorted( + set(firewall["uPnP"]["TCP"]) | set(firewall["uPnP"]["UDP"]) + ) return ret @@ -202,20 +220,22 @@ def firewall_reload(skip_upnp=False): # Check if SSH port is allowed ssh_port = _get_ssh_port() - if ssh_port not in firewall_list()['opened_ports']: - firewall_allow('TCP', ssh_port, no_reload=True) + if ssh_port not in firewall_list()["opened_ports"]: + firewall_allow("TCP", ssh_port, no_reload=True) # Retrieve firewall rules and UPnP status firewall = firewall_list(raw=True) - upnp = firewall_upnp()['enabled'] if not skip_upnp else False + upnp = firewall_upnp()["enabled"] if not skip_upnp else False # IPv4 try: process.check_output("iptables -w -L") except process.CalledProcessError as e: - logger.debug('iptables seems to be not available, it outputs:\n%s', - prependlines(e.output.rstrip(), '> ')) - logger.warning(m18n.n('iptables_unavailable')) + logger.debug( + "iptables seems to be not available, it outputs:\n%s", + prependlines(e.output.rstrip(), "> "), + ) + logger.warning(m18n.n("iptables_unavailable")) else: rules = [ "iptables -w -F", @@ -223,10 +243,12 @@ def firewall_reload(skip_upnp=False): "iptables -w -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT", ] # 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)))) + 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 += [ "iptables -w -A INPUT -i lo -j ACCEPT", "iptables -w -A INPUT -p icmp -j ACCEPT", @@ -242,9 +264,11 @@ def firewall_reload(skip_upnp=False): try: process.check_output("ip6tables -L") except process.CalledProcessError as e: - logger.debug('ip6tables seems to be not available, it outputs:\n%s', - prependlines(e.output.rstrip(), '> ')) - logger.warning(m18n.n('ip6tables_unavailable')) + logger.debug( + "ip6tables seems to be not available, it outputs:\n%s", + prependlines(e.output.rstrip(), "> "), + ) + logger.warning(m18n.n("ip6tables_unavailable")) else: rules = [ "ip6tables -w -F", @@ -252,10 +276,12 @@ def firewall_reload(skip_upnp=False): "ip6tables -w -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT", ] # 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)))) + 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 += [ "ip6tables -w -A INPUT -i lo -j ACCEPT", "ip6tables -w -A INPUT -p icmpv6 -j ACCEPT", @@ -268,10 +294,11 @@ def firewall_reload(skip_upnp=False): reloaded = True if not reloaded: - raise YunohostError('firewall_reload_failed') + raise YunohostError("firewall_reload_failed") - hook_callback('post_iptable_rules', - args=[upnp, os.path.exists("/proc/net/if_inet6")]) + hook_callback( + "post_iptable_rules", args=[upnp, os.path.exists("/proc/net/if_inet6")] + ) if upnp: # Refresh port forwarding with UPnP @@ -280,13 +307,13 @@ def firewall_reload(skip_upnp=False): _run_service_command("reload", "fail2ban") if errors: - logger.warning(m18n.n('firewall_rules_cmd_failed')) + logger.warning(m18n.n("firewall_rules_cmd_failed")) else: - logger.success(m18n.n('firewall_reloaded')) + logger.success(m18n.n("firewall_reloaded")) return firewall_list() -def firewall_upnp(action='status', no_refresh=False): +def firewall_upnp(action="status", no_refresh=False): """ Manage port forwarding using UPnP @@ -300,44 +327,46 @@ def firewall_upnp(action='status', no_refresh=False): """ firewall = firewall_list(raw=True) - enabled = firewall['uPnP']['enabled'] + enabled = firewall["uPnP"]["enabled"] # Compatibility with previous version - if action == 'reload': + if action == "reload": logger.debug("'reload' action is deprecated and will be removed") try: # Remove old cron job - os.remove('/etc/cron.d/yunohost-firewall') + os.remove("/etc/cron.d/yunohost-firewall") except: pass - action = 'status' + action = "status" no_refresh = False - if action == 'status' and no_refresh: + if action == "status" and no_refresh: # Only return current state - return {'enabled': enabled} - elif action == 'enable' or (enabled and action == 'status'): + return {"enabled": enabled} + elif action == "enable" or (enabled and action == "status"): # Add cron job - with open(UPNP_CRON_JOB, 'w+') as f: - f.write('*/50 * * * * root ' - '/usr/bin/yunohost firewall upnp status >>/dev/null\n') + 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 - if 1900 not in firewall['ipv4']['UDP']: - firewall_allow('UDP', 1900, no_upnp=True, no_reload=True) + if 1900 not in firewall["ipv4"]["UDP"]: + firewall_allow("UDP", 1900, no_upnp=True, no_reload=True) if not enabled: firewall_reload(skip_upnp=True) enabled = True - elif action == 'disable' or (not enabled and action == 'status'): + elif action == "disable" or (not enabled and action == "status"): try: # Remove cron job os.remove(UPNP_CRON_JOB) except: pass enabled = False - if action == 'status': + if action == "status": no_refresh = True else: - raise YunohostError('action_invalid', action=action) + raise YunohostError("action_invalid", action=action) # Refresh port mapping using UPnP if not no_refresh: @@ -345,33 +374,33 @@ def firewall_upnp(action='status', no_refresh=False): upnpc.discoverdelay = 3000 # Discover UPnP device(s) - logger.debug('discovering UPnP devices...') + logger.debug("discovering UPnP devices...") nb_dev = upnpc.discover() - logger.debug('found %d UPnP device(s)', int(nb_dev)) + logger.debug("found %d UPnP device(s)", int(nb_dev)) if nb_dev < 1: - logger.error(m18n.n('upnp_dev_not_found')) + logger.error(m18n.n("upnp_dev_not_found")) enabled = False else: try: # Select UPnP device upnpc.selectigd() except: - logger.debug('unable to select UPnP device', exc_info=1) + logger.debug("unable to select UPnP device", exc_info=1) enabled = False else: # Iterate over ports - for protocol in ['TCP', 'UDP']: - if firewall['uPnP'][protocol + "_TO_CLOSE"]: - for port in firewall['uPnP'][protocol + "_TO_CLOSE"]: + for protocol in ["TCP", "UDP"]: + if firewall["uPnP"][protocol + "_TO_CLOSE"]: + for port in firewall["uPnP"][protocol + "_TO_CLOSE"]: # Clean the mapping of this port if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) except: pass - firewall['uPnP'][protocol + "_TO_CLOSE"] = [] + firewall["uPnP"][protocol + "_TO_CLOSE"] = [] - for port in firewall['uPnP'][protocol]: + for port in firewall["uPnP"][protocol]: # Clean the mapping of this port if upnpc.getspecificportmapping(port, protocol): try: @@ -382,40 +411,47 @@ def firewall_upnp(action='status', no_refresh=False): continue try: # Add new port mapping - upnpc.addportmapping(port, protocol, upnpc.lanaddr, - port, 'yunohost firewall: port %d' % port, '') + upnpc.addportmapping( + port, + protocol, + upnpc.lanaddr, + port, + "yunohost firewall: port %d" % port, + "", + ) except: - logger.debug('unable to add port %d using UPnP', - port, exc_info=1) + logger.debug( + "unable to add port %d using UPnP", port, exc_info=1 + ) enabled = False - + _update_firewall_file(firewall) - if enabled != firewall['uPnP']['enabled']: + if enabled != firewall["uPnP"]["enabled"]: firewall = firewall_list(raw=True) - firewall['uPnP']['enabled'] = enabled + firewall["uPnP"]["enabled"] = enabled _update_firewall_file(firewall) if not no_refresh: # Display success message if needed - if action == 'enable' and enabled: - logger.success(m18n.n('upnp_enabled')) - elif action == 'disable' and not enabled: - logger.success(m18n.n('upnp_disabled')) + if action == "enable" and enabled: + logger.success(m18n.n("upnp_enabled")) + elif action == "disable" and not enabled: + logger.success(m18n.n("upnp_disabled")) # Make sure to disable UPnP - elif action != 'disable' and not enabled: - firewall_upnp('disable', no_refresh=True) + elif action != "disable" and not enabled: + firewall_upnp("disable", no_refresh=True) - if not enabled and (action == 'enable' or 1900 in firewall['ipv4']['UDP']): + if not enabled and (action == "enable" or 1900 in firewall["ipv4"]["UDP"]): # Close unused port 1900 - firewall_disallow('UDP', 1900, no_reload=True) + firewall_disallow("UDP", 1900, no_reload=True) if not no_refresh: firewall_reload(skip_upnp=True) - if action == 'enable' and not enabled: - raise YunohostError('upnp_port_open_failed') - return {'enabled': enabled} + if action == "enable" and not enabled: + raise YunohostError("upnp_port_open_failed") + return {"enabled": enabled} def firewall_stop(): @@ -426,7 +462,7 @@ def firewall_stop(): """ if os.system("iptables -w -P INPUT ACCEPT") != 0: - raise YunohostError('iptables_unavailable') + raise YunohostError("iptables_unavailable") os.system("iptables -w -F") os.system("iptables -w -X") @@ -437,7 +473,7 @@ def firewall_stop(): os.system("ip6tables -X") if os.path.exists(UPNP_CRON_JOB): - firewall_upnp('disable') + firewall_upnp("disable") def _get_ssh_port(default=22): @@ -447,9 +483,9 @@ def _get_ssh_port(default=22): one if it's not defined. """ from moulinette.utils.text import searchf + try: - m = searchf(r'^Port[ \t]+([0-9]+)$', - '/etc/ssh/sshd_config', count=-1) + m = searchf(r"^Port[ \t]+([0-9]+)$", "/etc/ssh/sshd_config", count=-1) if m: return int(m) except: @@ -460,13 +496,17 @@ 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: + 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 - logger.debug('"%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 a243192e9..30f607455 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -36,10 +36,10 @@ from yunohost.utils.error import YunohostError from moulinette.utils import log from moulinette.utils.filesystem import read_json -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') +logger = log.getActionLogger("yunohost.hook") def hook_add(app, file): @@ -59,11 +59,11 @@ def hook_add(app, file): except OSError: os.makedirs(CUSTOM_HOOK_FOLDER + action) - finalpath = CUSTOM_HOOK_FOLDER + action + '/' + priority + '-' + app - os.system('cp %s %s' % (file, finalpath)) - os.system('chown -hR admin: %s' % HOOK_FOLDER) + 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): @@ -78,7 +78,7 @@ 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) + os.remove(CUSTOM_HOOK_FOLDER + action + "/" + script) except OSError: pass @@ -96,34 +96,36 @@ def hook_info(action, name): priorities = set() # Search in custom folder first - for h in iglob('{:s}{:s}/*-{:s}'.format( - CUSTOM_HOOK_FOLDER, action, name)): + for h in iglob("{:s}{:s}/*-{:s}".format(CUSTOM_HOOK_FOLDER, action, name)): priority, _ = _extract_filename_parts(os.path.basename(h)) priorities.add(priority) - hooks.append({ - 'priority': priority, - 'path': h, - }) + hooks.append( + { + "priority": priority, + "path": h, + } + ) # Append non-overwritten system hooks - for h in iglob('{:s}{:s}/*-{:s}'.format( - HOOK_FOLDER, action, name)): + for h in iglob("{:s}{:s}/*-{:s}".format(HOOK_FOLDER, action, name)): priority, _ = _extract_filename_parts(os.path.basename(h)) if priority not in priorities: - hooks.append({ - 'priority': priority, - 'path': h, - }) + hooks.append( + { + "priority": priority, + "path": h, + } + ) if not hooks: - raise YunohostError('hook_name_unknown', name=name) + raise YunohostError("hook_name_unknown", name=name) return { - 'action': action, - 'name': name, - 'hooks': hooks, + "action": action, + "name": name, + "hooks": hooks, } -def hook_list(action, list_by='name', show_info=False): +def hook_list(action, list_by="name", show_info=False): """ List available hooks for an action @@ -136,64 +138,75 @@ def hook_list(action, list_by='name', show_info=False): result = {} # Process the property to list hook by - if list_by == 'priority': + if list_by == "priority": if show_info: + 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} + else: + def _append_hook(d, priority, name, path): # Use the priority as key and the name as value try: d[priority].add(name) except KeyError: d[priority] = set([name]) - elif list_by == 'name' or list_by == 'folder': + + elif list_by == "name" or list_by == "folder": if show_info: + def _append_hook(d, priority, name, path): # Use the name as key and a list of hooks info - the # executed ones with this name - as value l = d.get(name, list()) for h in l: # Only one priority for the hook is accepted - if h['priority'] == priority: + if h["priority"] == priority: # Custom hooks overwrite system ones and they # are appended at the end - so overwite it - if h['path'] != path: - h['path'] = path + 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': + if list_by == "name": result = set() def _append_hook(d, priority, name, path): # Add only the name d.add(name) + else: - raise YunohostError('hook_list_by_invalid') + raise YunohostError("hook_list_by_invalid") def _append_folder(d, folder): # Iterate over and add hook from a folder for f in os.listdir(folder + action): - if f[0] == '.' or f[-1] == '~' or f.endswith(".pyc") \ - or (f.startswith("__") and f.endswith("__")): + if ( + f[0] == "." + or f[-1] == "~" + or f.endswith(".pyc") + or (f.startswith("__") and f.endswith("__")) + ): continue - path = '%s%s/%s' % (folder, action, f) + path = "%s%s/%s" % (folder, action, f) priority, name = _extract_filename_parts(f) _append_hook(d, priority, name, path) try: # Append system hooks first - if list_by == 'folder': - result['system'] = dict() if show_info else set() - _append_folder(result['system'], HOOK_FOLDER) + if list_by == "folder": + result["system"] = dict() if show_info else set() + _append_folder(result["system"], HOOK_FOLDER) else: _append_folder(result, HOOK_FOLDER) except OSError: @@ -201,19 +214,26 @@ def hook_list(action, list_by='name', show_info=False): try: # Append custom hooks - if list_by == 'folder': - result['custom'] = dict() if show_info else set() - _append_folder(result['custom'], CUSTOM_HOOK_FOLDER) + if list_by == "folder": + result["custom"] = dict() if show_info else set() + _append_folder(result["custom"], CUSTOM_HOOK_FOLDER) else: _append_folder(result, CUSTOM_HOOK_FOLDER) except OSError: pass - return {'hooks': result} + return {"hooks": result} -def hook_callback(action, hooks=[], args=None, chdir=None, - env=None, pre_callback=None, post_callback=None): +def hook_callback( + action, + hooks=[], + args=None, + chdir=None, + env=None, + pre_callback=None, + post_callback=None, +): """ Execute all scripts binded to an action @@ -235,11 +255,9 @@ def hook_callback(action, hooks=[], args=None, chdir=None, # Retrieve hooks if not hooks: - hooks_dict = hook_list(action, list_by='priority', - show_info=True)['hooks'] + hooks_dict = hook_list(action, list_by="priority", show_info=True)["hooks"] else: - hooks_names = hook_list(action, list_by='name', - show_info=True)['hooks'] + hooks_names = hook_list(action, list_by="name", show_info=True)["hooks"] # Add similar hooks to the list # For example: Having a 16-postfix hook in the list will execute a @@ -247,8 +265,7 @@ def hook_callback(action, hooks=[], args=None, chdir=None, all_hooks = [] for n in hooks: for key in hooks_names.keys(): - if key == n or key.startswith("%s_" % n) \ - and key not in all_hooks: + if key == n or key.startswith("%s_" % n) and key not in all_hooks: all_hooks.append(key) # Iterate over given hooks names list @@ -256,49 +273,55 @@ def hook_callback(action, hooks=[], args=None, chdir=None, try: hl = hooks_names[n] except KeyError: - raise YunohostError('hook_name_unknown', n) + raise YunohostError("hook_name_unknown", n) # Iterate over hooks with this name for h in hl: # Update hooks dict - d = hooks_dict.get(h['priority'], dict()) - d.update({n: {'path': h['path']}}) - hooks_dict[h['priority']] = d + d = hooks_dict.get(h["priority"], dict()) + d.update({n: {"path": h["path"]}}) + hooks_dict[h["priority"]] = d if not hooks_dict: return result # Validate callbacks if not callable(pre_callback): - def pre_callback(name, priority, path, args): return args + + def pre_callback(name, priority, path, args): + return args + if not callable(post_callback): - def post_callback(name, priority, path, succeed): return None + + def post_callback(name, priority, path, succeed): + return None # Iterate over hooks and execute them for priority in sorted(hooks_dict): for name, info in iter(hooks_dict[priority].items()): - state = 'succeed' - path = info['path'] + state = "succeed" + path = info["path"] try: - hook_args = pre_callback(name=name, priority=priority, - path=path, args=args) - hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env, - raise_on_error=True)[1] + hook_args = pre_callback( + name=name, priority=priority, path=path, args=args + ) + hook_return = hook_exec( + path, args=hook_args, chdir=chdir, env=env, raise_on_error=True + )[1] except YunohostError as e: - state = 'failed' + state = "failed" hook_return = {} logger.error(e.strerror, exc_info=1) - post_callback(name=name, priority=priority, path=path, - succeed=False) + post_callback(name=name, priority=priority, path=path, succeed=False) else: - post_callback(name=name, priority=priority, path=path, - succeed=True) + post_callback(name=name, priority=priority, path=path, succeed=True) if name not in result: result[name] = {} - result[name][path] = {'state': state, 'stdreturn': hook_return} + result[name][path] = {"state": state, "stdreturn": hook_return} return result -def hook_exec(path, args=None, raise_on_error=False, - chdir=None, env=None, return_format="json"): +def hook_exec( + path, args=None, raise_on_error=False, chdir=None, env=None, return_format="json" +): """ Execute hook from a file with arguments @@ -311,10 +334,10 @@ def hook_exec(path, args=None, raise_on_error=False, """ # Validate hook path - if path[0] != '/': + if path[0] != "/": path = os.path.realpath(path) if not os.path.isfile(path): - raise YunohostError('file_does_not_exist', path=path) + raise YunohostError("file_does_not_exist", path=path) def is_relevant_warning(msg): @@ -329,34 +352,38 @@ def hook_exec(path, args=None, raise_on_error=False, r"Creating config file .* with new version", r"Created symlink /etc/systemd", r"dpkg: warning: while removing .* not empty so not removed", - r"apt-key output should not be parsed" + r"apt-key output should not be parsed", ] return all(not re.search(w, msg) for w in irrelevant_warnings) # Define output loggers and call command loggers = ( lambda l: logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) if is_relevant_warning(l.rstrip()) else logger.debug(l.rstrip()), - lambda l: logger.info(l.rstrip()) + lambda l: logger.warning(l.rstrip()) + if is_relevant_warning(l.rstrip()) + else logger.debug(l.rstrip()), + lambda l: logger.info(l.rstrip()), ) # Check the type of the hook (bash by default) # For now we support only python and bash hooks. hook_type = mimetypes.MimeTypes().guess_type(path)[0] - if hook_type == 'text/x-python': + if hook_type == "text/x-python": returncode, returndata = _hook_exec_python(path, args, env, loggers) else: - returncode, returndata = _hook_exec_bash(path, args, chdir, env, return_format, loggers) + returncode, returndata = _hook_exec_bash( + path, args, chdir, env, return_format, loggers + ) # Check and return process' return code if returncode is None: if raise_on_error: - raise YunohostError('hook_exec_not_terminated', path=path) + raise YunohostError("hook_exec_not_terminated", path=path) else: - logger.error(m18n.n('hook_exec_not_terminated', path=path)) + logger.error(m18n.n("hook_exec_not_terminated", path=path)) return 1, {} elif raise_on_error and returncode != 0: - raise YunohostError('hook_exec_failed', path=path) + raise YunohostError("hook_exec_failed", path=path) return returncode, returndata @@ -366,31 +393,31 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): from moulinette.utils.process import call_async_output # Construct command variables - cmd_args = '' + cmd_args = "" if args and isinstance(args, list): # Concatenate escaped arguments - cmd_args = ' '.join(shell_quote(s) for s in args) + cmd_args = " ".join(shell_quote(s) for s in args) if not chdir: # use the script directory as current one chdir, cmd_script = os.path.split(path) - cmd_script = './{0}'.format(cmd_script) + cmd_script = "./{0}".format(cmd_script) else: cmd_script = path # Add Execution dir to environment var if env is None: env = {} - env['YNH_CWD'] = chdir + env["YNH_CWD"] = chdir - env['YNH_INTERFACE'] = msettings.get('interface') + env["YNH_INTERFACE"] = msettings.get("interface") stdreturn = os.path.join(tempfile.mkdtemp(), "stdreturn") - with open(stdreturn, 'w') as f: - f.write('') - env['YNH_STDRETURN'] = stdreturn + with open(stdreturn, "w") as f: + f.write("") + env["YNH_STDRETURN"] = stdreturn # use xtrace on fd 7 which is redirected to stdout - env['BASH_XTRACEFD'] = "7" + env["BASH_XTRACEFD"] = "7" cmd = '/bin/bash -x "{script}" {args} 7>&1' cmd = cmd.format(script=cmd_script, args=cmd_args) @@ -399,25 +426,25 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): _env = os.environ.copy() _env.update(env) - returncode = call_async_output( - cmd, loggers, shell=True, cwd=chdir, - env=_env - ) + returncode = call_async_output(cmd, loggers, shell=True, cwd=chdir, env=_env) raw_content = None try: - with open(stdreturn, 'r') as f: + with open(stdreturn, "r") as f: raw_content = f.read() returncontent = {} if return_format == "json": - if raw_content != '': + if raw_content != "": try: returncontent = read_json(stdreturn) except Exception as e: - raise YunohostError('hook_json_return_error', - path=path, msg=str(e), - raw_content=raw_content) + raise YunohostError( + "hook_json_return_error", + path=path, + msg=str(e), + raw_content=raw_content, + ) elif return_format == "plain_dict": for line in raw_content.split("\n"): @@ -426,7 +453,10 @@ def _hook_exec_bash(path, args, chdir, env, return_format, loggers): returncontent[key] = value else: - raise YunohostError("Expected value for return_format is either 'json' or 'plain_dict', got '%s'" % return_format) + raise YunohostError( + "Expected value for return_format is either 'json' or 'plain_dict', got '%s'" + % return_format + ) finally: stdreturndir = os.path.split(stdreturn)[0] os.remove(stdreturn) @@ -446,20 +476,21 @@ def _hook_exec_python(path, args, env, loggers): ret = module.main(args, env, loggers) # # Assert that the return is a (int, dict) tuple - assert isinstance(ret, tuple) \ - and len(ret) == 2 \ - and isinstance(ret[0], int) \ - and isinstance(ret[1], dict), \ - "Module %s did not return a (int, dict) tuple !" % module + assert ( + isinstance(ret, tuple) + and len(ret) == 2 + and isinstance(ret[0], int) + and isinstance(ret[1], dict) + ), ("Module %s did not return a (int, dict) tuple !" % module) return ret def _extract_filename_parts(filename): """Extract hook parts from filename""" - if '-' in filename: - priority, action = filename.split('-', 1) + if "-" in filename: + priority, action = filename.split("-", 1) else: - priority = '50' + priority = "50" action = filename # Remove extension if there's one @@ -469,7 +500,7 @@ def _extract_filename_parts(filename): # Taken from Python 3 shlex module -------------------------------------------- -_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.UNICODE).search +_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.UNICODE).search def shell_quote(s): diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 9159d4117..1489f109a 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -40,13 +40,13 @@ from yunohost.utils.packages import get_ynh_package_version from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml -CATEGORIES_PATH = '/var/log/yunohost/categories/' -OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' -METADATA_FILE_EXT = '.yml' -LOG_FILE_EXT = '.log' -RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user'] +CATEGORIES_PATH = "/var/log/yunohost/categories/" +OPERATIONS_PATH = "/var/log/yunohost/categories/operation/" +METADATA_FILE_EXT = ".yml" +LOG_FILE_EXT = ".log" +RELATED_CATEGORIES = ["app", "domain", "group", "service", "user"] -logger = getActionLogger('yunohost.log') +logger = getActionLogger("yunohost.log") def log_list(limit=None, with_details=False, with_suboperations=False): @@ -73,7 +73,7 @@ def log_list(limit=None, with_details=False, with_suboperations=False): for log in logs: - base_filename = log[:-len(METADATA_FILE_EXT)] + base_filename = log[: -len(METADATA_FILE_EXT)] md_path = os.path.join(OPERATIONS_PATH, log) entry = { @@ -88,10 +88,12 @@ def log_list(limit=None, with_details=False, with_suboperations=False): pass try: - metadata = read_yaml(md_path) or {} # Making sure this is a dict and not None..? + metadata = ( + read_yaml(md_path) or {} + ) # Making sure this is a dict and not None..? except Exception as e: # If we can't read the yaml for some reason, report an error and ignore this entry... - logger.error(m18n.n('log_corrupted_md_file', md_file=md_path, error=e)) + logger.error(m18n.n("log_corrupted_md_file", md_file=md_path, error=e)) continue if with_details: @@ -123,14 +125,16 @@ def log_list(limit=None, with_details=False, with_suboperations=False): operations = list(reversed(sorted(operations, key=lambda o: o["name"]))) # Reverse the order of log when in cli, more comfortable to read (avoid # unecessary scrolling) - is_api = msettings.get('interface') == 'api' + is_api = msettings.get("interface") == "api" if not is_api: operations = list(reversed(operations)) return {"operation": operations} -def log_show(path, number=None, share=False, filter_irrelevant=False, with_suboperations=False): +def log_show( + path, number=None, share=False, filter_irrelevant=False, with_suboperations=False +): """ Display a log file enriched with metadata if any. @@ -156,7 +160,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop r"args_array=.*$", r"local -A args_array$", r"ynh_handle_getopts_args", - r"ynh_script_progression" + r"ynh_script_progression", ] else: filters = [] @@ -169,14 +173,14 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop # Normalize log/metadata paths and filenames abs_path = path log_path = None - if not path.startswith('/'): + if not path.startswith("/"): abs_path = os.path.join(OPERATIONS_PATH, path) 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): - base_path = ''.join(os.path.splitext(abs_path)[:-1]) + base_path = "".join(os.path.splitext(abs_path)[:-1]) else: base_path = abs_path base_filename = os.path.basename(base_path) @@ -185,17 +189,18 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): - raise YunohostError('log_does_exists', log=path) + raise YunohostError("log_does_exists", log=path) infos = {} # If it's a unit operation, display the name and the description if base_path.startswith(CATEGORIES_PATH): infos["description"] = _get_description_from_name(base_filename) - infos['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) @@ -207,7 +212,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop url = yunopaste(content) logger.info(m18n.n("log_available_on_yunopaste", url=url)) - if msettings.get('interface') == 'api': + if msettings.get("interface") == "api": return {"url": url} else: return @@ -217,17 +222,17 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop try: metadata = read_yaml(md_path) except MoulinetteError as e: - error = m18n.n('log_corrupted_md_file', md_file=md_path, error=e) + error = m18n.n("log_corrupted_md_file", md_file=md_path, error=e) if os.path.exists(log_path): logger.warning(error) else: raise YunohostError(error) else: - infos['metadata_path'] = md_path - infos['metadata'] = metadata + infos["metadata_path"] = md_path + infos["metadata"] = metadata - if 'log_path' in metadata: - log_path = metadata['log_path'] + if "log_path" in metadata: + log_path = metadata["log_path"] if with_suboperations: @@ -248,19 +253,25 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop date = _get_datetime_from_name(base_filename) except ValueError: continue - if (date < log_start) or (date > log_start + timedelta(hours=48)): + if (date < log_start) or ( + date > log_start + timedelta(hours=48) + ): continue try: - submetadata = read_yaml(os.path.join(OPERATIONS_PATH, filename)) + submetadata = read_yaml( + os.path.join(OPERATIONS_PATH, filename) + ) except Exception: continue if submetadata and submetadata.get("parent") == base_filename: yield { - "name": filename[:-len(METADATA_FILE_EXT)], - "description": _get_description_from_name(filename[:-len(METADATA_FILE_EXT)]), - "success": submetadata.get("success", "?") + "name": filename[: -len(METADATA_FILE_EXT)], + "description": _get_description_from_name( + filename[: -len(METADATA_FILE_EXT)] + ), + "success": submetadata.get("success", "?"), } metadata["suboperations"] = list(suboperations()) @@ -268,6 +279,7 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop # Display logs if exist if os.path.exists(log_path): from yunohost.service import _tail + if number and filters: logs = _tail(log_path, int(number * 4)) elif number: @@ -277,17 +289,21 @@ def log_show(path, number=None, share=False, filter_irrelevant=False, with_subop logs = _filter_lines(logs, filters) if number: logs = logs[-number:] - infos['log_path'] = log_path - infos['logs'] = logs + infos["log_path"] = log_path + infos["logs"] = logs return infos + def log_share(path): return log_show(path, share=True) -def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], - exclude=['password'], operation_key=None): +def is_unit_operation( + entities=["app", "domain", "group", "service", "user"], + exclude=["password"], + operation_key=None, +): """ Configure quickly a unit operation @@ -309,6 +325,7 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], 'log_' is present in locales/en.json otherwise it won't be translatable. """ + def decorate(func): def func_wrapper(*args, **kwargs): op_key = operation_key @@ -322,9 +339,10 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], # 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 - if 'operation_logger' in keys: - keys.remove('operation_logger') + if "operation_logger" in keys: + keys.remove("operation_logger") for k, arg in enumerate(args): kwargs[keys[k]] = arg args = () @@ -364,12 +382,13 @@ def is_unit_operation(entities=['app', 'domain', 'group', 'service', 'user'], else: operation_logger.success() return result + return func_wrapper + return decorate class RedactingFormatter(Formatter): - def __init__(self, format_string, data_to_redact): super(RedactingFormatter, self).__init__(format_string) self.data_to_redact = data_to_redact @@ -389,11 +408,19 @@ class RedactingFormatter(Formatter): # This matches stuff like db_pwd=the_secret or admin_password=other_secret # (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=") # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest - match = re.search(r'(pwd|pass|password|secret|\w+key|token)=(\S{3,})$', record.strip()) - if match and match.group(2) not in self.data_to_redact and match.group(1) not in ["key", "manifest_key"]: + match = re.search( + r"(pwd|pass|password|secret|\w+key|token)=(\S{3,})$", record.strip() + ) + if ( + match + and match.group(2) not in self.data_to_redact + and match.group(1) not in ["key", "manifest_key"] + ): self.data_to_redact.append(match.group(2)) except Exception as e: - logger.warning("Failed to parse line to try to identify data to redact ... : %s" % e) + logger.warning( + "Failed to parse line to try to identify data to redact ... : %s" % e + ) class OperationLogger(object): @@ -462,13 +489,19 @@ class OperationLogger(object): # 4. if among those file, there's an operation log file, we use the id # of the most recent file - recent_operation_logs = sorted(glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getctime, reverse=True)[:20] + recent_operation_logs = sorted( + glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getctime, reverse=True + )[:20] proc = psutil.Process().parent() while proc is not None: # We use proc.open_files() to list files opened / actively used by this proc # We only keep files matching a recent yunohost operation log - active_logs = sorted([f.path for f in proc.open_files() if f.path in recent_operation_logs], key=os.path.getctime, reverse=True) + active_logs = sorted( + [f.path for f in proc.open_files() if f.path in recent_operation_logs], + key=os.path.getctime, + reverse=True, + ) if active_logs != []: # extra the log if from the full path return os.path.basename(active_logs[0])[:-4] @@ -514,10 +547,12 @@ class OperationLogger(object): # N.B. : the subtle thing here is that the class will remember a pointer to the list, # so we can directly append stuff to self.data_to_redact and that'll be automatically # propagated to the RedactingFormatter - self.file_handler.formatter = RedactingFormatter('%(asctime)s: %(levelname)s - %(message)s', self.data_to_redact) + self.file_handler.formatter = RedactingFormatter( + "%(asctime)s: %(levelname)s - %(message)s", self.data_to_redact + ) # Listen to the root logger - self.logger = getLogger('yunohost') + self.logger = getLogger("yunohost") self.logger.addHandler(self.file_handler) def flush(self): @@ -529,7 +564,7 @@ class OperationLogger(object): for data in self.data_to_redact: # N.B. : we need quotes here, otherwise yaml isn't happy about loading the yml later dump = dump.replace(data, "'**********'") - with open(self.md_path, 'w') as outfile: + with open(self.md_path, "w") as outfile: outfile.write(dump) @property @@ -553,7 +588,7 @@ class OperationLogger(object): # We use the name of the first related thing name.append(self.related_to[0][1]) - self._name = '-'.join(name) + self._name = "-".join(name) return self._name @property @@ -563,19 +598,19 @@ class OperationLogger(object): """ data = { - 'started_at': self.started_at, - 'operation': self.operation, - 'parent': self.parent, - 'yunohost_version': get_ynh_package_version("yunohost")["version"], - 'interface': msettings.get('interface'), + "started_at": self.started_at, + "operation": self.operation, + "parent": self.parent, + "yunohost_version": get_ynh_package_version("yunohost")["version"], + "interface": msettings.get("interface"), } if self.related_to is not None: - data['related_to'] = self.related_to + data["related_to"] = self.related_to if self.ended_at is not None: - data['ended_at'] = self.ended_at - data['success'] = self._success + data["ended_at"] = self.ended_at + data["success"] = self._success if self.error is not None: - data['error'] = self._error + data["error"] = self._error # TODO: detect if 'extra' erase some key of 'data' data.update(self.extra) return data @@ -608,21 +643,23 @@ class OperationLogger(object): self.logger.removeHandler(self.file_handler) self.file_handler.close() - is_api = msettings.get('interface') == 'api' + is_api = msettings.get("interface") == "api" desc = _get_description_from_name(self.name) if error is None: if is_api: - msg = m18n.n('log_link_to_log', name=self.name, desc=desc) + 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) + msg = m18n.n("log_help_to_get_log", name=self.name, desc=desc) logger.debug(msg) else: if is_api: - msg = "" + 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: - 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 @@ -636,7 +673,7 @@ class OperationLogger(object): if self.ended_at is not None or self.started_at is None: return else: - self.error(m18n.n('log_operation_unit_unclosed_properly')) + self.error(m18n.n("log_operation_unit_unclosed_properly")) def _get_datetime_from_name(name): diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 4db06e291..da0ae29ad 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -34,7 +34,7 @@ from moulinette.utils.log import getActionLogger from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation -logger = getActionLogger('yunohost.user') +logger = getActionLogger("yunohost.user") SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"] @@ -45,7 +45,9 @@ SYSTEM_PERMS = ["mail", "xmpp", "sftp", "ssh"] # -def user_permission_list(short=False, full=False, ignore_system_perms=False, absolute_urls=False): +def user_permission_list( + short=False, full=False, ignore_system_perms=False, absolute_urls=False +): """ List permissions and corresponding accesses """ @@ -53,32 +55,50 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs # Fetch relevant informations from yunohost.app import app_setting, _installed_apps from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract + ldap = _get_ldap_interface() - permissions_infos = ldap.search('ou=permission,dc=yunohost,dc=org', - '(objectclass=permissionYnh)', - ["cn", 'groupPermission', 'inheritPermission', - 'URL', 'additionalUrls', 'authHeader', 'label', 'showTile', 'isProtected']) + permissions_infos = ldap.search( + "ou=permission,dc=yunohost,dc=org", + "(objectclass=permissionYnh)", + [ + "cn", + "groupPermission", + "inheritPermission", + "URL", + "additionalUrls", + "authHeader", + "label", + "showTile", + "isProtected", + ], + ) # Parse / organize information to be outputed apps = sorted(_installed_apps()) - apps_base_path = {app: app_setting(app, 'domain') + app_setting(app, 'path') - for app in apps - if app_setting(app, 'domain') and app_setting(app, 'path')} + apps_base_path = { + app: app_setting(app, "domain") + app_setting(app, "path") + for app in apps + if app_setting(app, "domain") and app_setting(app, "path") + } permissions = {} for infos in permissions_infos: - name = infos['cn'][0] + name = infos["cn"][0] if ignore_system_perms and name.split(".")[0] in SYSTEM_PERMS: continue - app = name.split('.')[0] + app = name.split(".")[0] perm = {} - perm["allowed"] = [_ldap_path_extract(p, "cn") for p in infos.get('groupPermission', [])] + perm["allowed"] = [ + _ldap_path_extract(p, "cn") for p in infos.get("groupPermission", []) + ] if full: - perm["corresponding_users"] = [_ldap_path_extract(p, "uid") for p in infos.get('inheritPermission', [])] + perm["corresponding_users"] = [ + _ldap_path_extract(p, "uid") for p in infos.get("inheritPermission", []) + ] perm["auth_header"] = infos.get("authHeader", [False])[0] == "TRUE" perm["label"] = infos.get("label", [None])[0] perm["show_tile"] = infos.get("showTile", [False])[0] == "TRUE" @@ -87,19 +107,29 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs perm["additional_urls"] = infos.get("additionalUrls", []) if absolute_urls: - app_base_path = apps_base_path[app] if app in apps_base_path else "" # Meh in some situation where the app is currently installed/removed, this function may be called and we still need to act as if the corresponding permission indeed exists ... dunno if that's really the right way to proceed but okay. + app_base_path = ( + apps_base_path[app] if app in apps_base_path else "" + ) # Meh in some situation where the app is currently installed/removed, this function may be called and we still need to act as if the corresponding permission indeed exists ... dunno if that's really the right way to proceed but okay. perm["url"] = _get_absolute_url(perm["url"], app_base_path) - perm["additional_urls"] = [_get_absolute_url(url, app_base_path) for url in perm["additional_urls"]] + perm["additional_urls"] = [ + _get_absolute_url(url, app_base_path) + for url in perm["additional_urls"] + ] permissions[name] = perm # Make sure labels for sub-permissions are the form " Applabel (Sublabel) " if full: - subpermissions = {k: v for k, v in permissions.items() if not k.endswith(".main")} + subpermissions = { + k: v for k, v in permissions.items() if not k.endswith(".main") + } for name, infos in subpermissions.items(): main_perm_name = name.split(".")[0] + ".main" if main_perm_name not in permissions: - logger.debug("Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)" % main_perm_name) + logger.debug( + "Uhoh, unknown permission %s ? (Maybe we're in the process or deleting the perm for this app...)" + % main_perm_name + ) continue main_perm_label = permissions[main_perm_name]["label"] infos["sublabel"] = infos["label"] @@ -108,13 +138,21 @@ def user_permission_list(short=False, full=False, ignore_system_perms=False, abs if short: permissions = list(permissions.keys()) - return {'permissions': permissions} + return {"permissions": permissions} @is_unit_operation() -def user_permission_update(operation_logger, permission, add=None, remove=None, - label=None, show_tile=None, - protected=None, force=False, sync_perm=True): +def user_permission_update( + operation_logger, + permission, + add=None, + remove=None, + label=None, + show_tile=None, + protected=None, + force=False, + sync_perm=True, +): """ Allow or Disallow a user or group to a permission for a specific application @@ -137,43 +175,57 @@ def user_permission_update(operation_logger, permission, add=None, remove=None, # Refuse to add "visitors" to mail, xmpp ... they require an account to make sense. if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS: - raise YunohostError('permission_require_account', permission=permission) + raise YunohostError("permission_require_account", permission=permission) # Refuse to add "visitors" to protected permission - if ((add and "visitors" in add and existing_permission["protected"]) or - (remove and "visitors" in remove and existing_permission["protected"])) and not force: - raise YunohostError('permission_protected', permission=permission) + if ( + (add and "visitors" in add and existing_permission["protected"]) + or (remove and "visitors" in remove and existing_permission["protected"]) + ) and not force: + raise YunohostError("permission_protected", permission=permission) # Fetch currently allowed groups for this permission current_allowed_groups = existing_permission["allowed"] - operation_logger.related_to.append(('app', permission.split(".")[0])) + operation_logger.related_to.append(("app", permission.split(".")[0])) # Compute new allowed group list (and make sure what we're doing make sense) new_allowed_groups = copy.copy(current_allowed_groups) - all_existing_groups = user_group_list()['groups'].keys() + all_existing_groups = user_group_list()["groups"].keys() if add: groups_to_add = [add] if not isinstance(add, list) else add for group in groups_to_add: if group not in all_existing_groups: - raise YunohostError('group_unknown', group=group) + raise YunohostError("group_unknown", group=group) if group in current_allowed_groups: - logger.warning(m18n.n('permission_already_allowed', permission=permission, group=group)) + logger.warning( + m18n.n( + "permission_already_allowed", permission=permission, group=group + ) + ) else: - operation_logger.related_to.append(('group', group)) + operation_logger.related_to.append(("group", group)) new_allowed_groups += [group] if remove: groups_to_remove = [remove] if not isinstance(remove, list) else remove for group in groups_to_remove: if group not in current_allowed_groups: - logger.warning(m18n.n('permission_already_disallowed', permission=permission, group=group)) + logger.warning( + m18n.n( + "permission_already_disallowed", + permission=permission, + group=group, + ) + ) else: - operation_logger.related_to.append(('group', group)) + operation_logger.related_to.append(("group", group)) - new_allowed_groups = [g for g in new_allowed_groups if g not in groups_to_remove] + new_allowed_groups = [ + g for g in new_allowed_groups if g not in groups_to_remove + ] # If we end up with something like allowed groups is ["all_users", "volunteers"] # we shall warn the users that they should probably choose between one or @@ -191,17 +243,32 @@ def user_permission_update(operation_logger, permission, add=None, remove=None, else: show_tile = False - if existing_permission['url'] and existing_permission['url'].startswith('re:') and show_tile: - logger.warning(m18n.n('regex_incompatible_with_tile', regex=existing_permission['url'], permission=permission)) + if ( + existing_permission["url"] + and existing_permission["url"].startswith("re:") + and show_tile + ): + logger.warning( + m18n.n( + "regex_incompatible_with_tile", + regex=existing_permission["url"], + permission=permission, + ) + ) # Commit the new allowed group list operation_logger.start() - new_permission = _update_ldap_group_permission(permission=permission, allowed=new_allowed_groups, - label=label, show_tile=show_tile, - protected=protected, sync_perm=sync_perm) + new_permission = _update_ldap_group_permission( + permission=permission, + allowed=new_allowed_groups, + label=label, + show_tile=show_tile, + protected=protected, + sync_perm=sync_perm, + ) - logger.debug(m18n.n('permission_updated', permission=permission)) + logger.debug(m18n.n("permission_updated", permission=permission)) return new_permission @@ -229,12 +296,14 @@ def user_permission_reset(operation_logger, permission, sync_perm=True): # Update permission with default (all_users) - operation_logger.related_to.append(('app', permission.split(".")[0])) + operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() - new_permission = _update_ldap_group_permission(permission=permission, allowed="all_users", sync_perm=sync_perm) + new_permission = _update_ldap_group_permission( + permission=permission, allowed="all_users", sync_perm=sync_perm + ) - logger.debug(m18n.n('permission_updated', permission=permission)) + logger.debug(m18n.n("permission_updated", permission=permission)) return new_permission @@ -253,9 +322,11 @@ def user_permission_info(permission): # Fetch existing permission - existing_permission = user_permission_list(full=True)["permissions"].get(permission, None) + existing_permission = user_permission_list(full=True)["permissions"].get( + permission, None + ) if existing_permission is None: - raise YunohostError('permission_not_found', permission=permission) + raise YunohostError("permission_not_found", permission=permission) return existing_permission @@ -270,10 +341,18 @@ def user_permission_info(permission): @is_unit_operation() -def permission_create(operation_logger, permission, allowed=None, - url=None, additional_urls=None, auth_header=True, - label=None, show_tile=False, - protected=False, sync_perm=True): +def permission_create( + operation_logger, + permission, + allowed=None, + url=None, + additional_urls=None, + auth_header=True, + label=None, + show_tile=False, + protected=False, + sync_perm=True, +): """ Create a new permission for a specific application @@ -301,6 +380,7 @@ def permission_create(operation_logger, permission, allowed=None, from yunohost.utils.ldap import _get_ldap_interface from yunohost.user import user_group_list + ldap = _get_ldap_interface() # By default, manipulate main permission @@ -308,9 +388,10 @@ def permission_create(operation_logger, permission, allowed=None, permission = permission + ".main" # Validate uniqueness of permission in LDAP - if ldap.get_conflict({'cn': permission}, - base_dn='ou=permission,dc=yunohost,dc=org'): - raise YunohostError('permission_already_exist', permission=permission) + if ldap.get_conflict( + {"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org" + ): + raise YunohostError("permission_already_exist", permission=permission) # Get random GID all_gid = {x.gr_gid for x in grp.getgrall()} @@ -323,13 +404,19 @@ def permission_create(operation_logger, permission, allowed=None, app, subperm = permission.split(".") attr_dict = { - 'objectClass': ['top', 'permissionYnh', 'posixGroup'], - 'cn': str(permission), - 'gidNumber': gid, - 'authHeader': ['TRUE'], - 'label': [str(label) if label else (subperm if subperm != "main" else app.title())], - 'showTile': ['FALSE'], # Dummy value, it will be fixed when we call '_update_ldap_group_permission' - 'isProtected': ['FALSE'] # Dummy value, it will be fixed when we call '_update_ldap_group_permission' + "objectClass": ["top", "permissionYnh", "posixGroup"], + "cn": str(permission), + "gidNumber": gid, + "authHeader": ["TRUE"], + "label": [ + str(label) if label else (subperm if subperm != "main" else app.title()) + ], + "showTile": [ + "FALSE" + ], # Dummy value, it will be fixed when we call '_update_ldap_group_permission' + "isProtected": [ + "FALSE" + ], # Dummy value, it will be fixed when we call '_update_ldap_group_permission' } if allowed is not None: @@ -337,34 +424,53 @@ def permission_create(operation_logger, permission, allowed=None, allowed = [allowed] # Validate that the groups to add actually exist - all_existing_groups = user_group_list()['groups'].keys() + all_existing_groups = user_group_list()["groups"].keys() for group in allowed or []: if group not in all_existing_groups: - raise YunohostError('group_unknown', group=group) + raise YunohostError("group_unknown", group=group) - operation_logger.related_to.append(('app', permission.split(".")[0])) + operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: - ldap.add('cn=%s,ou=permission' % permission, attr_dict) + ldap.add("cn=%s,ou=permission" % permission, attr_dict) except Exception as e: - raise YunohostError('permission_creation_failed', permission=permission, error=e) + raise YunohostError( + "permission_creation_failed", permission=permission, error=e + ) - permission_url(permission, url=url, add_url=additional_urls, auth_header=auth_header, - sync_perm=False) + permission_url( + permission, + url=url, + add_url=additional_urls, + auth_header=auth_header, + sync_perm=False, + ) - new_permission = _update_ldap_group_permission(permission=permission, allowed=allowed, - label=label, show_tile=show_tile, - protected=protected, sync_perm=sync_perm) + new_permission = _update_ldap_group_permission( + permission=permission, + allowed=allowed, + label=label, + show_tile=show_tile, + protected=protected, + sync_perm=sync_perm, + ) - logger.debug(m18n.n('permission_created', permission=permission)) + logger.debug(m18n.n("permission_created", permission=permission)) return new_permission @is_unit_operation() -def permission_url(operation_logger, permission, - url=None, add_url=None, remove_url=None, auth_header=None, - clear_urls=False, sync_perm=True): +def permission_url( + operation_logger, + permission, + url=None, + add_url=None, + remove_url=None, + auth_header=None, + clear_urls=False, + sync_perm=True, +): """ Update urls related to a permission for a specific application @@ -378,19 +484,20 @@ def permission_url(operation_logger, permission, """ from yunohost.app import app_setting from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() # By default, manipulate main permission if "." not in permission: permission = permission + ".main" - app = permission.split('.')[0] + app = permission.split(".")[0] if url or add_url: - domain = app_setting(app, 'domain') - path = app_setting(app, 'path') + domain = app_setting(app, "domain") + path = app_setting(app, "path") if domain is None or path is None: - raise YunohostError('unknown_main_domain_path', app=app) + raise YunohostError("unknown_main_domain_path", app=app) else: app_main_path = domain + path @@ -398,15 +505,17 @@ def permission_url(operation_logger, permission, existing_permission = user_permission_info(permission) - show_tile = existing_permission['show_tile'] + show_tile = existing_permission["show_tile"] if url is None: url = existing_permission["url"] else: url = _validate_and_sanitize_permission_url(url, app_main_path, app) - if url.startswith('re:') and existing_permission['show_tile']: - logger.warning(m18n.n('regex_incompatible_with_tile', regex=url, permission=permission)) + if url.startswith("re:") and existing_permission["show_tile"]: + logger.warning( + m18n.n("regex_incompatible_with_tile", regex=url, permission=permission) + ) show_tile = False current_additional_urls = existing_permission["additional_urls"] @@ -415,7 +524,11 @@ def permission_url(operation_logger, permission, if add_url: for ur in add_url: if ur in current_additional_urls: - logger.warning(m18n.n('additional_urls_already_added', permission=permission, url=ur)) + logger.warning( + m18n.n( + "additional_urls_already_added", permission=permission, url=ur + ) + ) else: ur = _validate_and_sanitize_permission_url(ur, app_main_path, app) new_additional_urls += [ur] @@ -423,12 +536,16 @@ def permission_url(operation_logger, permission, if remove_url: for ur in remove_url: if ur not in current_additional_urls: - logger.warning(m18n.n('additional_urls_already_removed', permission=permission, url=ur)) + logger.warning( + m18n.n( + "additional_urls_already_removed", permission=permission, url=ur + ) + ) new_additional_urls = [u for u in new_additional_urls if u not in remove_url] if auth_header is None: - auth_header = existing_permission['auth_header'] + auth_header = existing_permission["auth_header"] if clear_urls: url = None @@ -440,21 +557,26 @@ def permission_url(operation_logger, permission, # Actually commit the change - operation_logger.related_to.append(('app', permission.split(".")[0])) + operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: - ldap.update('cn=%s,ou=permission' % permission, {'URL': [url] if url is not None else [], - 'additionalUrls': new_additional_urls, - 'authHeader': [str(auth_header).upper()], - 'showTile': [str(show_tile).upper()], }) + ldap.update( + "cn=%s,ou=permission" % permission, + { + "URL": [url] if url is not None else [], + "additionalUrls": new_additional_urls, + "authHeader": [str(auth_header).upper()], + "showTile": [str(show_tile).upper()], + }, + ) except Exception as e: - raise YunohostError('permission_update_failed', permission=permission, error=e) + raise YunohostError("permission_update_failed", permission=permission, error=e) if sync_perm: permission_sync_to_user() - logger.debug(m18n.n('permission_updated', permission=permission)) + logger.debug(m18n.n("permission_updated", permission=permission)) return user_permission_info(permission) @@ -472,9 +594,10 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True) permission = permission + ".main" if permission.endswith(".main") and not force: - raise YunohostError('permission_cannot_remove_main') + raise YunohostError("permission_cannot_remove_main") from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() # Make sure this permission exists @@ -483,17 +606,19 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True) # Actually delete the permission - operation_logger.related_to.append(('app', permission.split(".")[0])) + operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() try: - ldap.remove('cn=%s,ou=permission' % permission) + ldap.remove("cn=%s,ou=permission" % permission) except Exception as e: - raise YunohostError('permission_deletion_failed', permission=permission, error=e) + raise YunohostError( + "permission_deletion_failed", permission=permission, error=e + ) if sync_perm: permission_sync_to_user() - logger.debug(m18n.n('permission_deleted', permission=permission)) + logger.debug(m18n.n("permission_deleted", permission=permission)) def permission_sync_to_user(): @@ -505,6 +630,7 @@ def permission_sync_to_user(): from yunohost.app import app_ssowatconf from yunohost.user import user_group_list from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() groups = user_group_list(full=True)["groups"] @@ -516,7 +642,13 @@ def permission_sync_to_user(): currently_allowed_users = set(permission_infos["corresponding_users"]) # These are the users that should be allowed because they are member of a group that is allowed for this permission ... - should_be_allowed_users = set([user for group in permission_infos["allowed"] for user in groups[group]["members"]]) + should_be_allowed_users = set( + [ + user + for group in permission_infos["allowed"] + for user in groups[group]["members"] + ] + ) # Note that a LDAP operation with the same value that is in LDAP crash SLAP. # So we need to check before each ldap operation that we really change something in LDAP @@ -524,47 +656,55 @@ def permission_sync_to_user(): # We're all good, this permission is already correctly synchronized ! continue - new_inherited_perms = {'inheritPermission': ["uid=%s,ou=users,dc=yunohost,dc=org" % u for u in should_be_allowed_users], - 'memberUid': should_be_allowed_users} + new_inherited_perms = { + "inheritPermission": [ + "uid=%s,ou=users,dc=yunohost,dc=org" % u + for u in should_be_allowed_users + ], + "memberUid": should_be_allowed_users, + } # Commit the change with the new inherited stuff try: - ldap.update('cn=%s,ou=permission' % permission_name, new_inherited_perms) + ldap.update("cn=%s,ou=permission" % permission_name, new_inherited_perms) except Exception as e: - raise YunohostError('permission_update_failed', permission=permission_name, error=e) + raise YunohostError( + "permission_update_failed", permission=permission_name, error=e + ) logger.debug("The permission database has been resynchronized") app_ssowatconf() # Reload unscd, otherwise the group ain't propagated to the LDAP database - os.system('nscd --invalidate=passwd') - os.system('nscd --invalidate=group') + os.system("nscd --invalidate=passwd") + os.system("nscd --invalidate=group") -def _update_ldap_group_permission(permission, allowed, - label=None, show_tile=None, - protected=None, sync_perm=True): +def _update_ldap_group_permission( + permission, allowed, label=None, show_tile=None, protected=None, sync_perm=True +): """ - Internal function that will rewrite user permission + Internal function that will rewrite user permission - permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) - allowed -- (optional) A list of group/user to allow for the permission - label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin - show_tile -- (optional) Define if a tile will be shown in the SSO - protected -- (optional) Define if the permission can be added/removed to the visitor group + permission -- Name of the permission (e.g. mail or nextcloud or wordpress.editors) + allowed -- (optional) A list of group/user to allow for the permission + label -- (optional) Define a name for the permission. This label will be shown on the SSO and in the admin + show_tile -- (optional) Define if a tile will be shown in the SSO + protected -- (optional) Define if the permission can be added/removed to the visitor group - Assumptions made, that should be checked before calling this function: - - the permission does currently exists ... - - the 'allowed' list argument is *different* from the current - permission state ... otherwise ldap will miserably fail in such - case... - - the 'allowed' list contains *existing* groups. + Assumptions made, that should be checked before calling this function: + - the permission does currently exists ... + - the 'allowed' list argument is *different* from the current + permission state ... otherwise ldap will miserably fail in such + case... + - the 'allowed' list contains *existing* groups. """ from yunohost.hook import hook_callback from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() existing_permission = user_permission_info(permission) @@ -575,7 +715,9 @@ def _update_ldap_group_permission(permission, allowed, allowed = [allowed] if not isinstance(allowed, list) else allowed # Guarantee uniqueness of values in allowed, which would otherwise make ldap.update angry. allowed = set(allowed) - update['groupPermission'] = ['cn=' + g + ',ou=groups,dc=yunohost,dc=org' for g in allowed] + update["groupPermission"] = [ + "cn=" + g + ",ou=groups,dc=yunohost,dc=org" for g in allowed + ] if label is not None: update["label"] = [str(label)] @@ -586,18 +728,25 @@ def _update_ldap_group_permission(permission, allowed, if show_tile is not None: if show_tile is True: - if not existing_permission['url']: - logger.warning(m18n.n('show_tile_cant_be_enabled_for_url_not_defined', permission=permission)) + if not existing_permission["url"]: + logger.warning( + m18n.n( + "show_tile_cant_be_enabled_for_url_not_defined", + permission=permission, + ) + ) show_tile = False - elif existing_permission['url'].startswith('re:'): - logger.warning(m18n.n('show_tile_cant_be_enabled_for_regex', permission=permission)) + elif existing_permission["url"].startswith("re:"): + logger.warning( + m18n.n("show_tile_cant_be_enabled_for_regex", permission=permission) + ) show_tile = False update["showTile"] = [str(show_tile).upper()] try: - ldap.update('cn=%s,ou=permission' % permission, update) + ldap.update("cn=%s,ou=permission" % permission, update) except Exception as e: - raise YunohostError('permission_update_failed', permission=permission, error=e) + raise YunohostError("permission_update_failed", permission=permission, error=e) # Trigger permission sync if asked @@ -620,13 +769,33 @@ def _update_ldap_group_permission(permission, allowed, effectively_added_users = new_corresponding_users - old_corresponding_users effectively_removed_users = old_corresponding_users - new_corresponding_users - effectively_added_group = new_allowed_users - old_allowed_users - effectively_added_users - effectively_removed_group = old_allowed_users - new_allowed_users - effectively_removed_users + effectively_added_group = ( + new_allowed_users - old_allowed_users - effectively_added_users + ) + effectively_removed_group = ( + old_allowed_users - new_allowed_users - effectively_removed_users + ) if effectively_added_users or effectively_added_group: - hook_callback('post_app_addaccess', args=[app, ','.join(effectively_added_users), sub_permission, ','.join(effectively_added_group)]) + hook_callback( + "post_app_addaccess", + args=[ + app, + ",".join(effectively_added_users), + sub_permission, + ",".join(effectively_added_group), + ], + ) if effectively_removed_users or effectively_removed_group: - hook_callback('post_app_removeaccess', args=[app, ','.join(effectively_removed_users), sub_permission, ','.join(effectively_removed_group)]) + hook_callback( + "post_app_removeaccess", + args=[ + app, + ",".join(effectively_removed_users), + sub_permission, + ",".join(effectively_removed_group), + ], + ) return new_permission @@ -642,10 +811,10 @@ def _get_absolute_url(url, base_path): base_path = base_path.rstrip("/") if url is None: return None - if url.startswith('/'): + if url.startswith("/"): return base_path + url.rstrip("/") - if url.startswith('re:/'): - return 're:' + base_path.replace('.', '\\.') + url[3:] + if url.startswith("re:/"): + return "re:" + base_path.replace(".", "\\.") + url[3:] else: return url @@ -676,43 +845,45 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): from yunohost.domain import domain_list from yunohost.app import _assert_no_conflicting_apps - domains = domain_list()['domains'] + domains = domain_list()["domains"] # # Regexes # def validate_regex(regex): - if '%' in regex: - logger.warning("/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead.") + if "%" in regex: + logger.warning( + "/!\\ Packagers! You are probably using a lua regex. You should use a PCRE regex instead." + ) return try: re.compile(regex) except Exception: - raise YunohostError('invalid_regex', regex=regex) + raise YunohostError("invalid_regex", regex=regex) - if url.startswith('re:'): + if url.startswith("re:"): # regex without domain # we check for the first char after 're:' - if url[3] in ['/', '^', '\\']: + if url[3] in ["/", "^", "\\"]: validate_regex(url[3:]) return url # regex with domain - if '/' not in url: - raise YunohostError('regex_with_only_domain') - domain, path = url[3:].split('/', 1) - path = '/' + path + if "/" not in url: + raise YunohostError("regex_with_only_domain") + domain, path = url[3:].split("/", 1) + path = "/" + path - if domain.replace('%', '').replace('\\', '') not in domains: - raise YunohostError('domain_name_unknown', domain=domain) + if domain.replace("%", "").replace("\\", "") not in domains: + raise YunohostError("domain_name_unknown", domain=domain) validate_regex(path) - return 're:' + domain + path + return "re:" + domain + path # # "Regular" URIs @@ -720,13 +891,13 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): def split_domain_path(url): url = url.strip("/") - (domain, path) = url.split('/', 1) if "/" in url else (url, "/") + (domain, path) = url.split("/", 1) if "/" in url else (url, "/") if path != "/": path = "/" + path return (domain, path) # uris without domain - if url.startswith('/'): + if url.startswith("/"): # if url is for example /admin/ # we want sanitized_url to be: /admin # and (domain, path) to be : (domain.tld, /app/admin) @@ -743,7 +914,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): sanitized_url = domain + path if domain not in domains: - raise YunohostError('domain_name_unknown', domain=domain) + raise YunohostError("domain_name_unknown", domain=domain) _assert_no_conflicting_apps(domain, path, ignore_app=app) diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py index 9423b1b36..c99f34a1c 100644 --- a/src/yunohost/regenconf.py +++ b/src/yunohost/regenconf.py @@ -35,19 +35,25 @@ from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation from yunohost.hook import hook_callback, hook_list -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') -REGEN_CONF_FILE = '/etc/yunohost/regenconf.yml' +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") +REGEN_CONF_FILE = "/etc/yunohost/regenconf.yml" -logger = log.getActionLogger('yunohost.regenconf') +logger = log.getActionLogger("yunohost.regenconf") # FIXME : those ain't just services anymore ... what are we supposed to do with this ... # FIXME : check for all reference of 'service' close to operation_logger stuff -@is_unit_operation([('names', 'configuration')]) -def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False, - list_pending=False): +@is_unit_operation([("names", "configuration")]) +def regen_conf( + operation_logger, + names=[], + with_diff=False, + force=False, + dry_run=False, + list_pending=False, +): """ Regenerate the configuration file(s) @@ -73,19 +79,20 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run for system_path, pending_path in conf_files.items(): pending_conf[category][system_path] = { - 'pending_conf': pending_path, - 'diff': _get_files_diff( - system_path, pending_path, True), + "pending_conf": pending_path, + "diff": _get_files_diff(system_path, pending_path, True), } return pending_conf if not dry_run: - operation_logger.related_to = [('configuration', x) for x in names] + operation_logger.related_to = [("configuration", x) for x in names] if not names: - operation_logger.name_parameter_override = 'all' + operation_logger.name_parameter_override = "all" elif len(names) != 1: - operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_categories' + operation_logger.name_parameter_override = ( + str(len(operation_logger.related_to)) + "_categories" + ) operation_logger.start() # Clean pending conf directory @@ -94,8 +101,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run shutil.rmtree(PENDING_CONF_DIR, ignore_errors=True) else: for name in names: - shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), - ignore_errors=True) + shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) @@ -103,22 +109,25 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run 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 category category_pending_path = os.path.join(PENDING_CONF_DIR, name) - filesystem.mkdir(category_pending_path, 0o755, True, uid='root') + filesystem.mkdir(category_pending_path, 0o755, True, uid="root") # return the arguments to pass to the script - return pre_args + [category_pending_path, ] + return pre_args + [ + category_pending_path, + ] ssh_explicitly_specified = isinstance(names, list) and "ssh" in names # By default, we regen everything if not names: - names = hook_list('conf_regen', list_by='name', - show_info=False)['hooks'] + names = hook_list("conf_regen", list_by="name", show_info=False)["hooks"] # Dirty hack for legacy code : avoid attempting to regen the conf for # glances because it got removed ... This is only needed *once* @@ -134,6 +143,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run # hooks to avoid having to call "yunohost domain list" so many times which # ends up in wasted time (about 3~5 seconds per call on a RPi2) from yunohost.domain import domain_list + env = {} # Well we can only do domain_list() if postinstall is done ... # ... but hooks that effectively need the domain list are only @@ -142,18 +152,23 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run if os.path.exists("/etc/yunohost/installed"): env["YNH_DOMAINS"] = " ".join(domain_list()["domains"]) - pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call, env=env) + pre_result = hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) # Keep only the hook names with at least one success - names = [hook for hook, infos in pre_result.items() - if any(result["state"] == "succeed" for result in infos.values())] + names = [ + hook + for hook, infos in pre_result.items() + if any(result["state"] == "succeed" for result in infos.values()) + ] # FIXME : what do in case of partial success/failure ... if not names: - ret_failed = [hook for hook, infos in pre_result.items() - if any(result["state"] == "failed" for result in infos.values())] - raise YunohostError('regenconf_failed', - categories=', '.join(ret_failed)) + ret_failed = [ + hook + for hook, infos in pre_result.items() + if any(result["state"] == "failed" for result in infos.values()) + ] + raise YunohostError("regenconf_failed", categories=", ".join(ret_failed)) # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True @@ -163,12 +178,12 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run # Iterate over categories and process pending conf for category, conf_files in _get_pending_conf(names).items(): if not dry_run: - operation_logger.related_to.append(('configuration', category)) + operation_logger.related_to.append(("configuration", category)) if dry_run: - logger.debug(m18n.n('regenconf_pending_applying', category=category)) + logger.debug(m18n.n("regenconf_pending_applying", category=category)) else: - logger.debug(m18n.n('regenconf_dry_pending_applying', category=category)) + logger.debug(m18n.n("regenconf_dry_pending_applying", category=category)) conf_hashes = _get_conf_hashes(category) succeed_regen = {} @@ -184,7 +199,11 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run # hash of the pending configuration ... # That way, the file will later appear as manually modified. sshd_config = "/etc/ssh/sshd_config" - if category == "ssh" and sshd_config not in conf_hashes and sshd_config in conf_files: + if ( + category == "ssh" + and sshd_config not in conf_hashes + and sshd_config in conf_files + ): conf_hashes[sshd_config] = _calculate_hash(conf_files[sshd_config]) _update_conf_hashes(category, conf_hashes) @@ -227,17 +246,23 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run force_update_hashes_for_this_category = False for system_path, pending_path in conf_files.items(): - logger.debug("processing pending conf '%s' to system conf '%s'", - pending_path, system_path) + logger.debug( + "processing pending conf '%s' to system conf '%s'", + pending_path, + system_path, + ) conf_status = None regenerated = False # Get the diff between files - conf_diff = _get_files_diff( - system_path, pending_path, True) if with_diff else None + conf_diff = ( + _get_files_diff(system_path, pending_path, True) if with_diff else None + ) # Check if the conf must be removed - to_remove = True if pending_path and os.path.getsize(pending_path) == 0 else False + to_remove = ( + True if pending_path and os.path.getsize(pending_path) == 0 else False + ) # Retrieve and calculate hashes system_hash = _calculate_hash(system_path) @@ -251,7 +276,7 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run if not system_hash: logger.debug("> forgetting about stale file/hash") conf_hashes[system_path] = None - conf_status = 'forget-about-it' + conf_status = "forget-about-it" regenerated = True # Otherwise there's still a file on the system but it's not managed by # Yunohost anymore... But if user requested --force we shall @@ -259,13 +284,13 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run elif force: logger.debug("> force-remove stale file") regenerated = _regen(system_path) - conf_status = 'force-removed' + conf_status = "force-removed" # Otherwise, flag the file as manually modified else: - logger.warning(m18n.n( - 'regenconf_file_manually_modified', - conf=system_path)) - conf_status = 'modified' + logger.warning( + m18n.n("regenconf_file_manually_modified", conf=system_path) + ) + conf_status = "modified" # -> system conf does not exists elif not system_hash: @@ -273,56 +298,65 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run logger.debug("> system conf is already removed") os.remove(pending_path) conf_hashes[system_path] = None - conf_status = 'forget-about-it' + conf_status = "forget-about-it" force_update_hashes_for_this_category = True continue elif not saved_hash or force: if force: logger.debug("> system conf has been manually removed") - conf_status = 'force-created' + conf_status = "force-created" else: logger.debug("> system conf does not exist yet") - conf_status = 'created' - regenerated = _regen( - system_path, pending_path, save=False) + conf_status = "created" + regenerated = _regen(system_path, pending_path, save=False) else: - logger.info(m18n.n( - 'regenconf_file_manually_removed', - conf=system_path)) - conf_status = 'removed' + logger.info( + m18n.n("regenconf_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") if system_hash == new_hash: logger.debug("> no changes to system conf has been made") - conf_status = 'managed' + conf_status = "managed" regenerated = True 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.info(m18n.n('regenconf_now_managed_by_yunohost', - conf=system_path, category=category)) + logger.info( + m18n.n( + "regenconf_now_managed_by_yunohost", + conf=system_path, + category=category, + ) + ) regenerated = _regen(system_path, pending_path) - conf_status = 'new' + conf_status = "new" elif force: regenerated = _regen(system_path) - conf_status = 'force-removed' + conf_status = "force-removed" else: - logger.info(m18n.n('regenconf_file_kept_back', - conf=system_path, category=category)) - conf_status = 'unmanaged' + logger.info( + m18n.n( + "regenconf_file_kept_back", + conf=system_path, + category=category, + ) + ) + conf_status = "unmanaged" # -> system conf has not been manually modified elif system_hash == saved_hash: if to_remove: regenerated = _regen(system_path) - conf_status = 'removed' + conf_status = "removed" elif system_hash != new_hash: regenerated = _regen(system_path, pending_path) - conf_status = 'updated' + conf_status = "updated" else: logger.debug("> system conf is already up-to-date") os.remove(pending_path) @@ -332,24 +366,28 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run logger.debug("> system conf has been manually modified") if system_hash == new_hash: logger.debug("> new conf is as current system conf") - conf_status = 'managed' + conf_status = "managed" regenerated = True - elif force and system_path == sshd_config and not ssh_explicitly_specified: - logger.warning(m18n.n('regenconf_need_to_explicitly_specify_ssh')) - conf_status = 'modified' + elif ( + force + and system_path == sshd_config + and not ssh_explicitly_specified + ): + logger.warning(m18n.n("regenconf_need_to_explicitly_specify_ssh")) + conf_status = "modified" elif force: regenerated = _regen(system_path, pending_path) - conf_status = 'force-updated' + conf_status = "force-updated" else: - logger.warning(m18n.n( - 'regenconf_file_manually_modified', - conf=system_path)) - conf_status = 'modified' + logger.warning( + m18n.n("regenconf_file_manually_modified", conf=system_path) + ) + conf_status = "modified" # Store the result - conf_result = {'status': conf_status} + conf_result = {"status": conf_status} if conf_diff is not None: - conf_result['diff'] = conf_diff + conf_result["diff"] = conf_diff if regenerated: succeed_regen[system_path] = conf_result conf_hashes[system_path] = new_hash @@ -360,39 +398,40 @@ def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run # Check for category conf changes if not succeed_regen and not failed_regen: - logger.debug(m18n.n('regenconf_up_to_date', category=category)) + logger.debug(m18n.n("regenconf_up_to_date", category=category)) continue elif not failed_regen: if not dry_run: - logger.success(m18n.n('regenconf_updated', category=category)) + logger.success(m18n.n("regenconf_updated", category=category)) else: - logger.success(m18n.n('regenconf_would_be_updated', category=category)) + logger.success(m18n.n("regenconf_would_be_updated", category=category)) if (succeed_regen or force_update_hashes_for_this_category) and not dry_run: _update_conf_hashes(category, conf_hashes) # Append the category results - result[category] = { - 'applied': succeed_regen, - 'pending': failed_regen - } + result[category] = {"applied": succeed_regen, "pending": failed_regen} # Return in case of dry run if dry_run: 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 category - if name in result and result[name]['applied']: - regen_conf_files = ','.join(result[name]['applied'].keys()) + 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, ] + regen_conf_files = "" + return post_args + [ + regen_conf_files, + ] - hook_callback('conf_regen', names, pre_callback=_pre_call, env=env) + hook_callback("conf_regen", names, pre_callback=_pre_call, env=env) operation_logger.success() @@ -404,7 +443,7 @@ def _get_regenconf_infos(): Get a dict of regen conf informations """ try: - with open(REGEN_CONF_FILE, 'r') as f: + with open(REGEN_CONF_FILE, "r") as f: return yaml.load(f) except: return {} @@ -422,10 +461,12 @@ def _save_regenconf_infos(infos): del infos["glances"] try: - with open(REGEN_CONF_FILE, 'w') as f: + with open(REGEN_CONF_FILE, "w") as f: yaml.safe_dump(infos, f, default_flow_style=False) except Exception as e: - logger.warning('Error while saving regenconf infos, exception: %s', e, exc_info=1) + logger.warning( + "Error while saving regenconf infos, exception: %s", e, exc_info=1 + ) raise @@ -439,13 +480,13 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): """ if orig_file and os.path.exists(orig_file): - with open(orig_file, 'r') as orig_file: + with open(orig_file, "r") as orig_file: orig_file = orig_file.readlines() else: orig_file = [] if new_file and os.path.exists(new_file): - with open(new_file, 'r') as new_file: + with open(new_file, "r") as new_file: new_file = new_file.readlines() else: new_file = [] @@ -461,7 +502,7 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): pass if as_string: - return ''.join(diff).rstrip() + return "".join(diff).rstrip() return diff @@ -475,12 +516,14 @@ def _calculate_hash(path): hasher = hashlib.md5() try: - with open(path, 'rb') as f: + with open(path, "rb") as f: hasher.update(f.read()) return hasher.hexdigest() except IOError as e: - logger.warning("Error while calculating file '%s' hash: %s", path, e, exc_info=1) + logger.warning( + "Error while calculating file '%s' hash: %s", path, e, exc_info=1 + ) return None @@ -535,18 +578,17 @@ def _get_conf_hashes(category): logger.debug("category %s is not in categories.yml yet.", category) return {} - elif categories[category] is None or 'conffiles' not in categories[category]: + elif categories[category] is None or "conffiles" not in categories[category]: logger.debug("No configuration files for category %s.", category) return {} else: - return categories[category]['conffiles'] + return categories[category]["conffiles"] def _update_conf_hashes(category, hashes): """Update the registered conf hashes for a category""" - logger.debug("updating conf hashes for '%s' with: %s", - category, hashes) + logger.debug("updating conf hashes for '%s' with: %s", category, hashes) categories = _get_regenconf_infos() category_conf = categories.get(category, {}) @@ -559,9 +601,13 @@ def _update_conf_hashes(category, hashes): # that path. # It avoid keeping weird old entries like # /etc/nginx/conf.d/some.domain.that.got.removed.conf - hashes = {path: hash_ for path, hash_ in hashes.items() if hash_ is not None or os.path.exists(path)} + hashes = { + path: hash_ + for path, hash_ in hashes.items() + if hash_ is not None or os.path.exists(path) + } - category_conf['conffiles'] = hashes + category_conf["conffiles"] = hashes categories[category] = category_conf _save_regenconf_infos(categories) @@ -571,9 +617,12 @@ def _force_clear_hashes(paths): categories = _get_regenconf_infos() for path in paths: for category in categories.keys(): - if path in categories[category]['conffiles']: - logger.debug("force-clearing old conf hash for %s in category %s" % (path, category)) - del categories[category]['conffiles'][path] + if path in categories[category]["conffiles"]: + logger.debug( + "force-clearing old conf hash for %s in category %s" + % (path, category) + ) + del categories[category]["conffiles"][path] _save_regenconf_infos(categories) @@ -587,22 +636,26 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): """ if save: - backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format( - system_conf.lstrip('/'), datetime.utcnow().strftime("%Y%m%d.%H%M%S"))) + backup_path = os.path.join( + BACKUP_CONF_DIR, + "{0}-{1}".format( + system_conf.lstrip("/"), datetime.utcnow().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, 0o755, True) shutil.copy2(system_conf, backup_path) - logger.debug(m18n.n('regenconf_file_backed_up', - conf=system_conf, backup=backup_path)) + logger.debug( + m18n.n("regenconf_file_backed_up", conf=system_conf, backup=backup_path) + ) try: if not new_conf: os.remove(system_conf) - logger.debug(m18n.n('regenconf_file_removed', - conf=system_conf)) + logger.debug(m18n.n("regenconf_file_removed", conf=system_conf)) else: system_dir = os.path.dirname(system_conf) @@ -610,14 +663,18 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): filesystem.mkdir(system_dir, 0o755, True) shutil.copyfile(new_conf, system_conf) - logger.debug(m18n.n('regenconf_file_updated', - conf=system_conf)) + logger.debug(m18n.n("regenconf_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) + 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('regenconf_file_remove_failed', - conf=system_conf), - exc_info=1) + logger.warning( + m18n.n("regenconf_file_remove_failed", conf=system_conf), exc_info=1 + ) return False elif new_conf: @@ -630,9 +687,12 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): copy_succeed = False finally: if not copy_succeed: - logger.warning(m18n.n('regenconf_file_copy_failed', - conf=system_conf, new=new_conf), - exc_info=1) + logger.warning( + m18n.n( + "regenconf_file_copy_failed", conf=system_conf, new=new_conf + ), + exc_info=1, + ) return False return True @@ -651,13 +711,17 @@ def manually_modified_files(): return output -def manually_modified_files_compared_to_debian_default(ignore_handled_by_regenconf=False): +def manually_modified_files_compared_to_debian_default( + ignore_handled_by_regenconf=False, +): # from https://serverfault.com/a/90401 - files = check_output("dpkg-query -W -f='${Conffiles}\n' '*' \ + files = check_output( + "dpkg-query -W -f='${Conffiles}\n' '*' \ | awk 'OFS=\" \"{print $2,$1}' \ | md5sum -c 2>/dev/null \ - | awk -F': ' '$2 !~ /OK/{print $1}'") + | awk -F': ' '$2 !~ /OK/{print $1}'" + ) files = files.strip().split("\n") if ignore_handled_by_regenconf: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 347932add..00eeb1bd9 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -41,10 +41,20 @@ from moulinette.utils.filesystem import read_file, append_to_file, write_to_file MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" -logger = getActionLogger('yunohost.service') +logger = getActionLogger("yunohost.service") -def service_add(name, description=None, log=None, log_type=None, test_status=None, test_conf=None, needs_exposed_ports=None, need_lock=False, status=None): +def service_add( + name, + description=None, + log=None, + log_type=None, + test_status=None, + test_conf=None, + needs_exposed_ports=None, + need_lock=False, + status=None, +): """ Add a custom service @@ -69,12 +79,14 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non # Deprecated log_type stuff if log_type is not None: - logger.warning("/!\\ Packagers! --log_type is deprecated. You do not need to specify --log_type systemd anymore ... Yunohost now automatically fetch the journalctl of the systemd service by default.") + logger.warning( + "/!\\ Packagers! --log_type is deprecated. You do not need to specify --log_type systemd anymore ... Yunohost now automatically fetch the journalctl of the systemd service by default." + ) # Usually when adding such a service, the service name will be provided so we remove it as it's not a log file path if name in log: log.remove(name) - service['log'] = log + service["log"] = log if not description: # Try to get the description from systemd service @@ -87,12 +99,14 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non description = "" if description: - service['description'] = description + service["description"] = description else: - logger.warning("/!\\ Packagers! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services.") + logger.warning( + "/!\\ Packagers! You added a custom service without specifying a description. Please add a proper Description in the systemd configuration, or use --description to explain what the service does in a similar fashion to existing services." + ) if need_lock: - service['need_lock'] = True + service["need_lock"] = True if test_status: service["test_status"] = test_status @@ -101,7 +115,9 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non _, systemd_info = _get_service_information_from_systemd(name) type_ = systemd_info.get("Type") if systemd_info is not None else "" if type_ == "oneshot" and name != "postgresql": - logger.warning("/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not.") + logger.warning( + "/!\\ Packagers! Please provide a --test_status when adding oneshot-type services in Yunohost, such that it has a reliable way to check if the service is running or not." + ) if test_conf: service["test_conf"] = test_conf @@ -113,9 +129,9 @@ def service_add(name, description=None, log=None, log_type=None, test_status=Non _save_services(services) except Exception: # we'll get a logger.warning with more details in _save_services - raise YunohostError('service_add_failed', service=name) + raise YunohostError("service_add_failed", service=name) - logger.success(m18n.n('service_added', service=name)) + logger.success(m18n.n("service_added", service=name)) def service_remove(name): @@ -129,16 +145,16 @@ def service_remove(name): services = _get_services() if name not in services: - raise YunohostError('service_unknown', service=name) + raise YunohostError("service_unknown", service=name) del services[name] try: _save_services(services) except Exception: # we'll get a logger.warning with more details in _save_services - raise YunohostError('service_remove_failed', service=name) + raise YunohostError("service_remove_failed", service=name) - logger.success(m18n.n('service_removed', service=name)) + logger.success(m18n.n("service_removed", service=name)) def service_start(names): @@ -153,12 +169,16 @@ def service_start(names): names = [names] for name in names: - if _run_service_command('start', name): - logger.success(m18n.n('service_started', service=name)) + if _run_service_command("start", name): + logger.success(m18n.n("service_started", service=name)) else: - if service_status(name)['status'] != 'running': - raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name)) - logger.debug(m18n.n('service_already_started', service=name)) + if service_status(name)["status"] != "running": + raise YunohostError( + "service_start_failed", + service=name, + logs=_get_journalctl_logs(name), + ) + logger.debug(m18n.n("service_already_started", service=name)) def service_stop(names): @@ -172,12 +192,14 @@ def service_stop(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('stop', name): - logger.success(m18n.n('service_stopped', service=name)) + if _run_service_command("stop", name): + logger.success(m18n.n("service_stopped", service=name)) else: - if service_status(name)['status'] != 'inactive': - raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name)) - logger.debug(m18n.n('service_already_stopped', service=name)) + if service_status(name)["status"] != "inactive": + raise YunohostError( + "service_stop_failed", service=name, logs=_get_journalctl_logs(name) + ) + logger.debug(m18n.n("service_already_stopped", service=name)) def service_reload(names): @@ -191,11 +213,15 @@ def service_reload(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('reload', name): - logger.success(m18n.n('service_reloaded', service=name)) + if _run_service_command("reload", name): + logger.success(m18n.n("service_reloaded", service=name)) else: - if service_status(name)['status'] != 'inactive': - raise YunohostError('service_reload_failed', service=name, logs=_get_journalctl_logs(name)) + if service_status(name)["status"] != "inactive": + raise YunohostError( + "service_reload_failed", + service=name, + logs=_get_journalctl_logs(name), + ) def service_restart(names): @@ -209,11 +235,15 @@ def service_restart(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('restart', name): - logger.success(m18n.n('service_restarted', service=name)) + if _run_service_command("restart", name): + logger.success(m18n.n("service_restarted", service=name)) else: - if service_status(name)['status'] != 'inactive': - raise YunohostError('service_restart_failed', service=name, logs=_get_journalctl_logs(name)) + if service_status(name)["status"] != "inactive": + raise YunohostError( + "service_restart_failed", + service=name, + logs=_get_journalctl_logs(name), + ) def service_reload_or_restart(names): @@ -227,11 +257,15 @@ def service_reload_or_restart(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('reload-or-restart', name): - logger.success(m18n.n('service_reloaded_or_restarted', service=name)) + if _run_service_command("reload-or-restart", name): + logger.success(m18n.n("service_reloaded_or_restarted", service=name)) else: - if service_status(name)['status'] != 'inactive': - raise YunohostError('service_reload_or_restart_failed', service=name, logs=_get_journalctl_logs(name)) + if service_status(name)["status"] != "inactive": + raise YunohostError( + "service_reload_or_restart_failed", + service=name, + logs=_get_journalctl_logs(name), + ) def service_enable(names): @@ -245,10 +279,12 @@ def service_enable(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('enable', name): - logger.success(m18n.n('service_enabled', service=name)) + if _run_service_command("enable", name): + logger.success(m18n.n("service_enabled", service=name)) else: - raise YunohostError('service_enable_failed', service=name, logs=_get_journalctl_logs(name)) + raise YunohostError( + "service_enable_failed", service=name, logs=_get_journalctl_logs(name) + ) def service_disable(names): @@ -262,10 +298,12 @@ def service_disable(names): if isinstance(names, str): names = [names] for name in names: - if _run_service_command('disable', name): - logger.success(m18n.n('service_disabled', service=name)) + if _run_service_command("disable", name): + logger.success(m18n.n("service_disabled", service=name)) else: - raise YunohostError('service_disable_failed', service=name, logs=_get_journalctl_logs(name)) + raise YunohostError( + "service_disable_failed", service=name, logs=_get_journalctl_logs(name) + ) def service_status(names=[]): @@ -287,7 +325,7 @@ def service_status(names=[]): # Validate service names requested for name in names: if name not in services.keys(): - raise YunohostError('service_unknown', service=name) + raise YunohostError("service_unknown", service=name) # Filter only requested servivces services = {k: v for k, v in services.items() if k in names} @@ -300,7 +338,9 @@ def service_status(names=[]): # the hack was to add fake services... services = {k: v for k, v in services.items() if v.get("status", "") is not None} - output = {s: _get_and_format_service_status(s, infos) for s, infos in services.items()} + output = { + s: _get_and_format_service_status(s, infos) for s, infos in services.items() + } if len(names) == 1: return output[names[0]] @@ -313,17 +353,19 @@ def _get_service_information_from_systemd(service): d = dbus.SystemBus() - systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') - manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') + systemd = d.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + manager = dbus.Interface(systemd, "org.freedesktop.systemd1.Manager") # c.f. https://zignar.net/2014/09/08/getting-started-with-dbus-python-systemd/ # Very interface, much intuitive, wow - service_unit = manager.LoadUnit(service + '.service') - service_proxy = d.get_object('org.freedesktop.systemd1', str(service_unit)) - properties_interface = dbus.Interface(service_proxy, 'org.freedesktop.DBus.Properties') + service_unit = manager.LoadUnit(service + ".service") + service_proxy = d.get_object("org.freedesktop.systemd1", str(service_unit)) + properties_interface = dbus.Interface( + service_proxy, "org.freedesktop.DBus.Properties" + ) - unit = properties_interface.GetAll('org.freedesktop.systemd1.Unit') - service = properties_interface.GetAll('org.freedesktop.systemd1.Service') + unit = properties_interface.GetAll("org.freedesktop.systemd1.Unit") + service = properties_interface.GetAll("org.freedesktop.systemd1.Service") if unit.get("LoadState", "not-found") == "not-found": # Service doesn't really exist @@ -338,13 +380,16 @@ def _get_and_format_service_status(service, infos): raw_status, raw_service = _get_service_information_from_systemd(systemd_service) if raw_status is None: - logger.error("Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." % systemd_service) + logger.error( + "Failed to get status information via dbus for service %s, systemctl didn't recognize this service ('NoSuchUnit')." + % systemd_service + ) return { - 'status': "unknown", - 'start_on_boot': "unknown", - 'last_state_change': "unknown", - 'description': "Error: failed to get information for this service, it doesn't exists for systemd", - 'configuration': "unknown", + "status": "unknown", + "start_on_boot": "unknown", + "last_state_change": "unknown", + "description": "Error: failed to get information for this service, it doesn't exists for systemd", + "configuration": "unknown", } # Try to get description directly from services.yml @@ -363,35 +408,46 @@ def _get_and_format_service_status(service, infos): description = str(raw_status.get("Description", "")) output = { - 'status': str(raw_status.get("SubState", "unknown")), - 'start_on_boot': str(raw_status.get("UnitFileState", "unknown")), - 'last_state_change': "unknown", - 'description': description, - 'configuration': "unknown", + "status": str(raw_status.get("SubState", "unknown")), + "start_on_boot": str(raw_status.get("UnitFileState", "unknown")), + "last_state_change": "unknown", + "description": description, + "configuration": "unknown", } # Fun stuff™ : to obtain the enabled/disabled status for sysv services, # gotta do this ... cf code of /lib/systemd/systemd-sysv-install if output["start_on_boot"] == "generated": - output["start_on_boot"] = "enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled" - elif os.path.exists("/etc/systemd/system/multi-user.target.wants/%s.service" % service): + output["start_on_boot"] = ( + "enabled" if glob("/etc/rc[S5].d/S??" + service) else "disabled" + ) + elif os.path.exists( + "/etc/systemd/system/multi-user.target.wants/%s.service" % service + ): output["start_on_boot"] = "enabled" if "StateChangeTimestamp" in raw_status: - output['last_state_change'] = datetime.utcfromtimestamp(raw_status["StateChangeTimestamp"] / 1000000) + output["last_state_change"] = datetime.utcfromtimestamp( + raw_status["StateChangeTimestamp"] / 1000000 + ) # 'test_status' is an optional field to test the status of the service using a custom command if "test_status" in infos: - p = subprocess.Popen(infos["test_status"], - shell=True, - executable='/bin/bash', - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + p = subprocess.Popen( + infos["test_status"], + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) p.communicate() output["status"] = "running" if p.returncode == 0 else "failed" - elif raw_service.get("Type", "").lower() == "oneshot" and output["status"] == "exited": + elif ( + raw_service.get("Type", "").lower() == "oneshot" + and output["status"] == "exited" + ): # These are services like yunohost-firewall, hotspot, vpnclient, # ... they will be "exited" why doesn't provide any info about # the real state of the service (unless they did provide a @@ -400,11 +456,13 @@ def _get_and_format_service_status(service, infos): # 'test_status' is an optional field to test the status of the service using a custom command if "test_conf" in infos: - p = subprocess.Popen(infos["test_conf"], - shell=True, - executable='/bin/bash', - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + p = subprocess.Popen( + infos["test_conf"], + shell=True, + executable="/bin/bash", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) out, _ = p.communicate() if p.returncode == 0: @@ -429,9 +487,9 @@ def service_log(name, number=50): number = int(number) if name not in services.keys(): - raise YunohostError('service_unknown', service=name) + raise YunohostError("service_unknown", service=name) - log_list = services[name].get('log', []) + log_list = services[name].get("log", []) if not isinstance(log_list, list): log_list = [log_list] @@ -472,13 +530,16 @@ def service_log(name, number=50): if not log_file.endswith(".log"): continue - result[log_file_path] = _tail(log_file_path, number) if os.path.exists(log_file_path) else [] + result[log_file_path] = ( + _tail(log_file_path, number) if os.path.exists(log_file_path) else [] + ) return result -def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, - list_pending=False): +def service_regen_conf( + names=[], with_diff=False, force=False, dry_run=False, list_pending=False +): services = _get_services() @@ -487,7 +548,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, for name in names: if name not in services.keys(): - raise YunohostError('service_unknown', service=name) + raise YunohostError("service_unknown", service=name) if names is []: names = list(services.keys()) @@ -495,6 +556,7 @@ def service_regen_conf(names=[], with_diff=False, force=False, dry_run=False, logger.warning(m18n.n("service_regen_conf_is_deprecated")) from yunohost.regenconf import regen_conf + return regen_conf(names, with_diff, force, dry_run, list_pending) @@ -509,16 +571,32 @@ def _run_service_command(action, service): """ services = _get_services() if service not in services.keys(): - raise YunohostError('service_unknown', service=service) + raise YunohostError("service_unknown", service=service) - possible_actions = ['start', 'stop', 'restart', 'reload', 'reload-or-restart', 'enable', 'disable'] + possible_actions = [ + "start", + "stop", + "restart", + "reload", + "reload-or-restart", + "enable", + "disable", + ] if action not in possible_actions: - raise ValueError("Unknown action '%s', available actions are: %s" % (action, ", ".join(possible_actions))) + raise ValueError( + "Unknown action '%s', available actions are: %s" + % (action, ", ".join(possible_actions)) + ) - cmd = 'systemctl %s %s' % (action, service) + cmd = "systemctl %s %s" % (action, service) - need_lock = services[service].get('need_lock', False) \ - and action in ['start', 'stop', 'restart', 'reload', 'reload-or-restart'] + need_lock = services[service].get("need_lock", False) and action in [ + "start", + "stop", + "restart", + "reload", + "reload-or-restart", + ] if action in ["enable", "disable"]: cmd += " --quiet" @@ -535,7 +613,7 @@ def _run_service_command(action, service): p.communicate() if p.returncode != 0: - logger.warning(m18n.n('service_cmd_exec_failed', command=cmd)) + logger.warning(m18n.n("service_cmd_exec_failed", command=cmd)) return False except Exception as e: @@ -571,8 +649,9 @@ def _give_lock(action, service, p): # 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)) + logger.debug( + "Giving a lock to PID %s for service %s !" % (str(son_PID), service) + ) append_to_file(MOULINETTE_LOCK, "\n%s" % str(son_PID)) return son_PID @@ -583,7 +662,7 @@ def _remove_lock(PID_to_remove): PIDs = read_file(MOULINETTE_LOCK).split("\n") PIDs_to_keep = [PID for PID in PIDs if int(PID) != PID_to_remove] - write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep)) + write_to_file(MOULINETTE_LOCK, "\n".join(PIDs_to_keep)) def _get_services(): @@ -592,7 +671,7 @@ def _get_services(): """ try: - with open('/etc/yunohost/services.yml', 'r') as f: + with open("/etc/yunohost/services.yml", "r") as f: services = yaml.load(f) or {} except: return {} @@ -604,7 +683,9 @@ def _get_services(): del services[key] # Dirty hack to automatically find custom SSH port ... - ssh_port_line = re.findall(r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config")) + ssh_port_line = re.findall( + r"\bPort *([0-9]{2,5})\b", read_file("/etc/ssh/sshd_config") + ) if len(ssh_port_line) == 1: services["ssh"]["needs_exposed_ports"] = [int(ssh_port_line[0])] @@ -636,10 +717,10 @@ def _save_services(services): """ try: - with open('/etc/yunohost/services.yml', 'w') as f: + 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) + logger.warning("Error while saving services, exception: %s", e, exc_info=1) raise @@ -657,6 +738,7 @@ def _tail(file, n): try: if file.endswith(".gz"): import gzip + f = gzip.open(file) lines = f.read().splitlines() else: @@ -697,15 +779,15 @@ def _find_previous_log_file(file): Find the previous log file """ splitext = os.path.splitext(file) - if splitext[1] == '.gz': + if splitext[1] == ".gz": file = splitext[0] splitext = os.path.splitext(file) ext = splitext[1] - i = re.findall(r'\.(\d+)', ext) + 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) + previous_file = previous_file + ".%d" % (i) if os.path.exists(previous_file): return previous_file @@ -720,7 +802,15 @@ def _get_journalctl_logs(service, number="all"): services = _get_services() systemd_service = services.get(service, {}).get("actual_systemd_service", service) try: - return check_output("journalctl --no-hostname --no-pager -u {0} -n{1}".format(systemd_service, number)) + return check_output( + "journalctl --no-hostname --no-pager -u {0} -n{1}".format( + systemd_service, number + ) + ) except: import traceback - return "error while get services logs from journalctl:\n%s" % traceback.format_exc() + + return ( + "error while get services logs from journalctl:\n%s" + % traceback.format_exc() + ) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 17ab93cd4..a56e40aad 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -10,7 +10,7 @@ from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.regenconf import regen_conf -logger = getActionLogger('yunohost.settings') +logger = getActionLogger("yunohost.settings") SETTINGS_PATH = "/etc/yunohost/settings.json" SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" @@ -30,8 +30,8 @@ def is_boolean(value): if isinstance(value, bool): return True, value elif isinstance(value, str): - if str(value).lower() in ['true', 'on', 'yes', 'false', 'off', 'no']: - return True, str(value).lower() in ['true', 'on', 'yes'] + if str(value).lower() in ["true", "on", "yes", "false", "off", "no"]: + return True, str(value).lower() in ["true", "on", "yes"] else: return False, None else: @@ -53,28 +53,49 @@ def is_boolean(value): # * string # * enum (in the form of a python list) -DEFAULTS = OrderedDict([ - # Password Validation - # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest - ("security.password.admin.strength", {"type": "int", "default": 1}), - ("security.password.user.strength", {"type": "int", "default": 1}), - - ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}), - ("security.ssh.compatibility", {"type": "enum", "default": "modern", - "choices": ["intermediate", "modern"]}), - ("security.nginx.compatibility", {"type": "enum", "default": "intermediate", - "choices": ["intermediate", "modern"]}), - ("security.postfix.compatibility", {"type": "enum", "default": "intermediate", - "choices": ["intermediate", "modern"]}), - - ("pop3.enabled", {"type": "bool", "default": False}), - ("smtp.allow_ipv6", {"type": "bool", "default": True}), - ("smtp.relay.host", {"type": "string", "default": ""}), - ("smtp.relay.port", {"type": "int", "default": 587}), - ("smtp.relay.user", {"type": "string", "default": ""}), - ("smtp.relay.password", {"type": "string", "default": ""}), - ("backup.compress_tar_archives", {"type": "bool", "default": False}), -]) +DEFAULTS = OrderedDict( + [ + # Password Validation + # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest + ("security.password.admin.strength", {"type": "int", "default": 1}), + ("security.password.user.strength", {"type": "int", "default": 1}), + ( + "service.ssh.allow_deprecated_dsa_hostkey", + {"type": "bool", "default": False}, + ), + ( + "security.ssh.compatibility", + { + "type": "enum", + "default": "modern", + "choices": ["intermediate", "modern"], + }, + ), + ( + "security.nginx.compatibility", + { + "type": "enum", + "default": "intermediate", + "choices": ["intermediate", "modern"], + }, + ), + ( + "security.postfix.compatibility", + { + "type": "enum", + "default": "intermediate", + "choices": ["intermediate", "modern"], + }, + ), + ("pop3.enabled", {"type": "bool", "default": False}), + ("smtp.allow_ipv6", {"type": "bool", "default": True}), + ("smtp.relay.host", {"type": "string", "default": ""}), + ("smtp.relay.port", {"type": "int", "default": 587}), + ("smtp.relay.user", {"type": "string", "default": ""}), + ("smtp.relay.password", {"type": "string", "default": ""}), + ("backup.compress_tar_archives", {"type": "bool", "default": False}), + ] +) def settings_get(key, full=False): @@ -88,12 +109,12 @@ def settings_get(key, full=False): settings = _get_settings() if key not in settings: - raise YunohostError('global_settings_key_doesnt_exists', settings_key=key) + raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) if full: return settings[key] - return settings[key]['value'] + return settings[key]["value"] def settings_list(): @@ -116,7 +137,7 @@ def settings_set(key, value): settings = _get_settings() if key not in settings: - raise YunohostError('global_settings_key_doesnt_exists', settings_key=key) + raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) key_type = settings[key]["type"] @@ -125,33 +146,51 @@ def settings_set(key, value): if boolean_value[0]: value = boolean_value[1] else: - raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + raise YunohostError( + "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): if isinstance(value, str): try: value = int(value) except: - raise YunohostError('global_settings_bad_type_for_setting', - setting=key, - received_type=type(value).__name__, - expected_type=key_type) + raise YunohostError( + "global_settings_bad_type_for_setting", + setting=key, + received_type=type(value).__name__, + expected_type=key_type, + ) else: - raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + raise YunohostError( + "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, str): - raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + raise YunohostError( + "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 YunohostError('global_settings_bad_choice_for_enum', setting=key, - choice=str(value), - available_choices=", ".join(settings[key]["choices"])) + raise YunohostError( + "global_settings_bad_choice_for_enum", + setting=key, + choice=str(value), + available_choices=", ".join(settings[key]["choices"]), + ) else: - raise YunohostError('global_settings_unknown_type', setting=key, - unknown_type=key_type) + raise YunohostError( + "global_settings_unknown_type", setting=key, unknown_type=key_type + ) old_value = settings[key].get("value") settings[key]["value"] = value @@ -175,7 +214,7 @@ def settings_reset(key): settings = _get_settings() if key not in settings: - raise YunohostError('global_settings_key_doesnt_exists', settings_key=key) + raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) settings[key]["value"] = settings[key]["default"] _save_settings(settings) @@ -196,7 +235,9 @@ def settings_reset_all(): # 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.utcnow().strftime("%F_%X") + old_settings_backup_path = ( + SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X") + ) _save_settings(settings, location=old_settings_backup_path) for value in settings.values(): @@ -206,12 +247,13 @@ def settings_reset_all(): return { "old_settings_backup_path": old_settings_backup_path, - "message": m18n.n("global_settings_reset_success", path=old_settings_backup_path) + "message": m18n.n( + "global_settings_reset_success", path=old_settings_backup_path + ), } def _get_settings(): - def get_setting_description(key): if key.startswith("example"): # (This is for dummy stuff used during unit tests) @@ -254,18 +296,24 @@ def _get_settings(): settings[key] = value settings[key]["description"] = get_setting_description(key) else: - logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file', - setting_key=key)) + logger.warning( + m18n.n( + "global_settings_unknown_setting_from_settings_file", + setting_key=key, + ) + ) unknown_settings[key] = value except Exception as e: - raise YunohostError('global_settings_cant_open_settings', reason=e) + raise YunohostError("global_settings_cant_open_settings", reason=e) if unknown_settings: try: _save_settings(unknown_settings, location=unknown_settings_path) _save_settings(settings) except Exception as e: - logger.warning("Failed to save unknown settings (because %s), aborting." % e) + logger.warning( + "Failed to save unknown settings (because %s), aborting." % e + ) return settings @@ -280,13 +328,13 @@ def _save_settings(settings, location=SETTINGS_PATH): try: result = json.dumps(settings_without_description, indent=4) except Exception as e: - raise YunohostError('global_settings_cant_serialize_settings', reason=e) + raise YunohostError("global_settings_cant_serialize_settings", reason=e) try: with open(location, "w") as settings_fd: settings_fd.write(result) except Exception as e: - raise YunohostError('global_settings_cant_write_settings', reason=e) + raise YunohostError("global_settings_cant_write_settings", reason=e) # Meant to be a dict of setting_name -> function to call @@ -295,10 +343,16 @@ post_change_hooks = {} def post_change_hook(setting_name): def decorator(func): - assert setting_name in DEFAULTS.keys(), "The setting %s does not exists" % setting_name - assert setting_name not in post_change_hooks, "You can only register one post change hook per setting (in particular for %s)" % setting_name + assert setting_name in DEFAULTS.keys(), ( + "The setting %s does not exists" % setting_name + ) + assert setting_name not in post_change_hooks, ( + "You can only register one post change hook per setting (in particular for %s)" + % setting_name + ) post_change_hooks[setting_name] = func return func + return decorator @@ -322,16 +376,17 @@ def trigger_post_change_hook(setting_name, old_value, new_value): # # =========================================== + @post_change_hook("security.nginx.compatibility") def reconfigure_nginx(setting_name, old_value, new_value): if old_value != new_value: - regen_conf(names=['nginx']) + regen_conf(names=["nginx"]) @post_change_hook("security.ssh.compatibility") def reconfigure_ssh(setting_name, old_value, new_value): if old_value != new_value: - regen_conf(names=['ssh']) + regen_conf(names=["ssh"]) @post_change_hook("smtp.allow_ipv6") @@ -342,31 +397,31 @@ def reconfigure_ssh(setting_name, old_value, new_value): @post_change_hook("security.postfix.compatibility") def reconfigure_postfix(setting_name, old_value, new_value): if old_value != new_value: - regen_conf(names=['postfix']) + regen_conf(names=["postfix"]) @post_change_hook("pop3.enabled") def reconfigure_dovecot(setting_name, old_value, new_value): - dovecot_package = 'dovecot-pop3d' + dovecot_package = "dovecot-pop3d" environment = os.environ.copy() - environment.update({'DEBIAN_FRONTEND': 'noninteractive'}) + environment.update({"DEBIAN_FRONTEND": "noninteractive"}) if new_value == "True": command = [ - 'apt-get', - '-y', - '--no-remove', - '-o Dpkg::Options::=--force-confdef', - '-o Dpkg::Options::=--force-confold', - 'install', + "apt-get", + "-y", + "--no-remove", + "-o Dpkg::Options::=--force-confdef", + "-o Dpkg::Options::=--force-confold", + "install", dovecot_package, ] subprocess.call(command, env=environment) if old_value != new_value: - regen_conf(names=['dovecot']) + regen_conf(names=["dovecot"]) else: if old_value != new_value: - regen_conf(names=['dovecot']) - command = ['apt-get', '-y', 'remove', dovecot_package] + regen_conf(names=["dovecot"]) + command = ["apt-get", "-y", "remove", dovecot_package] subprocess.call(command, env=environment) diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index be876ce16..f7c6fcbb1 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -21,15 +21,16 @@ def user_ssh_allow(username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): - raise YunohostError('user_unknown', user=username) + raise YunohostError("user_unknown", user=username) from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() - ldap.update('uid=%s,ou=users' % username, {'loginShell': ['/bin/bash']}) + ldap.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']) + subprocess.call(["nscd", "-i", "passwd"]) def user_ssh_disallow(username): @@ -42,15 +43,16 @@ def user_ssh_disallow(username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): - raise YunohostError('user_unknown', user=username) + raise YunohostError("user_unknown", user=username) from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() - ldap.update('uid=%s,ou=users' % username, {'loginShell': ['/bin/false']}) + ldap.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']) + subprocess.call(["nscd", "-i", "passwd"]) def user_ssh_list_keys(username): @@ -58,7 +60,9 @@ def user_ssh_list_keys(username): 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") + authorized_keys_file = os.path.join( + user["homeDirectory"][0], ".ssh", "authorized_keys" + ) if not os.path.exists(authorized_keys_file): return {"keys": []} @@ -76,10 +80,12 @@ def user_ssh_list_keys(username): # assuming a key per non empty line key = line.strip() - keys.append({ - "key": key, - "name": last_comment, - }) + keys.append( + { + "key": key, + "name": last_comment, + } + ) last_comment = "" @@ -91,12 +97,18 @@ def user_ssh_add_key(username, key, comment): 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") + 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]) + 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, "") @@ -125,10 +137,14 @@ def user_ssh_remove_key(username, key): 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") + 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)) + raise Exception( + "this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file) + ) authorized_keys_content = read_file(authorized_keys_file) @@ -147,6 +163,7 @@ def user_ssh_remove_key(username, key): write_to_file(authorized_keys_file, authorized_keys_content) + # # Helpers # @@ -164,8 +181,11 @@ def _get_user_for_ssh(username, attrs=None): # default is “yes”. sshd_config_content = read_file(SSHD_CONFIG_PATH) - if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$", - sshd_config_content, re.MULTILINE): + if re.search( + "^ *PermitRootLogin +(no|forced-commands-only) *$", + sshd_config_content, + re.MULTILINE, + ): return {"PermitRootLogin": False} return {"PermitRootLogin": True} @@ -173,31 +193,34 @@ def _get_user_for_ssh(username, attrs=None): if username == "root": root_unix = pwd.getpwnam("root") return { - 'username': 'root', - 'fullname': '', - 'mail': '', - 'ssh_allowed': ssh_root_login_status()["PermitRootLogin"], - 'shell': root_unix.pw_shell, - 'home_path': root_unix.pw_dir, + "username": "root", + "fullname": "", + "mail": "", + "ssh_allowed": ssh_root_login_status()["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, + "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 from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() - user = ldap.search('ou=users,dc=yunohost,dc=org', - '(&(objectclass=person)(uid=%s))' % username, - attrs) + user = ldap.search( + "ou=users,dc=yunohost,dc=org", + "(&(objectclass=person)(uid=%s))" % username, + attrs, + ) assert len(user) in (0, 1) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 1aaefb993..49f87decf 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -6,6 +6,7 @@ import moulinette from moulinette import m18n, msettings from yunohost.utils.error import YunohostError from contextlib import contextmanager + sys.path.append("..") @@ -43,6 +44,7 @@ def raiseYunohostError(mocker, key, **kwargs): def pytest_addoption(parser): parser.addoption("--yunodebug", action="store_true", default=False) + # # Tweak translator to raise exceptions if string keys are not defined # # @@ -77,5 +79,6 @@ def pytest_cmdline_main(config): sys.path.insert(0, "/usr/lib/moulinette/") import yunohost + yunohost.init(debug=config.option.yunodebug) msettings["interface"] = "test" diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index baa3d5181..ae8a4829b 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -159,7 +159,9 @@ def install_legacy_app(domain, path, public=True): def install_full_domain_app(domain): app_install( - os.path.join(get_test_apps_dir(), "full_domain_app_ynh"), args="domain=%s" % domain, force=True + os.path.join(get_test_apps_dir(), "full_domain_app_ynh"), + args="domain=%s" % domain, + force=True, ) @@ -376,7 +378,10 @@ def test_systemfuckedup_during_app_upgrade(mocker, secondary_domain): with pytest.raises(YunohostError): with message(mocker, "app_action_broke_system"): - app_upgrade("break_yo_system", file=os.path.join(get_test_apps_dir(), "break_yo_system_ynh")) + app_upgrade( + "break_yo_system", + file=os.path.join(get_test_apps_dir(), "break_yo_system_ynh"), + ) def test_failed_multiple_app_upgrade(mocker, secondary_domain): @@ -389,7 +394,9 @@ def test_failed_multiple_app_upgrade(mocker, secondary_domain): app_upgrade( ["break_yo_system", "legacy_app"], file={ - "break_yo_system": os.path.join(get_test_apps_dir(), "break_yo_system_ynh"), + "break_yo_system": os.path.join( + get_test_apps_dir(), "break_yo_system_ynh" + ), "legacy": os.path.join(get_test_apps_dir(), "legacy_app_ynh"), }, ) diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 03a2fa0e6..7ce07f5dd 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -40,21 +40,34 @@ def test_parse_args_in_yunohost_format_empty(): def test_parse_args_in_yunohost_format_string(): - questions = [{"name": "some_string", "type": "string", }] + questions = [ + { + "name": "some_string", + "type": "string", + } + ] answers = {"some_string": "some_value"} expected_result = OrderedDict({"some_string": ("some_value", "string")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_string_default_type(): - questions = [{"name": "some_string", }] + questions = [ + { + "name": "some_string", + } + ] answers = {"some_string": "some_value"} expected_result = OrderedDict({"some_string": ("some_value", "string")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_string_no_input(): - questions = [{"name": "some_string", }] + questions = [ + { + "name": "some_string", + } + ] answers = {} with pytest.raises(YunohostError): @@ -62,7 +75,12 @@ def test_parse_args_in_yunohost_format_string_no_input(): def test_parse_args_in_yunohost_format_string_input(): - questions = [{"name": "some_string", "ask": "some question", }] + questions = [ + { + "name": "some_string", + "ask": "some question", + } + ] answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) @@ -71,7 +89,11 @@ def test_parse_args_in_yunohost_format_string_input(): def test_parse_args_in_yunohost_format_string_input_no_ask(): - questions = [{"name": "some_string", }] + questions = [ + { + "name": "some_string", + } + ] answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) @@ -80,14 +102,25 @@ def test_parse_args_in_yunohost_format_string_input_no_ask(): def test_parse_args_in_yunohost_format_string_no_input_optional(): - questions = [{"name": "some_string", "optional": True, }] + questions = [ + { + "name": "some_string", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_string": ("", "string")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_string_optional_with_input(): - questions = [{"name": "some_string", "ask": "some question", "optional": True, }] + questions = [ + { + "name": "some_string", + "ask": "some question", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) @@ -96,7 +129,13 @@ def test_parse_args_in_yunohost_format_string_optional_with_input(): def test_parse_args_in_yunohost_format_string_optional_with_empty_input(): - questions = [{"name": "some_string", "ask": "some question", "optional": True, }] + questions = [ + { + "name": "some_string", + "ask": "some question", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_string": ("", "string")}) @@ -105,7 +144,12 @@ def test_parse_args_in_yunohost_format_string_optional_with_empty_input(): def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask(): - questions = [{"name": "some_string", "optional": True, }] + questions = [ + { + "name": "some_string", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) @@ -115,7 +159,11 @@ def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask(): def test_parse_args_in_yunohost_format_string_no_input_default(): questions = [ - {"name": "some_string", "ask": "some question", "default": "some_value", } + { + "name": "some_string", + "ask": "some question", + "default": "some_value", + } ] answers = {} expected_result = OrderedDict({"some_string": ("some_value", "string")}) @@ -124,7 +172,12 @@ def test_parse_args_in_yunohost_format_string_no_input_default(): def test_parse_args_in_yunohost_format_string_input_test_ask(): ask_text = "some question" - questions = [{"name": "some_string", "ask": ask_text, }] + questions = [ + { + "name": "some_string", + "ask": ask_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -135,7 +188,13 @@ def test_parse_args_in_yunohost_format_string_input_test_ask(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_default(): ask_text = "some question" default_text = "some example" - questions = [{"name": "some_string", "ask": ask_text, "default": default_text, }] + questions = [ + { + "name": "some_string", + "ask": ask_text, + "default": default_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -147,7 +206,13 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_default(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_example(): ask_text = "some question" example_text = "some example" - questions = [{"name": "some_string", "ask": ask_text, "example": example_text, }] + questions = [ + { + "name": "some_string", + "ask": ask_text, + "example": example_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -160,7 +225,13 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_example(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_help(): ask_text = "some question" help_text = "some_help" - questions = [{"name": "some_string", "ask": ask_text, "help": help_text, }] + questions = [ + { + "name": "some_string", + "ask": ask_text, + "help": help_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -195,7 +266,13 @@ def test_parse_args_in_yunohost_format_string_with_choice_bad(): def test_parse_args_in_yunohost_format_string_with_choice_ask(): ask_text = "some question" choices = ["fr", "en", "es", "it", "ru"] - questions = [{"name": "some_string", "ask": ask_text, "choices": choices, }] + questions = [ + { + "name": "some_string", + "ask": ask_text, + "choices": choices, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="ru") as prompt: @@ -221,14 +298,24 @@ def test_parse_args_in_yunohost_format_string_with_choice_default(): def test_parse_args_in_yunohost_format_password(): - questions = [{"name": "some_password", "type": "password", }] + questions = [ + { + "name": "some_password", + "type": "password", + } + ] answers = {"some_password": "some_value"} expected_result = OrderedDict({"some_password": ("some_value", "password")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_password_no_input(): - questions = [{"name": "some_password", "type": "password", }] + questions = [ + { + "name": "some_password", + "type": "password", + } + ] answers = {} with pytest.raises(YunohostError): @@ -236,7 +323,13 @@ def test_parse_args_in_yunohost_format_password_no_input(): def test_parse_args_in_yunohost_format_password_input(): - questions = [{"name": "some_password", "type": "password", "ask": "some question", }] + questions = [ + { + "name": "some_password", + "type": "password", + "ask": "some question", + } + ] answers = {} expected_result = OrderedDict({"some_password": ("some_value", "password")}) @@ -245,7 +338,12 @@ def test_parse_args_in_yunohost_format_password_input(): def test_parse_args_in_yunohost_format_password_input_no_ask(): - questions = [{"name": "some_password", "type": "password", }] + questions = [ + { + "name": "some_password", + "type": "password", + } + ] answers = {} expected_result = OrderedDict({"some_password": ("some_value", "password")}) @@ -254,13 +352,21 @@ def test_parse_args_in_yunohost_format_password_input_no_ask(): def test_parse_args_in_yunohost_format_password_no_input_optional(): - questions = [{"name": "some_password", "type": "password", "optional": True, }] + questions = [ + { + "name": "some_password", + "type": "password", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_password": ("", "password")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result - questions = [{"name": "some_password", "type": "password", "optional": True, "default": ""}] + questions = [ + {"name": "some_password", "type": "password", "optional": True, "default": ""} + ] assert _parse_args_in_yunohost_format(answers, questions) == expected_result @@ -298,7 +404,13 @@ def test_parse_args_in_yunohost_format_password_optional_with_empty_input(): def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask(): - questions = [{"name": "some_password", "type": "password", "optional": True, }] + questions = [ + { + "name": "some_password", + "type": "password", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_password": ("some_value", "password")}) @@ -341,7 +453,13 @@ def test_parse_args_in_yunohost_format_password_no_input_example(): def test_parse_args_in_yunohost_format_password_input_test_ask(): ask_text = "some question" - questions = [{"name": "some_password", "type": "password", "ask": ask_text, }] + questions = [ + { + "name": "some_password", + "type": "password", + "ask": ask_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -441,14 +559,24 @@ def test_parse_args_in_yunohost_format_password_optional_strong_enough(): def test_parse_args_in_yunohost_format_path(): - questions = [{"name": "some_path", "type": "path", }] + questions = [ + { + "name": "some_path", + "type": "path", + } + ] answers = {"some_path": "some_value"} expected_result = OrderedDict({"some_path": ("some_value", "path")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_path_no_input(): - questions = [{"name": "some_path", "type": "path", }] + questions = [ + { + "name": "some_path", + "type": "path", + } + ] answers = {} with pytest.raises(YunohostError): @@ -456,7 +584,13 @@ def test_parse_args_in_yunohost_format_path_no_input(): def test_parse_args_in_yunohost_format_path_input(): - questions = [{"name": "some_path", "type": "path", "ask": "some question", }] + questions = [ + { + "name": "some_path", + "type": "path", + "ask": "some question", + } + ] answers = {} expected_result = OrderedDict({"some_path": ("some_value", "path")}) @@ -465,7 +599,12 @@ def test_parse_args_in_yunohost_format_path_input(): def test_parse_args_in_yunohost_format_path_input_no_ask(): - questions = [{"name": "some_path", "type": "path", }] + questions = [ + { + "name": "some_path", + "type": "path", + } + ] answers = {} expected_result = OrderedDict({"some_path": ("some_value", "path")}) @@ -474,7 +613,13 @@ def test_parse_args_in_yunohost_format_path_input_no_ask(): def test_parse_args_in_yunohost_format_path_no_input_optional(): - questions = [{"name": "some_path", "type": "path", "optional": True, }] + questions = [ + { + "name": "some_path", + "type": "path", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_path": ("", "path")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result @@ -482,7 +627,12 @@ def test_parse_args_in_yunohost_format_path_no_input_optional(): def test_parse_args_in_yunohost_format_path_optional_with_input(): questions = [ - {"name": "some_path", "ask": "some question", "type": "path", "optional": True, } + { + "name": "some_path", + "ask": "some question", + "type": "path", + "optional": True, + } ] answers = {} expected_result = OrderedDict({"some_path": ("some_value", "path")}) @@ -493,7 +643,12 @@ def test_parse_args_in_yunohost_format_path_optional_with_input(): def test_parse_args_in_yunohost_format_path_optional_with_empty_input(): questions = [ - {"name": "some_path", "ask": "some question", "type": "path", "optional": True, } + { + "name": "some_path", + "ask": "some question", + "type": "path", + "optional": True, + } ] answers = {} expected_result = OrderedDict({"some_path": ("", "path")}) @@ -503,7 +658,13 @@ def test_parse_args_in_yunohost_format_path_optional_with_empty_input(): def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask(): - questions = [{"name": "some_path", "type": "path", "optional": True, }] + questions = [ + { + "name": "some_path", + "type": "path", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_path": ("some_value", "path")}) @@ -527,7 +688,13 @@ def test_parse_args_in_yunohost_format_path_no_input_default(): def test_parse_args_in_yunohost_format_path_input_test_ask(): ask_text = "some question" - questions = [{"name": "some_path", "type": "path", "ask": ask_text, }] + questions = [ + { + "name": "some_path", + "type": "path", + "ask": ask_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="some_value") as prompt: @@ -539,7 +706,12 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_default(): ask_text = "some question" default_text = "some example" questions = [ - {"name": "some_path", "type": "path", "ask": ask_text, "default": default_text, } + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "default": default_text, + } ] answers = {} @@ -553,7 +725,12 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_example(): ask_text = "some question" example_text = "some example" questions = [ - {"name": "some_path", "type": "path", "ask": ask_text, "example": example_text, } + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "example": example_text, + } ] answers = {} @@ -568,7 +745,12 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_help(): ask_text = "some question" help_text = "some_help" questions = [ - {"name": "some_path", "type": "path", "ask": ask_text, "help": help_text, } + { + "name": "some_path", + "type": "path", + "ask": ask_text, + "help": help_text, + } ] answers = {} @@ -579,113 +761,133 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_help(): def test_parse_args_in_yunohost_format_boolean(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] answers = {"some_boolean": "y"} expected_result = OrderedDict({"some_boolean": (1, "boolean")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_boolean_all_yes(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] expected_result = OrderedDict({"some_boolean": (1, "boolean")}) assert ( - _parse_args_in_yunohost_format({"some_boolean": "y"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "y"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "1"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "1"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": 1}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": 1}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": True}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": True}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "True"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "True"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "TRUE"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "true"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "true"}, questions) + == expected_result ) def test_parse_args_in_yunohost_format_boolean_all_no(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] expected_result = OrderedDict({"some_boolean": (0, "boolean")}) assert ( - _parse_args_in_yunohost_format({"some_boolean": "n"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "n"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "N"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "N"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "no"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "no"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "0"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "0"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": 0}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": 0}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": False}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": False}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "False"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "False"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "FALSE"}, questions) + == expected_result ) assert ( - _parse_args_in_yunohost_format({"some_boolean": "false"}, questions) == - expected_result + _parse_args_in_yunohost_format({"some_boolean": "false"}, questions) + == expected_result ) # XXX apparently boolean are always False (0) by default, I'm not sure what to think about that def test_parse_args_in_yunohost_format_boolean_no_input(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] answers = {} expected_result = OrderedDict({"some_boolean": (0, "boolean")}) @@ -693,7 +895,12 @@ def test_parse_args_in_yunohost_format_boolean_no_input(): def test_parse_args_in_yunohost_format_boolean_bad_input(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] answers = {"some_boolean": "stuff"} with pytest.raises(YunohostError): @@ -701,7 +908,13 @@ def test_parse_args_in_yunohost_format_boolean_bad_input(): def test_parse_args_in_yunohost_format_boolean_input(): - questions = [{"name": "some_boolean", "type": "boolean", "ask": "some question", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "ask": "some question", + } + ] answers = {} expected_result = OrderedDict({"some_boolean": (1, "boolean")}) @@ -714,7 +927,12 @@ def test_parse_args_in_yunohost_format_boolean_input(): def test_parse_args_in_yunohost_format_boolean_input_no_ask(): - questions = [{"name": "some_boolean", "type": "boolean", }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + } + ] answers = {} expected_result = OrderedDict({"some_boolean": (1, "boolean")}) @@ -723,7 +941,13 @@ def test_parse_args_in_yunohost_format_boolean_input_no_ask(): def test_parse_args_in_yunohost_format_boolean_no_input_optional(): - questions = [{"name": "some_boolean", "type": "boolean", "optional": True, }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false assert _parse_args_in_yunohost_format(answers, questions) == expected_result @@ -762,7 +986,13 @@ def test_parse_args_in_yunohost_format_boolean_optional_with_empty_input(): def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask(): - questions = [{"name": "some_boolean", "type": "boolean", "optional": True, }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_boolean": (0, "boolean")}) @@ -800,7 +1030,13 @@ def test_parse_args_in_yunohost_format_boolean_bad_default(): def test_parse_args_in_yunohost_format_boolean_input_test_ask(): ask_text = "some question" - questions = [{"name": "some_boolean", "type": "boolean", "ask": ask_text, }] + questions = [ + { + "name": "some_boolean", + "type": "boolean", + "ask": ask_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value=0) as prompt: @@ -827,23 +1063,31 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default(): def test_parse_args_in_yunohost_format_domain_empty(): - questions = [{"name": "some_domain", "type": "domain", }] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] main_domain = "my_main_domain.com" expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) answers = {} with patch.object( domain, "_get_maindomain", return_value="my_main_domain.com" - ), patch.object( - domain, "domain_list", return_value={"domains": [main_domain]} - ): + ), patch.object(domain, "domain_list", return_value={"domains": [main_domain]}): assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_domain(): main_domain = "my_main_domain.com" domains = [main_domain] - questions = [{"name": "some_domain", "type": "domain", }] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] answers = {"some_domain": main_domain} expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) @@ -859,7 +1103,12 @@ def test_parse_args_in_yunohost_format_domain_two_domains(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [{"name": "some_domain", "type": "domain", }] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] answers = {"some_domain": other_domain} expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) @@ -882,7 +1131,12 @@ def test_parse_args_in_yunohost_format_domain_two_domains_wrong_answer(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [{"name": "some_domain", "type": "domain", }] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] answers = {"some_domain": "doesnt_exist.pouet"} with patch.object( @@ -897,7 +1151,12 @@ def test_parse_args_in_yunohost_format_domain_two_domains_default_no_ask(): other_domain = "some_other_domain.tld" domains = [main_domain, other_domain] - questions = [{"name": "some_domain", "type": "domain", }] + questions = [ + { + "name": "some_domain", + "type": "domain", + } + ] answers = {} expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) @@ -954,7 +1213,12 @@ def test_parse_args_in_yunohost_format_user_empty(): } } - questions = [{"name": "some_user", "type": "user", }] + questions = [ + { + "name": "some_user", + "type": "user", + } + ] answers = {} with patch.object(user, "user_list", return_value={"users": users}): @@ -975,7 +1239,12 @@ def test_parse_args_in_yunohost_format_user(): } } - questions = [{"name": "some_user", "type": "user", }] + questions = [ + { + "name": "some_user", + "type": "user", + } + ] answers = {"some_user": username} expected_result = OrderedDict({"some_user": (username, "user")}) @@ -1007,7 +1276,12 @@ def test_parse_args_in_yunohost_format_user_two_users(): }, } - questions = [{"name": "some_user", "type": "user", }] + questions = [ + { + "name": "some_user", + "type": "user", + } + ] answers = {"some_user": other_user} expected_result = OrderedDict({"some_user": (other_user, "user")}) @@ -1045,7 +1319,12 @@ def test_parse_args_in_yunohost_format_user_two_users_wrong_answer(): }, } - questions = [{"name": "some_user", "type": "user", }] + questions = [ + { + "name": "some_user", + "type": "user", + } + ] answers = {"some_user": "doesnt_exist.pouet"} with patch.object(user, "user_list", return_value={"users": users}): @@ -1112,23 +1391,38 @@ def test_parse_args_in_yunohost_format_user_two_users_default_input(): with patch.object(user, "user_info", return_value={}): expected_result = OrderedDict({"some_user": (username, "user")}) with patch.object(msignals, "prompt", return_value=username): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result + assert ( + _parse_args_in_yunohost_format(answers, questions) + == expected_result + ) expected_result = OrderedDict({"some_user": (other_user, "user")}) with patch.object(msignals, "prompt", return_value=other_user): - assert _parse_args_in_yunohost_format(answers, questions) == expected_result - + assert ( + _parse_args_in_yunohost_format(answers, questions) + == expected_result + ) def test_parse_args_in_yunohost_format_number(): - questions = [{"name": "some_number", "type": "number", }] + questions = [ + { + "name": "some_number", + "type": "number", + } + ] answers = {"some_number": 1337} expected_result = OrderedDict({"some_number": (1337, "number")}) assert _parse_args_in_yunohost_format(answers, questions) == expected_result def test_parse_args_in_yunohost_format_number_no_input(): - questions = [{"name": "some_number", "type": "number", }] + questions = [ + { + "name": "some_number", + "type": "number", + } + ] answers = {} expected_result = OrderedDict({"some_number": (0, "number")}) @@ -1136,7 +1430,12 @@ def test_parse_args_in_yunohost_format_number_no_input(): def test_parse_args_in_yunohost_format_number_bad_input(): - questions = [{"name": "some_number", "type": "number", }] + questions = [ + { + "name": "some_number", + "type": "number", + } + ] answers = {"some_number": "stuff"} with pytest.raises(YunohostError): @@ -1148,7 +1447,13 @@ def test_parse_args_in_yunohost_format_number_bad_input(): def test_parse_args_in_yunohost_format_number_input(): - questions = [{"name": "some_number", "type": "number", "ask": "some question", }] + questions = [ + { + "name": "some_number", + "type": "number", + "ask": "some question", + } + ] answers = {} expected_result = OrderedDict({"some_number": (1337, "number")}) @@ -1164,7 +1469,12 @@ def test_parse_args_in_yunohost_format_number_input(): def test_parse_args_in_yunohost_format_number_input_no_ask(): - questions = [{"name": "some_number", "type": "number", }] + questions = [ + { + "name": "some_number", + "type": "number", + } + ] answers = {} expected_result = OrderedDict({"some_number": (1337, "number")}) @@ -1173,7 +1483,13 @@ def test_parse_args_in_yunohost_format_number_input_no_ask(): def test_parse_args_in_yunohost_format_number_no_input_optional(): - questions = [{"name": "some_number", "type": "number", "optional": True, }] + questions = [ + { + "name": "some_number", + "type": "number", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_number": (0, "number")}) # default to 0 assert _parse_args_in_yunohost_format(answers, questions) == expected_result @@ -1196,7 +1512,13 @@ def test_parse_args_in_yunohost_format_number_optional_with_input(): def test_parse_args_in_yunohost_format_number_optional_with_input_without_ask(): - questions = [{"name": "some_number", "type": "number", "optional": True, }] + questions = [ + { + "name": "some_number", + "type": "number", + "optional": True, + } + ] answers = {} expected_result = OrderedDict({"some_number": (0, "number")}) @@ -1234,7 +1556,13 @@ def test_parse_args_in_yunohost_format_number_bad_default(): def test_parse_args_in_yunohost_format_number_input_test_ask(): ask_text = "some question" - questions = [{"name": "some_number", "type": "number", "ask": ask_text, }] + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="1111") as prompt: @@ -1245,7 +1573,14 @@ def test_parse_args_in_yunohost_format_number_input_test_ask(): def test_parse_args_in_yunohost_format_number_input_test_ask_with_default(): ask_text = "some question" default_value = 1337 - questions = [{"name": "some_number", "type": "number", "ask": ask_text, "default": default_value, }] + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "default": default_value, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="1111") as prompt: @@ -1257,7 +1592,14 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_default(): def test_parse_args_in_yunohost_format_number_input_test_ask_with_example(): ask_text = "some question" example_value = 1337 - questions = [{"name": "some_number", "type": "number", "ask": ask_text, "example": example_value, }] + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "example": example_value, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="1111") as prompt: @@ -1270,7 +1612,14 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_example(): def test_parse_args_in_yunohost_format_number_input_test_ask_with_help(): ask_text = "some question" help_value = 1337 - questions = [{"name": "some_number", "type": "number", "ask": ask_text, "help": help_value, }] + questions = [ + { + "name": "some_number", + "type": "number", + "ask": ask_text, + "help": help_value, + } + ] answers = {} with patch.object(msignals, "prompt", return_value="1111") as prompt: @@ -1278,6 +1627,7 @@ def test_parse_args_in_yunohost_format_number_input_test_ask_with_help(): assert ask_text in prompt.call_args[0][0] assert help_value in prompt.call_args[0][0] + def test_parse_args_in_yunohost_format_display_text(): questions = [{"name": "some_app", "type": "display_text", "ask": "foobar"}] answers = {} diff --git a/src/yunohost/tests/test_appscatalog.py b/src/yunohost/tests/test_appscatalog.py index a173501d3..e3bd5d49d 100644 --- a/src/yunohost/tests/test_appscatalog.py +++ b/src/yunohost/tests/test_appscatalog.py @@ -9,18 +9,20 @@ from moulinette import m18n from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml from yunohost.utils.error import YunohostError -from yunohost.app import (_initialize_apps_catalog_system, - _read_apps_catalog_list, - _update_apps_catalog, - _actual_apps_catalog_api_url, - _load_apps_catalog, - app_catalog, - logger, - APPS_CATALOG_CACHE, - APPS_CATALOG_CONF, - APPS_CATALOG_CRON_PATH, - APPS_CATALOG_API_VERSION, - APPS_CATALOG_DEFAULT_URL) +from yunohost.app import ( + _initialize_apps_catalog_system, + _read_apps_catalog_list, + _update_apps_catalog, + _actual_apps_catalog_api_url, + _load_apps_catalog, + app_catalog, + logger, + APPS_CATALOG_CACHE, + APPS_CATALOG_CONF, + APPS_CATALOG_CRON_PATH, + APPS_CATALOG_API_VERSION, + APPS_CATALOG_DEFAULT_URL, +) APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAULT_URL) CRON_FOLDER, CRON_NAME = APPS_CATALOG_CRON_PATH.rsplit("/", 1) @@ -69,6 +71,7 @@ def cron_job_is_there(): r = os.system("run-parts -v --test %s | grep %s" % (CRON_FOLDER, CRON_NAME)) return r == 0 + # # ################################################ # @@ -86,7 +89,7 @@ def test_apps_catalog_init(mocker): # Initialize ... mocker.spy(m18n, "n") _initialize_apps_catalog_system() - m18n.n.assert_any_call('apps_catalog_init_success') + m18n.n.assert_any_call("apps_catalog_init_success") # Then there's a cron enabled assert cron_job_is_there() @@ -159,8 +162,7 @@ def test_apps_catalog_update_404(mocker): with requests_mock.Mocker() as m: # 404 error - m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, - status_code=404) + m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, status_code=404) with pytest.raises(YunohostError): mocker.spy(m18n, "n") @@ -176,8 +178,9 @@ def test_apps_catalog_update_timeout(mocker): with requests_mock.Mocker() as m: # Timeout - m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, - exc=requests.exceptions.ConnectTimeout) + m.register_uri( + "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.ConnectTimeout + ) with pytest.raises(YunohostError): mocker.spy(m18n, "n") @@ -193,8 +196,9 @@ def test_apps_catalog_update_sslerror(mocker): with requests_mock.Mocker() as m: # SSL error - m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, - exc=requests.exceptions.SSLError) + m.register_uri( + "GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.SSLError + ) with pytest.raises(YunohostError): mocker.spy(m18n, "n") @@ -210,8 +214,9 @@ def test_apps_catalog_update_corrupted(mocker): with requests_mock.Mocker() as m: # Corrupted json - m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, - text=DUMMY_APP_CATALOG[:-2]) + m.register_uri( + "GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG[:-2] + ) with pytest.raises(YunohostError): mocker.spy(m18n, "n") @@ -252,8 +257,13 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): # Initialize ... _initialize_apps_catalog_system() - conf = [{"id": "default", "url": APPS_CATALOG_DEFAULT_URL}, - {"id": "default2", "url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org")}] + conf = [ + {"id": "default", "url": APPS_CATALOG_DEFAULT_URL}, + { + "id": "default2", + "url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org"), + }, + ] write_to_yaml(APPS_CATALOG_CONF, conf) @@ -263,7 +273,11 @@ def test_apps_catalog_load_with_conflicts_between_lists(mocker): # Mock the server response with a dummy apps catalog # + the same apps catalog for the second list m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG) - m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"), text=DUMMY_APP_CATALOG) + m.register_uri( + "GET", + APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"), + text=DUMMY_APP_CATALOG, + ) # Try to load the apps catalog # This should implicitly trigger an update in the background diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 3a2313b0e..be68dd743 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -30,9 +30,18 @@ def teardown_function(function): 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") + 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(): @@ -47,70 +56,152 @@ def test_urlavailable(): def test_registerurl(): - app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "register_url_app_ynh"), + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), + force=True, + ) assert not domain_url_available(maindomain, "/urlregisterapp") # Try installing at same location with pytest.raises(YunohostError): - app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "register_url_app_ynh"), + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"), + force=True, + ) def test_registerurl_baddomain(): with pytest.raises(YunohostError): - app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "register_url_app_ynh"), + args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"), + force=True, + ) def test_normalize_permission_path(): # Relative path - assert _validate_and_sanitize_permission_url("/wiki/", maindomain + '/path', 'test_permission') == "/wiki" - assert _validate_and_sanitize_permission_url("/", maindomain + '/path', 'test_permission') == "/" - assert _validate_and_sanitize_permission_url("//salut/", maindomain + '/path', 'test_permission') == "/salut" + assert ( + _validate_and_sanitize_permission_url( + "/wiki/", maindomain + "/path", "test_permission" + ) + == "/wiki" + ) + assert ( + _validate_and_sanitize_permission_url( + "/", maindomain + "/path", "test_permission" + ) + == "/" + ) + assert ( + _validate_and_sanitize_permission_url( + "//salut/", maindomain + "/path", "test_permission" + ) + == "/salut" + ) # Full path - assert _validate_and_sanitize_permission_url(maindomain + "/hey/", maindomain + '/path', 'test_permission') == maindomain + "/hey" - assert _validate_and_sanitize_permission_url(maindomain + "//", maindomain + '/path', 'test_permission') == maindomain + "/" - assert _validate_and_sanitize_permission_url(maindomain + "/", maindomain + '/path', 'test_permission') == maindomain + "/" + assert ( + _validate_and_sanitize_permission_url( + maindomain + "/hey/", maindomain + "/path", "test_permission" + ) + == maindomain + "/hey" + ) + assert ( + _validate_and_sanitize_permission_url( + maindomain + "//", maindomain + "/path", "test_permission" + ) + == maindomain + "/" + ) + assert ( + _validate_and_sanitize_permission_url( + maindomain + "/", maindomain + "/path", "test_permission" + ) + == maindomain + "/" + ) # Relative Regex - assert _validate_and_sanitize_permission_url("re:/yolo.*/", maindomain + '/path', 'test_permission') == "re:/yolo.*/" - assert _validate_and_sanitize_permission_url("re:/y.*o(o+)[a-z]*/bo\1y", maindomain + '/path', 'test_permission') == "re:/y.*o(o+)[a-z]*/bo\1y" + assert ( + _validate_and_sanitize_permission_url( + "re:/yolo.*/", maindomain + "/path", "test_permission" + ) + == "re:/yolo.*/" + ) + assert ( + _validate_and_sanitize_permission_url( + "re:/y.*o(o+)[a-z]*/bo\1y", maindomain + "/path", "test_permission" + ) + == "re:/y.*o(o+)[a-z]*/bo\1y" + ) # Full Regex - assert _validate_and_sanitize_permission_url("re:" + maindomain + "/yolo.*/", maindomain + '/path', 'test_permission') == "re:" + maindomain + "/yolo.*/" - assert _validate_and_sanitize_permission_url("re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y", maindomain + '/path', 'test_permission') == "re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y" + assert ( + _validate_and_sanitize_permission_url( + "re:" + maindomain + "/yolo.*/", maindomain + "/path", "test_permission" + ) + == "re:" + maindomain + "/yolo.*/" + ) + assert ( + _validate_and_sanitize_permission_url( + "re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y", + maindomain + "/path", + "test_permission", + ) + == "re:" + maindomain + "/y.*o(o+)[a-z]*/bo\1y" + ) def test_normalize_permission_path_with_bad_regex(): # Relative Regex with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("re:/yolo.*[1-7]^?/", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "re:/yolo.*[1-7]^?/", maindomain + "/path", "test_permission" + ) with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("re:/yolo.*[1-7](]/", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "re:/yolo.*[1-7](]/", maindomain + "/path", "test_permission" + ) # Full Regex with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("re:" + maindomain + "/yolo?+/", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "re:" + maindomain + "/yolo?+/", maindomain + "/path", "test_permission" + ) with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("re:" + maindomain + "/yolo[1-9]**/", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "re:" + maindomain + "/yolo[1-9]**/", + maindomain + "/path", + "test_permission", + ) def test_normalize_permission_path_with_unknown_domain(): with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("shouldntexist.tld/hey", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "shouldntexist.tld/hey", maindomain + "/path", "test_permission" + ) with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("re:shouldntexist.tld/hey.*", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + "re:shouldntexist.tld/hey.*", maindomain + "/path", "test_permission" + ) def test_normalize_permission_path_conflicting_path(): - app_install(os.path.join(get_test_apps_dir(), "register_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, "/url/registerapp"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "register_url_app_ynh"), + args="domain=%s&path=%s" % (maindomain, "/url/registerapp"), + force=True, + ) with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url("/registerapp", maindomain + '/url', 'test_permission') + _validate_and_sanitize_permission_url( + "/registerapp", maindomain + "/url", "test_permission" + ) with pytest.raises(YunohostError): - _validate_and_sanitize_permission_url(maindomain + "/url/registerapp", maindomain + '/path', 'test_permission') + _validate_and_sanitize_permission_url( + maindomain + "/url/registerapp", maindomain + "/path", "test_permission" + ) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index c1f211d1b..021566544 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -7,11 +7,21 @@ from .conftest import message, raiseYunohostError, get_test_apps_dir 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, _recursive_umount +from yunohost.backup import ( + backup_create, + backup_restore, + backup_list, + backup_info, + backup_delete, + _recursive_umount, +) from yunohost.domain import _get_maindomain, domain_list, domain_add, domain_remove from yunohost.user import user_create, user_list, user_delete from yunohost.permission import user_permission_list -from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps +from yunohost.tests.test_permission import ( + check_LDAP_db_integrity, + check_permission_for_apps, +) from yunohost.hook import CUSTOM_HOOK_FOLDER # Get main domain @@ -32,7 +42,10 @@ def setup_function(function): assert len(backup_list()["archives"]) == 0 - markers = {m.name: {'args': m.args, 'kwargs': m.kwargs} for m in function.__dict__.get("pytestmark", [])} + markers = { + m.name: {"args": m.args, "kwargs": m.kwargs} + for m in function.__dict__.get("pytestmark", []) + } if "with_wordpress_archive_from_2p4" in markers: add_archive_wordpress_from_2p4() @@ -45,14 +58,16 @@ def setup_function(function): 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") + 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") + 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: @@ -62,13 +77,12 @@ def setup_function(function): if "with_permission_app_installed" in markers: assert not app_is_installed("permissions_app") user_create("alice", "Alice", "White", maindomain, "test123Ynh") - install_app("permissions_app_ynh", "/urlpermissionapp" - "&admin=alice") + install_app("permissions_app_ynh", "/urlpermissionapp" "&admin=alice") assert app_is_installed("permissions_app") if "with_custom_domain" in markers: - domain = markers['with_custom_domain']['args'][0] - if domain not in domain_list()['domains']: + domain = markers["with_custom_domain"]["args"][0] + if domain not in domain_list()["domains"]: domain_add(domain) @@ -80,7 +94,10 @@ def teardown_function(function): delete_all_backups() uninstall_test_apps_if_needed() - markers = {m.name: {'args': m.args, 'kwargs': m.kwargs} for m in function.__dict__.get("pytestmark", [])} + markers = { + m.name: {"args": m.args, "kwargs": m.kwargs} + for m in function.__dict__.get("pytestmark", []) + } if "clean_opt_dir" in markers: shutil.rmtree("/opt/test_backup_output_directory") @@ -89,7 +106,7 @@ def teardown_function(function): user_delete("alice") if "with_custom_domain" in markers: - domain = markers['with_custom_domain']['args'][0] + domain = markers["with_custom_domain"]["args"][0] domain_remove(domain) @@ -106,6 +123,7 @@ def check_permission_for_apps_call(): yield check_permission_for_apps() + # # Helpers # # @@ -128,9 +146,13 @@ def app_is_installed(app): def backup_test_dependencies_are_met(): # Dummy test apps (or backup archives) - assert os.path.exists(os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4")) + assert os.path.exists( + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4") + ) assert os.path.exists(os.path.join(get_test_apps_dir(), "legacy_app_ynh")) - assert os.path.exists(os.path.join(get_test_apps_dir(), "backup_recommended_app_ynh")) + assert os.path.exists( + os.path.join(get_test_apps_dir(), "backup_recommended_app_ynh") + ) return True @@ -140,7 +162,7 @@ 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 + return len(os.listdir("/home/yunohost.backup/tmp/")) == 0 def clean_tmp_backup_directory(): @@ -150,15 +172,16 @@ def clean_tmp_backup_directory(): mount_lines = subprocess.check_output("mount").decode().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")] + 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/'): + for f in os.listdir("/home/yunohost.backup/tmp/"): shutil.rmtree("/home/yunohost.backup/tmp/%s" % f) shutil.rmtree("/home/yunohost.backup/tmp/") @@ -186,31 +209,48 @@ def uninstall_test_apps_if_needed(): def install_app(app, path, additionnal_args=""): - app_install(os.path.join(get_test_apps_dir(), app), - args="domain=%s&path=%s%s" % (maindomain, path, - additionnal_args), force=True) + app_install( + os.path.join(get_test_apps_dir(), app), + args="domain=%s&path=%s%s" % (maindomain, path, additionnal_args), + force=True, + ) def add_archive_wordpress_from_2p4(): os.system("mkdir -p /home/yunohost.backup/archives") - os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json") - + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json") + os.system( + "cp " + + os.path.join( + get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json" + ) + + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json" + ) - os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz") - + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz") + os.system( + "cp " + + os.path.join(get_test_apps_dir(), "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 " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json") - + " /home/yunohost.backup/archives/backup_system_from_2p4.info.json") + os.system( + "cp " + + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json") + + " /home/yunohost.backup/archives/backup_system_from_2p4.info.json" + ) + + os.system( + "cp " + + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz") + + " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz" + ) - os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz") - + " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz") # # System backup # @@ -235,7 +275,7 @@ def test_backup_only_ldap(mocker): def test_backup_system_part_that_does_not_exists(mocker): # Create the backup - with message(mocker, 'backup_hook_unknown', hook="doesnt_exist"): + with message(mocker, "backup_hook_unknown", hook="doesnt_exist"): with raiseYunohostError(mocker, "backup_nothings_done"): backup_create(system=["doesnt_exist"], apps=None) @@ -256,8 +296,9 @@ def test_backup_and_restore_all_sys(mocker): 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/"))) + 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") @@ -266,8 +307,7 @@ def test_backup_and_restore_all_sys(mocker): # Restore the backup with message(mocker, "restore_complete"): - backup_restore(name=archives[0], force=True, - system=[], apps=None) + backup_restore(name=archives[0], force=True, system=[], apps=None) # Check ssowat conf is back assert os.path.exists("/etc/ssowat/conf.json") @@ -277,6 +317,7 @@ def test_backup_and_restore_all_sys(mocker): # System restore from 2.4 # # + @pytest.mark.with_system_archive_from_2p4 def test_restore_system_from_Ynh2p4(monkeypatch, mocker): @@ -289,16 +330,15 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker): # Restore system archive from 2.4 try: with message(mocker, "restore_complete"): - backup_restore(name=backup_list()["archives"][1], - system=[], - apps=None, - force=True) + backup_restore( + name=backup_list()["archives"][1], system=[], apps=None, force=True + ) finally: # Restore system as it was - backup_restore(name=backup_list()["archives"][0], - system=[], - apps=None, - force=True) + backup_restore( + name=backup_list()["archives"][0], system=[], apps=None, force=True + ) + # # App backup # @@ -307,7 +347,6 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker): @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_"): @@ -320,14 +359,13 @@ def test_backup_script_failure_handling(monkeypatch, mocker): # with the expected error message key monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) - with message(mocker, 'backup_app_failed', app='backup_recommended_app'): - with raiseYunohostError(mocker, 'backup_nothings_done'): + with message(mocker, "backup_app_failed", app="backup_recommended_app"): + with raiseYunohostError(mocker, "backup_nothings_done"): backup_create(system=None, apps=["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 @@ -335,10 +373,11 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): return 0 monkeypatch.setattr("yunohost.backup.disk_usage", custom_disk_usage) - monkeypatch.setattr("yunohost.backup.free_space_in_directory", - custom_free_space_in_directory) + monkeypatch.setattr( + "yunohost.backup.free_space_in_directory", custom_free_space_in_directory + ) - with raiseYunohostError(mocker, 'not_enough_disk_space'): + with raiseYunohostError(mocker, "not_enough_disk_space"): backup_create(system=None, apps=["backup_recommended_app"]) @@ -347,7 +386,7 @@ def test_backup_app_not_installed(mocker): assert not _is_installed("wordpress") with message(mocker, "unbackup_app", app="wordpress"): - with raiseYunohostError(mocker, 'backup_nothings_done'): + with raiseYunohostError(mocker, "backup_nothings_done"): backup_create(system=None, apps=["wordpress"]) @@ -358,8 +397,10 @@ def test_backup_app_with_no_backup_script(mocker): os.system("rm %s" % backup_script) assert not os.path.exists(backup_script) - with message(mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app"): - with raiseYunohostError(mocker, 'backup_nothings_done'): + with message( + mocker, "backup_with_no_backup_script_for_app", app="backup_recommended_app" + ): + with raiseYunohostError(mocker, "backup_nothings_done"): backup_create(system=None, apps=["backup_recommended_app"]) @@ -373,7 +414,9 @@ def test_backup_app_with_no_restore_script(mocker): # Backuping an app with no restore script will only display a warning to the # user... - with message(mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app"): + with message( + mocker, "backup_with_no_restore_script_for_app", app="backup_recommended_app" + ): backup_create(system=None, apps=["backup_recommended_app"]) @@ -382,9 +425,12 @@ def test_backup_with_different_output_directory(mocker): # Create the backup with message(mocker, "backup_created"): - backup_create(system=["conf_ssh"], apps=None, - output_directory="/opt/test_backup_output_directory", - name="backup") + backup_create( + system=["conf_ssh"], + apps=None, + output_directory="/opt/test_backup_output_directory", + name="backup", + ) assert os.path.exists("/opt/test_backup_output_directory/backup.tar") @@ -402,10 +448,13 @@ def test_backup_using_copy_method(mocker): # Create the backup with message(mocker, "backup_created"): - backup_create(system=["conf_nginx"], apps=None, - output_directory="/opt/test_backup_output_directory", - methods=["copy"], - name="backup") + backup_create( + system=["conf_nginx"], + apps=None, + output_directory="/opt/test_backup_output_directory", + methods=["copy"], + name="backup", + ) assert os.path.exists("/opt/test_backup_output_directory/info.json") @@ -414,19 +463,20 @@ def test_backup_using_copy_method(mocker): # App restore # # + @pytest.mark.with_wordpress_archive_from_2p4 @pytest.mark.with_custom_domain("yolo.test") def test_restore_app_wordpress_from_Ynh2p4(mocker): with message(mocker, "restore_complete"): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["wordpress"] + ) @pytest.mark.with_wordpress_archive_from_2p4 @pytest.mark.with_custom_domain("yolo.test") 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() @@ -436,28 +486,30 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") - with message(mocker, 'restore_app_failed', app='wordpress'): - with raiseYunohostError(mocker, 'restore_nothings_done'): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + with message(mocker, "restore_app_failed", app="wordpress"): + with raiseYunohostError(mocker, "restore_nothings_done"): + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["wordpress"] + ) 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) + monkeypatch.setattr( + "yunohost.backup.free_space_in_directory", custom_free_space_in_directory + ) assert not _is_installed("wordpress") - with raiseYunohostError(mocker, 'restore_not_enough_disk_space'): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + with raiseYunohostError(mocker, "restore_not_enough_disk_space"): + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["wordpress"] + ) assert not _is_installed("wordpress") @@ -468,10 +520,11 @@ def test_restore_app_not_in_backup(mocker): assert not _is_installed("wordpress") assert not _is_installed("yoloswag") - with message(mocker, 'backup_archive_app_not_found', app="yoloswag"): - with raiseYunohostError(mocker, 'restore_nothings_done'): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["yoloswag"]) + with message(mocker, "backup_archive_app_not_found", app="yoloswag"): + with raiseYunohostError(mocker, "restore_nothings_done"): + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["yoloswag"] + ) assert not _is_installed("wordpress") assert not _is_installed("yoloswag") @@ -484,14 +537,16 @@ def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") with message(mocker, "restore_complete"): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["wordpress"] + ) assert _is_installed("wordpress") - with raiseYunohostError(mocker, 'restore_already_installed_apps'): - backup_restore(system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + with raiseYunohostError(mocker, "restore_already_installed_apps"): + backup_restore( + system=None, name=backup_list()["archives"][0], apps=["wordpress"] + ) assert _is_installed("wordpress") @@ -517,33 +572,33 @@ def test_backup_and_restore_with_ynh_restore(mocker): @pytest.mark.with_permission_app_installed def test_backup_and_restore_permission_app(mocker): - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['url'] == "/" - assert res['permissions_app.admin']['url'] == "/admin" - assert res['permissions_app.dev']['url'] == "/dev" + assert res["permissions_app.main"]["url"] == "/" + assert res["permissions_app.admin"]["url"] == "/admin" + assert res["permissions_app.dev"]["url"] == "/dev" - assert "visitors" in res['permissions_app.main']['allowed'] - assert "all_users" in res['permissions_app.main']['allowed'] - assert res['permissions_app.admin']['allowed'] == ["alice"] - assert res['permissions_app.dev']['allowed'] == [] + assert "visitors" in res["permissions_app.main"]["allowed"] + assert "all_users" in res["permissions_app.main"]["allowed"] + assert res["permissions_app.admin"]["allowed"] == ["alice"] + assert res["permissions_app.dev"]["allowed"] == [] _test_backup_and_restore_app(mocker, "permissions_app") - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['url'] == "/" - assert res['permissions_app.admin']['url'] == "/admin" - assert res['permissions_app.dev']['url'] == "/dev" + assert res["permissions_app.main"]["url"] == "/" + assert res["permissions_app.admin"]["url"] == "/admin" + assert res["permissions_app.dev"]["url"] == "/dev" - assert "visitors" in res['permissions_app.main']['allowed'] - assert "all_users" in res['permissions_app.main']['allowed'] - assert res['permissions_app.admin']['allowed'] == ["alice"] - assert res['permissions_app.dev']['allowed'] == [] + assert "visitors" in res["permissions_app.main"]["allowed"] + assert "all_users" in res["permissions_app.main"]["allowed"] + assert res["permissions_app.admin"]["allowed"] == ["alice"] + assert res["permissions_app.dev"]["allowed"] == [] def _test_backup_and_restore_app(mocker, app): @@ -563,19 +618,19 @@ def _test_backup_and_restore_app(mocker, app): # Uninstall the app app_remove(app) assert not app_is_installed(app) - assert app + ".main" not in user_permission_list()['permissions'] + assert app + ".main" not in user_permission_list()["permissions"] # Restore the app with message(mocker, "restore_complete"): - backup_restore(system=None, name=archives[0], - apps=[app]) + backup_restore(system=None, name=archives[0], apps=[app]) assert app_is_installed(app) # Check permission - per_list = user_permission_list()['permissions'] + per_list = user_permission_list()["permissions"] assert app + ".main" in per_list + # # Some edge cases # # @@ -589,7 +644,7 @@ def test_restore_archive_with_no_json(mocker): assert "badbackup" in backup_list()["archives"] - with raiseYunohostError(mocker, 'backup_archive_cant_retrieve_info_json'): + with raiseYunohostError(mocker, "backup_archive_cant_retrieve_info_json"): backup_restore(name="badbackup", force=True) @@ -597,11 +652,13 @@ def test_restore_archive_with_no_json(mocker): def test_restore_archive_with_bad_archive(mocker): # Break the archive - os.system("head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz") + os.system( + "head -n 1000 /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz > /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz" + ) assert "backup_wordpress_from_2p4" in backup_list()["archives"] - with raiseYunohostError(mocker, 'backup_archive_open_failed'): + with raiseYunohostError(mocker, "backup_archive_open_failed"): backup_restore(name="backup_wordpress_from_2p4", force=True) clean_tmp_backup_directory() @@ -609,7 +666,7 @@ def test_restore_archive_with_bad_archive(mocker): def test_restore_archive_with_custom_hook(mocker): - custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, 'restore') + custom_restore_hook_folder = os.path.join(CUSTOM_HOOK_FOLDER, "restore") os.system("touch %s/99-yolo" % custom_restore_hook_folder) # Backup with custom hook system @@ -620,22 +677,23 @@ def test_restore_archive_with_custom_hook(mocker): # Restore system with custom hook with message(mocker, "restore_complete"): - backup_restore(name=backup_list()["archives"][0], - system=[], - apps=None, - force=True) + backup_restore( + name=backup_list()["archives"][0], system=[], apps=None, force=True + ) os.system("rm %s/99-yolo" % custom_restore_hook_folder) def test_backup_binds_are_readonly(mocker, monkeypatch): - def custom_mount_and_backup(self): 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, env={'LANG': 'en_US.UTF-8'}) + output = subprocess.check_output( + "touch %s/test 2>&1 || true" % confssh, + shell=True, + env={"LANG": "en_US.UTF-8"}, + ) output = output.decode() assert "Read-only file system" in output @@ -645,8 +703,9 @@ def test_backup_binds_are_readonly(mocker, monkeypatch): self.clean() - monkeypatch.setattr("yunohost.backup.BackupMethod.mount_and_backup", - custom_mount_and_backup) + monkeypatch.setattr( + "yunohost.backup.BackupMethod.mount_and_backup", custom_mount_and_backup + ) # Create the backup with message(mocker, "backup_created"): diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index ef076e67b..e375bd9f0 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -24,8 +24,11 @@ def teardown_function(function): def install_changeurl_app(path): - app_install(os.path.join(get_test_apps_dir(), "change_url_app_ynh"), - args="domain=%s&path=%s" % (maindomain, path), force=True) + app_install( + os.path.join(get_test_apps_dir(), "change_url_app_ynh"), + args="domain=%s&path=%s" % (maindomain, path), + force=True, + ) def check_changeurl_app(path): @@ -35,7 +38,9 @@ def check_changeurl_app(path): assert appmap[maindomain][path]["id"] == "change_url_app" - r = requests.get("https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False) + r = requests.get( + "https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False + ) assert r.status_code == 200 diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index a1f411b80..1e26d9492 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -8,11 +8,32 @@ import shutil from .conftest import message, raiseYunohostError, get_test_apps_dir -from yunohost.app import app_install, app_upgrade, app_remove, app_change_url, app_map, _installed_apps, APPS_SETTING_PATH, _set_app_settings, _get_app_settings -from yunohost.user import user_list, user_create, user_delete, \ - user_group_list, user_group_delete -from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \ - permission_create, permission_delete, permission_url +from yunohost.app import ( + app_install, + app_upgrade, + app_remove, + app_change_url, + app_map, + _installed_apps, + APPS_SETTING_PATH, + _set_app_settings, + _get_app_settings, +) +from yunohost.user import ( + user_list, + user_create, + user_delete, + user_group_list, + user_group_delete, +) +from yunohost.permission import ( + user_permission_update, + user_permission_list, + user_permission_reset, + permission_create, + permission_delete, + permission_url, +) from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list # Get main domain @@ -26,54 +47,75 @@ dummy_password = "test123Ynh" prv_getaddrinfo = socket.getaddrinfo -def _permission_create_with_dummy_app(permission, allowed=None, - url=None, additional_urls=None, auth_header=True, - label=None, show_tile=False, - protected=True, sync_perm=True, - domain=None, path=None): - app = permission.split('.')[0] +def _permission_create_with_dummy_app( + permission, + allowed=None, + url=None, + additional_urls=None, + auth_header=True, + label=None, + show_tile=False, + protected=True, + sync_perm=True, + domain=None, + path=None, +): + app = permission.split(".")[0] if app not in _installed_apps(): app_setting_path = os.path.join(APPS_SETTING_PATH, app) if not os.path.exists(app_setting_path): os.makedirs(app_setting_path) - settings = {'id': app, 'dummy_permission_app': True} + settings = {"id": app, "dummy_permission_app": True} if domain: - settings['domain'] = domain + settings["domain"] = domain if path: - settings['path'] = path + settings["path"] = path _set_app_settings(app, settings) - with open(os.path.join(APPS_SETTING_PATH, app, 'manifest.json'), 'w') as f: - json.dump({ - "name": app, - "id": app, - "description": { - "en": "Dummy app to test permissions" - } - }, f) - permission_create(permission=permission, allowed=allowed, url=url, additional_urls=additional_urls, auth_header=auth_header, - label=label, show_tile=show_tile, protected=protected, sync_perm=sync_perm) + with open(os.path.join(APPS_SETTING_PATH, app, "manifest.json"), "w") as f: + json.dump( + { + "name": app, + "id": app, + "description": {"en": "Dummy app to test permissions"}, + }, + f, + ) + permission_create( + permission=permission, + allowed=allowed, + url=url, + additional_urls=additional_urls, + auth_header=auth_header, + label=label, + show_tile=show_tile, + protected=protected, + sync_perm=sync_perm, + ) def _clear_dummy_app_settings(): # Clean dummy app settings for app in _installed_apps(): - if _get_app_settings(app).get('dummy_permission_app', False): + if _get_app_settings(app).get("dummy_permission_app", False): app_setting_path = os.path.join(APPS_SETTING_PATH, app) if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) def clean_user_groups_permission(): - for u in user_list()['users']: + for u in user_list()["users"]: user_delete(u) - for g in user_group_list()['groups']: + for g in user_group_list()["groups"]: if g not in ["all_users", "visitors"]: user_group_delete(g) - for p in user_permission_list()['permissions']: - if any(p.startswith(name) for name in ["wiki", "blog", "site", "web", "permissions_app"]): + for p in user_permission_list()["permissions"]: + if any( + p.startswith(name) + for name in ["wiki", "blog", "site", "web", "permissions_app"] + ): permission_delete(p, force=True, sync_perm=False) socket.getaddrinfo = prv_getaddrinfo @@ -85,19 +127,25 @@ def setup_function(function): global other_domains maindomain = _get_maindomain() - markers = {m.name: {'args': m.args, 'kwargs': m.kwargs} for m in function.__dict__.get("pytestmark", [])} + markers = { + m.name: {"args": m.args, "kwargs": m.kwargs} + for m in function.__dict__.get("pytestmark", []) + } if "other_domains" in markers: - other_domains = ["domain_%s.dev" % string.ascii_lowercase[number] for number in range(markers['other_domains']['kwargs']['number'])] + other_domains = [ + "domain_%s.dev" % string.ascii_lowercase[number] + for number in range(markers["other_domains"]["kwargs"]["number"]) + ] for domain in other_domains: - if domain not in domain_list()['domains']: + if domain not in domain_list()["domains"]: domain_add(domain) # Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address. # Mainly used for 'can_access_webpage' function - dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, '', ('127.0.0.1', 443))]} + dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, "", ("127.0.0.1", 443))]} for domain in other_domains: - dns_cache[(domain, 443, 0, 1)] = [(2, 1, 6, '', ('127.0.0.1', 443))] + dns_cache[(domain, 443, 0, 1)] = [(2, 1, 6, "", ("127.0.0.1", 443))] def new_getaddrinfo(*args): try: @@ -106,19 +154,38 @@ def setup_function(function): res = prv_getaddrinfo(*args) dns_cache[args] = res return res + socket.getaddrinfo = new_getaddrinfo user_create("alice", "Alice", "White", maindomain, dummy_password) user_create("bob", "Bob", "Snow", maindomain, dummy_password) - _permission_create_with_dummy_app(permission="wiki.main", url="/", additional_urls=['/whatever', '/idontnow'], auth_header=False, - label="Wiki", show_tile=True, - allowed=["all_users"], protected=False, sync_perm=False, - domain=maindomain, path='/wiki') - _permission_create_with_dummy_app(permission="blog.main", url="/", auth_header=True, - show_tile=False, - protected=False, sync_perm=False, - allowed=["alice"], domain=maindomain, path='/blog') - _permission_create_with_dummy_app(permission="blog.api", allowed=["visitors"], protected=True, sync_perm=True) + _permission_create_with_dummy_app( + permission="wiki.main", + url="/", + additional_urls=["/whatever", "/idontnow"], + auth_header=False, + label="Wiki", + show_tile=True, + allowed=["all_users"], + protected=False, + sync_perm=False, + domain=maindomain, + path="/wiki", + ) + _permission_create_with_dummy_app( + permission="blog.main", + url="/", + auth_header=True, + show_tile=False, + protected=False, + sync_perm=False, + allowed=["alice"], + domain=maindomain, + path="/blog", + ) + _permission_create_with_dummy_app( + permission="blog.api", allowed=["visitors"], protected=True, sync_perm=True + ) def teardown_function(function): @@ -165,82 +232,105 @@ def check_LDAP_db_integrity(): # The other part is done by the the "permission_sync_to_user" function of the permission module from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract + ldap = _get_ldap_interface() - user_search = ldap.search('ou=users,dc=yunohost,dc=org', - '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', - ['uid', 'memberOf', 'permission']) - group_search = ldap.search('ou=groups,dc=yunohost,dc=org', - '(objectclass=groupOfNamesYnh)', - ['cn', 'member', 'memberUid', 'permission']) - permission_search = ldap.search('ou=permission,dc=yunohost,dc=org', - '(objectclass=permissionYnh)', - ['cn', 'groupPermission', 'inheritPermission', 'memberUid']) + user_search = ldap.search( + "ou=users,dc=yunohost,dc=org", + "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", + ["uid", "memberOf", "permission"], + ) + group_search = ldap.search( + "ou=groups,dc=yunohost,dc=org", + "(objectclass=groupOfNamesYnh)", + ["cn", "member", "memberUid", "permission"], + ) + permission_search = ldap.search( + "ou=permission,dc=yunohost,dc=org", + "(objectclass=permissionYnh)", + ["cn", "groupPermission", "inheritPermission", "memberUid"], + ) - user_map = {u['uid'][0]: u for u in user_search} - group_map = {g['cn'][0]: g for g in group_search} - permission_map = {p['cn'][0]: p for p in permission_search} + user_map = {u["uid"][0]: u for u in user_search} + group_map = {g["cn"][0]: g for g in group_search} + permission_map = {p["cn"][0]: p for p in permission_search} for user in user_search: - user_dn = 'uid=' + user['uid'][0] + ',ou=users,dc=yunohost,dc=org' - group_list = [_ldap_path_extract(m, "cn") for m in user['memberOf']] - permission_list = [_ldap_path_extract(m, "cn") for m in user.get('permission', [])] + user_dn = "uid=" + user["uid"][0] + ",ou=users,dc=yunohost,dc=org" + group_list = [_ldap_path_extract(m, "cn") for m in user["memberOf"]] + permission_list = [ + _ldap_path_extract(m, "cn") for m in user.get("permission", []) + ] # This user's DN sould be found in all groups it is a member of for group in group_list: - assert user_dn in group_map[group]['member'] + assert user_dn in group_map[group]["member"] # This user's DN should be found in all perms it has access to for permission in permission_list: - assert user_dn in permission_map[permission]['inheritPermission'] + assert user_dn in permission_map[permission]["inheritPermission"] for permission in permission_search: - permission_dn = 'cn=' + permission['cn'][0] + ',ou=permission,dc=yunohost,dc=org' + permission_dn = ( + "cn=" + permission["cn"][0] + ",ou=permission,dc=yunohost,dc=org" + ) # inheritPermission uid's should match memberUids - user_list = [_ldap_path_extract(m, "uid") for m in permission.get('inheritPermission', [])] - assert set(user_list) == set(permission.get('memberUid', [])) + user_list = [ + _ldap_path_extract(m, "uid") + for m in permission.get("inheritPermission", []) + ] + assert set(user_list) == set(permission.get("memberUid", [])) # This perm's DN should be found on all related users it is related to for user in user_list: - assert permission_dn in user_map[user]['permission'] + assert permission_dn in user_map[user]["permission"] # Same for groups : we should find the permission's DN for all related groups - group_list = [_ldap_path_extract(m, "cn") for m in permission.get('groupPermission', [])] + group_list = [ + _ldap_path_extract(m, "cn") for m in permission.get("groupPermission", []) + ] for group in group_list: - assert permission_dn in group_map[group]['permission'] + assert permission_dn in group_map[group]["permission"] # The list of user in the group should be a subset of all users related to the current permission - users_in_group = [_ldap_path_extract(m, "uid") for m in group_map[group].get("member", [])] + users_in_group = [ + _ldap_path_extract(m, "uid") for m in group_map[group].get("member", []) + ] assert set(users_in_group) <= set(user_list) for group in group_search: - group_dn = 'cn=' + group['cn'][0] + ',ou=groups,dc=yunohost,dc=org' + group_dn = "cn=" + group["cn"][0] + ",ou=groups,dc=yunohost,dc=org" user_list = [_ldap_path_extract(m, "uid") for m in group.get("member", [])] # For primary groups, we should find that : # - len(user_list) is 1 (a primary group has only 1 member) # - the group name should be an existing yunohost user # - memberUid is empty (meaning no other member than the corresponding user) - if group['cn'][0] in user_list: + if group["cn"][0] in user_list: assert len(user_list) == 1 assert group["cn"][0] in user_map - assert group.get('memberUid', []) == [] + assert group.get("memberUid", []) == [] # Otherwise, user_list and memberUid should have the same content else: - assert set(user_list) == set(group.get('memberUid', [])) + assert set(user_list) == set(group.get("memberUid", [])) # For all users members, this group should be in the "memberOf" on the other side for user in user_list: - assert group_dn in user_map[user]['memberOf'] + assert group_dn in user_map[user]["memberOf"] # For all the permissions of this group, the group should be among the "groupPermission" on the other side - permission_list = [_ldap_path_extract(m, "cn") for m in group.get('permission', [])] + permission_list = [ + _ldap_path_extract(m, "cn") for m in group.get("permission", []) + ] for permission in permission_list: - assert group_dn in permission_map[permission]['groupPermission'] + assert group_dn in permission_map[permission]["groupPermission"] # And the list of user of this group (user_list) should be a subset of all allowed users for this perm... - allowed_user_list = [_ldap_path_extract(m, "uid") for m in permission_map[permission].get('inheritPermission', [])] + allowed_user_list = [ + _ldap_path_extract(m, "uid") + for m in permission_map[permission].get("inheritPermission", []) + ] assert set(user_list) <= set(allowed_user_list) @@ -273,12 +363,15 @@ def can_access_webpage(webpath, logged_as=None): # Login as a user using dummy password else: with requests.Session() as session: - session.post(sso_url, - data={"user": logged_as, - "password": dummy_password}, - headers={"Referer": sso_url, - "Content-Type": "application/x-www-form-urlencoded"}, - verify=False) + session.post( + sso_url, + data={"user": logged_as, "password": dummy_password}, + headers={ + "Referer": sso_url, + "Content-Type": "application/x-www-form-urlencoded", + }, + verify=False, + ) # We should have some cookies related to authentication now assert session.cookies r = session.get(webpath, verify=False) @@ -291,8 +384,9 @@ def can_access_webpage(webpath, logged_as=None): # List functions # + def test_permission_list(): - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "mail.main" in res assert "xmpp.main" in res @@ -301,36 +395,39 @@ def test_permission_list(): assert "blog.main" in res assert "blog.api" in res - assert res['wiki.main']['allowed'] == ["all_users"] - assert res['blog.main']['allowed'] == ["alice"] - assert res['blog.api']['allowed'] == ["visitors"] - assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"]) - assert res['blog.main']['corresponding_users'] == ["alice"] - assert res['blog.api']['corresponding_users'] == [] - assert res['wiki.main']['url'] == "/" - assert res['blog.main']['url'] == "/" - assert res['blog.api']['url'] is None - assert set(res['wiki.main']['additional_urls']) == {'/whatever', '/idontnow'} - assert res['wiki.main']['protected'] is False - assert res['blog.main']['protected'] is False - assert res['blog.api']['protected'] is True - assert res['wiki.main']['label'] == "Wiki" - assert res['blog.main']['label'] == "Blog" - assert res['blog.api']['label'] == "Blog (api)" - assert res['wiki.main']['show_tile'] is True - assert res['blog.main']['show_tile'] is False - assert res['blog.api']['show_tile'] is False - assert res['wiki.main']['auth_header'] is False - assert res['blog.main']['auth_header'] is True - assert res['blog.api']['auth_header'] is True + assert res["wiki.main"]["allowed"] == ["all_users"] + assert res["blog.main"]["allowed"] == ["alice"] + assert res["blog.api"]["allowed"] == ["visitors"] + assert set(res["wiki.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert res["blog.main"]["corresponding_users"] == ["alice"] + assert res["blog.api"]["corresponding_users"] == [] + assert res["wiki.main"]["url"] == "/" + assert res["blog.main"]["url"] == "/" + assert res["blog.api"]["url"] is None + assert set(res["wiki.main"]["additional_urls"]) == {"/whatever", "/idontnow"} + assert res["wiki.main"]["protected"] is False + assert res["blog.main"]["protected"] is False + assert res["blog.api"]["protected"] is True + assert res["wiki.main"]["label"] == "Wiki" + assert res["blog.main"]["label"] == "Blog" + assert res["blog.api"]["label"] == "Blog (api)" + assert res["wiki.main"]["show_tile"] is True + assert res["blog.main"]["show_tile"] is False + assert res["blog.api"]["show_tile"] is False + assert res["wiki.main"]["auth_header"] is False + assert res["blog.main"]["auth_header"] is True + assert res["blog.api"]["auth_header"] is True - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res['wiki.main']['url'] == maindomain + "/wiki" - assert res['blog.main']['url'] == maindomain + "/blog" - assert res['blog.api']['url'] is None - assert set(res['wiki.main']['additional_urls']) == {maindomain + '/wiki/whatever', maindomain + '/wiki/idontnow'} - assert res['blog.main']['additional_urls'] == [] - assert res['blog.api']['additional_urls'] == [] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["wiki.main"]["url"] == maindomain + "/wiki" + assert res["blog.main"]["url"] == maindomain + "/blog" + assert res["blog.api"]["url"] is None + assert set(res["wiki.main"]["additional_urls"]) == { + maindomain + "/wiki/whatever", + maindomain + "/wiki/idontnow", + } + assert res["blog.main"]["additional_urls"] == [] + assert res["blog.api"]["additional_urls"] == [] # @@ -342,122 +439,156 @@ def test_permission_create_main(mocker): with message(mocker, "permission_created", permission="site.main"): permission_create("site.main", allowed=["all_users"], protected=False) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "site.main" in res - assert res['site.main']['allowed'] == ["all_users"] - assert set(res['site.main']['corresponding_users']) == set(["alice", "bob"]) - assert res['site.main']['protected'] is False + assert res["site.main"]["allowed"] == ["all_users"] + assert set(res["site.main"]["corresponding_users"]) == set(["alice", "bob"]) + assert res["site.main"]["protected"] is False def test_permission_create_extra(mocker): with message(mocker, "permission_created", permission="site.test"): permission_create("site.test") - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "site.test" in res # all_users is only enabled by default on .main perms - assert "all_users" not in res['site.test']['allowed'] - assert res['site.test']['corresponding_users'] == [] - assert res['site.test']['protected'] is False + assert "all_users" not in res["site.test"]["allowed"] + assert res["site.test"]["corresponding_users"] == [] + assert res["site.test"]["protected"] is False def test_permission_create_with_specific_user(): permission_create("site.test", allowed=["alice"]) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "site.test" in res - assert res['site.test']['allowed'] == ["alice"] + assert res["site.test"]["allowed"] == ["alice"] def test_permission_create_with_tile_management(mocker): with message(mocker, "permission_created", permission="site.main"): - _permission_create_with_dummy_app("site.main", allowed=["all_users"], - label="The Site", show_tile=False, - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "site.main", + allowed=["all_users"], + label="The Site", + show_tile=False, + domain=maindomain, + path="/site", + ) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "site.main" in res - assert res['site.main']['label'] == "The Site" - assert res['site.main']['show_tile'] is False + assert res["site.main"]["label"] == "The Site" + assert res["site.main"]["show_tile"] is False def test_permission_create_with_tile_management_with_main_default_value(mocker): with message(mocker, "permission_created", permission="site.main"): - _permission_create_with_dummy_app("site.main", allowed=["all_users"], show_tile=True, url="/", - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "site.main", + allowed=["all_users"], + show_tile=True, + url="/", + domain=maindomain, + path="/site", + ) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "site.main" in res - assert res['site.main']['label'] == "Site" - assert res['site.main']['show_tile'] is True + assert res["site.main"]["label"] == "Site" + assert res["site.main"]["show_tile"] is True def test_permission_create_with_tile_management_with_not_main_default_value(mocker): with message(mocker, "permission_created", permission="wiki.api"): - _permission_create_with_dummy_app("wiki.api", allowed=["all_users"], show_tile=True, url="/", - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "wiki.api", + allowed=["all_users"], + show_tile=True, + url="/", + domain=maindomain, + path="/site", + ) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "wiki.api" in res - assert res['wiki.api']['label'] == "Wiki (api)" - assert res['wiki.api']['show_tile'] is True + assert res["wiki.api"]["label"] == "Wiki (api)" + assert res["wiki.api"]["show_tile"] is True def test_permission_create_with_urls_management_without_url(mocker): with message(mocker, "permission_created", permission="wiki.api"): - _permission_create_with_dummy_app("wiki.api", allowed=["all_users"], - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "wiki.api", allowed=["all_users"], domain=maindomain, path="/site" + ) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "wiki.api" in res - assert res['wiki.api']['url'] is None - assert res['wiki.api']['additional_urls'] == [] - assert res['wiki.api']['auth_header'] is True + assert res["wiki.api"]["url"] is None + assert res["wiki.api"]["additional_urls"] == [] + assert res["wiki.api"]["auth_header"] is True def test_permission_create_with_urls_management_simple_domain(mocker): with message(mocker, "permission_created", permission="site.main"): - _permission_create_with_dummy_app("site.main", allowed=["all_users"], - url="/", additional_urls=['/whatever', '/idontnow'], auth_header=False, - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "site.main", + allowed=["all_users"], + url="/", + additional_urls=["/whatever", "/idontnow"], + auth_header=False, + domain=maindomain, + path="/site", + ) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] assert "site.main" in res - assert res['site.main']['url'] == maindomain + "/site" - assert set(res['site.main']['additional_urls']) == {maindomain + "/site/whatever", maindomain + "/site/idontnow"} - assert res['site.main']['auth_header'] is False + assert res["site.main"]["url"] == maindomain + "/site" + assert set(res["site.main"]["additional_urls"]) == { + maindomain + "/site/whatever", + maindomain + "/site/idontnow", + } + assert res["site.main"]["auth_header"] is False @pytest.mark.other_domains(number=2) def test_permission_create_with_urls_management_multiple_domain(mocker): with message(mocker, "permission_created", permission="site.main"): - _permission_create_with_dummy_app("site.main", allowed=["all_users"], - url=maindomain + "/site/something", - additional_urls=[other_domains[0] + "/blabla", - other_domains[1] + "/ahh"], - auth_header=True, - domain=maindomain, path='/site') + _permission_create_with_dummy_app( + "site.main", + allowed=["all_users"], + url=maindomain + "/site/something", + additional_urls=[other_domains[0] + "/blabla", other_domains[1] + "/ahh"], + auth_header=True, + domain=maindomain, + path="/site", + ) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] assert "site.main" in res - assert res['site.main']['url'] == maindomain + "/site/something" - assert set(res['site.main']['additional_urls']) == {other_domains[0] + "/blabla", other_domains[1] + "/ahh"} - assert res['site.main']['auth_header'] is True + assert res["site.main"]["url"] == maindomain + "/site/something" + assert set(res["site.main"]["additional_urls"]) == { + other_domains[0] + "/blabla", + other_domains[1] + "/ahh", + } + assert res["site.main"]["auth_header"] is True def test_permission_delete(mocker): with message(mocker, "permission_deleted", permission="wiki.main"): permission_delete("wiki.main", force=True) - res = user_permission_list()['permissions'] + res = user_permission_list()["permissions"] assert "wiki.main" not in res with message(mocker, "permission_deleted", permission="blog.api"): permission_delete("blog.api", force=False) - res = user_permission_list()['permissions'] + res = user_permission_list()["permissions"] assert "blog.api" not in res + # # Error on create - remove function # @@ -472,7 +603,7 @@ def test_permission_delete_doesnt_existing(mocker): with raiseYunohostError(mocker, "permission_not_found"): permission_delete("doesnt.exist", force=True) - res = user_permission_list()['permissions'] + res = user_permission_list()["permissions"] assert "wiki.main" in res assert "blog.main" in res assert "mail.main" in res @@ -483,9 +614,10 @@ def test_permission_delete_main_without_force(mocker): with raiseYunohostError(mocker, "permission_cannot_remove_main"): permission_delete("blog.main") - res = user_permission_list()['permissions'] + res = user_permission_list()["permissions"] assert "blog.main" in res + # # Update functions # @@ -497,54 +629,58 @@ def test_permission_add_group(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", add="alice") - res = user_permission_list(full=True)['permissions'] - assert set(res['wiki.main']['allowed']) == set(["all_users", "alice"]) - assert set(res['wiki.main']['corresponding_users']) == set(["alice", "bob"]) + res = user_permission_list(full=True)["permissions"] + assert set(res["wiki.main"]["allowed"]) == set(["all_users", "alice"]) + assert set(res["wiki.main"]["corresponding_users"]) == set(["alice", "bob"]) def test_permission_remove_group(mocker): with message(mocker, "permission_updated", permission="blog.main"): user_permission_update("blog.main", remove="alice") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == [] - assert res['blog.main']['corresponding_users'] == [] + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == [] + assert res["blog.main"]["corresponding_users"] == [] def test_permission_add_and_remove_group(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", add="alice", remove="all_users") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['allowed'] == ["alice"] - assert res['wiki.main']['corresponding_users'] == ["alice"] + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["allowed"] == ["alice"] + assert res["wiki.main"]["corresponding_users"] == ["alice"] def test_permission_add_group_already_allowed(mocker): - with message(mocker, "permission_already_allowed", permission="blog.main", group="alice"): + with message( + mocker, "permission_already_allowed", permission="blog.main", group="alice" + ): user_permission_update("blog.main", add="alice") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == ["alice"] - assert res['blog.main']['corresponding_users'] == ["alice"] + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == ["alice"] + assert res["blog.main"]["corresponding_users"] == ["alice"] def test_permission_remove_group_already_not_allowed(mocker): - with message(mocker, "permission_already_disallowed", permission="blog.main", group="bob"): + with message( + mocker, "permission_already_disallowed", permission="blog.main", group="bob" + ): user_permission_update("blog.main", remove="bob") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == ["alice"] - assert res['blog.main']['corresponding_users'] == ["alice"] + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == ["alice"] + assert res["blog.main"]["corresponding_users"] == ["alice"] def test_permission_reset(mocker): with message(mocker, "permission_updated", permission="blog.main"): user_permission_reset("blog.main") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == ["all_users"] - assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"]) + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == ["all_users"] + assert set(res["blog.main"]["corresponding_users"]) == set(["alice", "bob"]) def test_permission_reset_idempotency(): @@ -552,25 +688,25 @@ def test_permission_reset_idempotency(): user_permission_reset("blog.main") user_permission_reset("blog.main") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == ["all_users"] - assert set(res['blog.main']['corresponding_users']) == set(["alice", "bob"]) + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == ["all_users"] + assert set(res["blog.main"]["corresponding_users"]) == set(["alice", "bob"]) def test_permission_change_label(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", label="New Wiki") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['label'] == "New Wiki" + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["label"] == "New Wiki" def test_permission_change_label_with_same_value(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", label="Wiki") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['label'] == "Wiki" + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["label"] == "Wiki" def test_permission_switch_show_tile(mocker): @@ -579,15 +715,15 @@ def test_permission_switch_show_tile(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", show_tile="false") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['show_tile'] is False + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["show_tile"] is False # Try with uppercase with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", show_tile="TRUE") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['show_tile'] is True + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["show_tile"] is True def test_permission_switch_show_tile_with_same_value(mocker): @@ -595,8 +731,8 @@ def test_permission_switch_show_tile_with_same_value(mocker): with message(mocker, "permission_updated", permission="wiki.main"): user_permission_update("wiki.main", show_tile="True") - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['show_tile'] is True + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["show_tile"] is True # @@ -608,9 +744,9 @@ def test_permission_add_group_that_doesnt_exist(mocker): with raiseYunohostError(mocker, "group_unknown"): user_permission_update("blog.main", add="doesnt_exist") - res = user_permission_list(full=True)['permissions'] - assert res['blog.main']['allowed'] == ["alice"] - assert res['blog.main']['corresponding_users'] == ["alice"] + res = user_permission_list(full=True)["permissions"] + assert res["blog.main"]["allowed"] == ["alice"] + assert res["blog.main"]["corresponding_users"] == ["alice"] def test_permission_update_permission_that_doesnt_exist(mocker): @@ -619,24 +755,24 @@ def test_permission_update_permission_that_doesnt_exist(mocker): def test_permission_protected_update(mocker): - res = user_permission_list(full=True)['permissions'] - assert res['blog.api']['allowed'] == ["visitors"] + res = user_permission_list(full=True)["permissions"] + assert res["blog.api"]["allowed"] == ["visitors"] with raiseYunohostError(mocker, "permission_protected"): user_permission_update("blog.api", remove="visitors") - res = user_permission_list(full=True)['permissions'] - assert res['blog.api']['allowed'] == ["visitors"] + res = user_permission_list(full=True)["permissions"] + assert res["blog.api"]["allowed"] == ["visitors"] user_permission_update("blog.api", remove="visitors", force=True) - res = user_permission_list(full=True)['permissions'] - assert res['blog.api']['allowed'] == [] + res = user_permission_list(full=True)["permissions"] + assert res["blog.api"]["allowed"] == [] with raiseYunohostError(mocker, "permission_protected"): user_permission_update("blog.api", add="visitors") - res = user_permission_list(full=True)['permissions'] - assert res['blog.api']['allowed'] == [] + res = user_permission_list(full=True)["permissions"] + assert res["blog.api"]["allowed"] == [] # Permission url management @@ -645,25 +781,27 @@ def test_permission_protected_update(mocker): def test_permission_redefine_url(): permission_url("blog.main", url="/pwet") - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["url"] == "/pwet" def test_permission_remove_url(): permission_url("blog.main", clear_urls=True) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["url"] is None def test_permission_main_url_regex(): permission_url("blog.main", url="re:/[a-z]+reboy/.*") - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["url"] == "re:/[a-z]+reboy/.*" - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res["blog.main"]["url"] == "re:%s/blog/[a-z]+reboy/.*" % maindomain.replace('.', r'\.') + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["blog.main"]["url"] == "re:%s/blog/[a-z]+reboy/.*" % maindomain.replace( + ".", r"\." + ) def test_permission_main_url_bad_regex(mocker): @@ -675,22 +813,26 @@ def test_permission_main_url_bad_regex(mocker): def test_permission_add_additional_url(): permission_url("wiki.main", add_url=[other_domains[0] + "/heyby", "/myhouse"]) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res['wiki.main']['url'] == maindomain + "/wiki" - assert set(res['wiki.main']['additional_urls']) == {maindomain + '/wiki/whatever', - maindomain + '/wiki/idontnow', - other_domains[0] + "/heyby", - maindomain + '/wiki/myhouse'} + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["wiki.main"]["url"] == maindomain + "/wiki" + assert set(res["wiki.main"]["additional_urls"]) == { + maindomain + "/wiki/whatever", + maindomain + "/wiki/idontnow", + other_domains[0] + "/heyby", + maindomain + "/wiki/myhouse", + } def test_permission_add_additional_regex(): permission_url("blog.main", add_url=["re:/[a-z]+reboy/.*"]) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert res["blog.main"]["additional_urls"] == ["re:/[a-z]+reboy/.*"] - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res["blog.main"]["additional_urls"] == ["re:%s/blog/[a-z]+reboy/.*" % maindomain.replace('.', r'\.')] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["blog.main"]["additional_urls"] == [ + "re:%s/blog/[a-z]+reboy/.*" % maindomain.replace(".", r"\.") + ] def test_permission_add_additional_bad_regex(mocker): @@ -699,130 +841,153 @@ def test_permission_add_additional_bad_regex(mocker): def test_permission_remove_additional_url(): - permission_url("wiki.main", remove_url=['/whatever']) + permission_url("wiki.main", remove_url=["/whatever"]) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res['wiki.main']['url'] == maindomain + "/wiki" - assert res['wiki.main']['additional_urls'] == [maindomain + '/wiki/idontnow'] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["wiki.main"]["url"] == maindomain + "/wiki" + assert res["wiki.main"]["additional_urls"] == [maindomain + "/wiki/idontnow"] def test_permssion_add_additional_url_already_exist(): - permission_url("wiki.main", add_url=['/whatever', "/myhouse"]) - permission_url("wiki.main", add_url=['/whatever']) + permission_url("wiki.main", add_url=["/whatever", "/myhouse"]) + permission_url("wiki.main", add_url=["/whatever"]) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res['wiki.main']['url'] == maindomain + "/wiki" - assert set(res['wiki.main']['additional_urls']) == {maindomain + '/wiki/whatever', - maindomain + '/wiki/idontnow', - maindomain + '/wiki/myhouse'} + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["wiki.main"]["url"] == maindomain + "/wiki" + assert set(res["wiki.main"]["additional_urls"]) == { + maindomain + "/wiki/whatever", + maindomain + "/wiki/idontnow", + maindomain + "/wiki/myhouse", + } def test_permission_remove_additional_url_dont_exist(): - permission_url("wiki.main", remove_url=['/shouldntexist', '/whatever']) - permission_url("wiki.main", remove_url=['/shouldntexist']) + permission_url("wiki.main", remove_url=["/shouldntexist", "/whatever"]) + permission_url("wiki.main", remove_url=["/shouldntexist"]) - res = user_permission_list(full=True, absolute_urls=True)['permissions'] - assert res['wiki.main']['url'] == maindomain + "/wiki" - assert res['wiki.main']['additional_urls'] == [maindomain + '/wiki/idontnow'] + res = user_permission_list(full=True, absolute_urls=True)["permissions"] + assert res["wiki.main"]["url"] == maindomain + "/wiki" + assert res["wiki.main"]["additional_urls"] == [maindomain + "/wiki/idontnow"] def test_permission_clear_additional_url(): permission_url("wiki.main", clear_urls=True) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['url'] is None - assert res['wiki.main']['additional_urls'] == [] + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["url"] is None + assert res["wiki.main"]["additional_urls"] == [] def test_permission_switch_auth_header(): permission_url("wiki.main", auth_header=True) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['auth_header'] is True + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["auth_header"] is True permission_url("wiki.main", auth_header=False) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['auth_header'] is False + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["auth_header"] is False def test_permission_switch_auth_header_with_same_value(): permission_url("wiki.main", auth_header=False) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['auth_header'] is False + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["auth_header"] is False # Permission protected + def test_permission_switch_protected(): user_permission_update("wiki.main", protected=True) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['protected'] is True + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["protected"] is True user_permission_update("wiki.main", protected=False) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['protected'] is False + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["protected"] is False def test_permission_switch_protected_with_same_value(): user_permission_update("wiki.main", protected=False) - res = user_permission_list(full=True)['permissions'] - assert res['wiki.main']['protected'] is False + res = user_permission_list(full=True)["permissions"] + assert res["wiki.main"]["protected"] is False # Test SSOWAT conf generation + def test_ssowat_conf(): with open("/etc/ssowat/conf.json") as f: res = json.load(f) - permissions = res['permissions'] + permissions = res["permissions"] assert "wiki.main" in permissions assert "blog.main" in permissions - assert "blog.api" not in permissions # blog.api has no url/additional url defined and therefore is not added to ssowat conf + assert ( + "blog.api" not in permissions + ) # blog.api has no url/additional url defined and therefore is not added to ssowat conf - assert set(permissions['wiki.main']['users']) == {'alice', 'bob'} - assert permissions['blog.main']['users'] == ['alice'] + assert set(permissions["wiki.main"]["users"]) == {"alice", "bob"} + assert permissions["blog.main"]["users"] == ["alice"] - assert permissions['wiki.main']['uris'][0] == maindomain + "/wiki" + assert permissions["wiki.main"]["uris"][0] == maindomain + "/wiki" - assert set(permissions['wiki.main']['uris']) == {maindomain + "/wiki", - maindomain + "/wiki/whatever", - maindomain + "/wiki/idontnow"} - assert permissions['blog.main']['uris'] == [maindomain + "/blog"] + assert set(permissions["wiki.main"]["uris"]) == { + maindomain + "/wiki", + maindomain + "/wiki/whatever", + maindomain + "/wiki/idontnow", + } + assert permissions["blog.main"]["uris"] == [maindomain + "/blog"] - assert permissions['wiki.main']['public'] is False - assert permissions['blog.main']['public'] is False + assert permissions["wiki.main"]["public"] is False + assert permissions["blog.main"]["public"] is False - assert permissions['wiki.main']['auth_header'] is False - assert permissions['blog.main']['auth_header'] is True + assert permissions["wiki.main"]["auth_header"] is False + assert permissions["blog.main"]["auth_header"] is True - assert permissions['wiki.main']['label'] == "Wiki" - assert permissions['blog.main']['label'] == "Blog" + assert permissions["wiki.main"]["label"] == "Wiki" + assert permissions["blog.main"]["label"] == "Blog" - assert permissions['wiki.main']['show_tile'] is True - assert permissions['blog.main']['show_tile'] is False + assert permissions["wiki.main"]["show_tile"] is True + assert permissions["blog.main"]["show_tile"] is False def test_show_tile_cant_be_enabled(): - _permission_create_with_dummy_app(permission="site.main", auth_header=False, - label="Site", show_tile=True, - allowed=["all_users"], protected=False, sync_perm=False, - domain=maindomain, path="/site") + _permission_create_with_dummy_app( + permission="site.main", + auth_header=False, + label="Site", + show_tile=True, + allowed=["all_users"], + protected=False, + sync_perm=False, + domain=maindomain, + path="/site", + ) - _permission_create_with_dummy_app(permission="web.main", url="re:/[a-z]{3}/bla", auth_header=False, - label="Web", show_tile=True, - allowed=["all_users"], protected=False, sync_perm=True, - domain=maindomain, path="/web") + _permission_create_with_dummy_app( + permission="web.main", + url="re:/[a-z]{3}/bla", + auth_header=False, + label="Web", + show_tile=True, + allowed=["all_users"], + protected=False, + sync_perm=True, + domain=maindomain, + path="/web", + ) - permissions = user_permission_list(full=True)['permissions'] + permissions = user_permission_list(full=True)["permissions"] - assert permissions['site.main']['show_tile'] is False - assert permissions['web.main']['show_tile'] is False + assert permissions["site.main"]["show_tile"] is False + assert permissions["web.main"]["show_tile"] is False # @@ -832,25 +997,31 @@ def test_show_tile_cant_be_enabled(): @pytest.mark.other_domains(number=1) def test_permission_app_install(): - app_install(os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" + % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + force=True, + ) - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert "permissions_app.main" in res assert "permissions_app.admin" in res assert "permissions_app.dev" in res - assert res['permissions_app.main']['url'] == "/" - assert res['permissions_app.admin']['url'] == "/admin" - assert res['permissions_app.dev']['url'] == "/dev" + assert res["permissions_app.main"]["url"] == "/" + assert res["permissions_app.admin"]["url"] == "/admin" + assert res["permissions_app.dev"]["url"] == "/dev" - assert res['permissions_app.main']['allowed'] == ["all_users"] - assert set(res['permissions_app.main']['corresponding_users']) == set(["alice", "bob"]) + assert res["permissions_app.main"]["allowed"] == ["all_users"] + assert set(res["permissions_app.main"]["corresponding_users"]) == set( + ["alice", "bob"] + ) - assert res['permissions_app.admin']['allowed'] == ["alice"] - assert res['permissions_app.admin']['corresponding_users'] == ["alice"] + assert res["permissions_app.admin"]["allowed"] == ["alice"] + assert res["permissions_app.admin"]["corresponding_users"] == ["alice"] - assert res['permissions_app.dev']['allowed'] == [] - assert set(res['permissions_app.dev']['corresponding_users']) == set() + assert res["permissions_app.dev"]["allowed"] == [] + assert set(res["permissions_app.dev"]["corresponding_users"]) == set() # Check that we get the right stuff in app_map, which is used to generate the ssowatconf assert maindomain + "/urlpermissionapp" in app_map(user="alice").keys() @@ -861,68 +1032,89 @@ def test_permission_app_install(): @pytest.mark.other_domains(number=1) def test_permission_app_remove(): - app_install(os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + args="domain=%s&domain_2=%s&path=%s&is_public=0&admin=%s" + % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + force=True, + ) app_remove("permissions_app") # Check all permissions for this app got deleted - res = user_permission_list(full=True)['permissions'] + res = user_permission_list(full=True)["permissions"] assert not any(p.startswith("permissions_app.") for p in res.keys()) @pytest.mark.other_domains(number=1) def test_permission_app_change_url(): - app_install(os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + args="domain=%s&domain_2=%s&path=%s&admin=%s" + % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + force=True, + ) # FIXME : should rework this test to look for differences in the generated app map / app tiles ... - res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['url'] == "/" - assert res['permissions_app.admin']['url'] == "/admin" - assert res['permissions_app.dev']['url'] == "/dev" + res = user_permission_list(full=True)["permissions"] + assert res["permissions_app.main"]["url"] == "/" + assert res["permissions_app.admin"]["url"] == "/admin" + assert res["permissions_app.dev"]["url"] == "/dev" app_change_url("permissions_app", maindomain, "/newchangeurl") - res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['url'] == "/" - assert res['permissions_app.admin']['url'] == "/admin" - assert res['permissions_app.dev']['url'] == "/dev" + res = user_permission_list(full=True)["permissions"] + assert res["permissions_app.main"]["url"] == "/" + assert res["permissions_app.admin"]["url"] == "/admin" + assert res["permissions_app.dev"]["url"] == "/dev" @pytest.mark.other_domains(number=1) def test_permission_protection_management_by_helper(): - app_install(os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + args="domain=%s&domain_2=%s&path=%s&admin=%s" + % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + force=True, + ) - res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['protected'] is False - assert res['permissions_app.admin']['protected'] is True - assert res['permissions_app.dev']['protected'] is False + res = user_permission_list(full=True)["permissions"] + assert res["permissions_app.main"]["protected"] is False + assert res["permissions_app.admin"]["protected"] is True + assert res["permissions_app.dev"]["protected"] is False - app_upgrade(["permissions_app"], file=os.path.join(get_test_apps_dir(), "permissions_app_ynh")) + app_upgrade( + ["permissions_app"], + file=os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + ) - res = user_permission_list(full=True)['permissions'] - assert res['permissions_app.main']['protected'] is False - assert res['permissions_app.admin']['protected'] is False - assert res['permissions_app.dev']['protected'] is True + res = user_permission_list(full=True)["permissions"] + assert res["permissions_app.main"]["protected"] is False + assert res["permissions_app.admin"]["protected"] is False + assert res["permissions_app.dev"]["protected"] is True @pytest.mark.other_domains(number=1) def test_permission_app_propagation_on_ssowat(): - app_install(os.path.join(get_test_apps_dir(), "permissions_app_ynh"), - args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "permissions_app_ynh"), + args="domain=%s&domain_2=%s&path=%s&is_public=1&admin=%s" + % (maindomain, other_domains[0], "/urlpermissionapp", "alice"), + force=True, + ) - res = user_permission_list(full=True)['permissions'] - assert "visitors" in res['permissions_app.main']['allowed'] - assert "all_users" in res['permissions_app.main']['allowed'] + res = user_permission_list(full=True)["permissions"] + assert "visitors" in res["permissions_app.main"]["allowed"] + assert "all_users" in res["permissions_app.main"]["allowed"] app_webroot = "https://%s/urlpermissionapp" % maindomain assert can_access_webpage(app_webroot, logged_as=None) assert can_access_webpage(app_webroot, logged_as="alice") - user_permission_update("permissions_app.main", remove=["visitors", "all_users"], add="bob") - res = user_permission_list(full=True)['permissions'] + user_permission_update( + "permissions_app.main", remove=["visitors", "all_users"], add="bob" + ) + res = user_permission_list(full=True)["permissions"] assert not can_access_webpage(app_webroot, logged_as=None) assert not can_access_webpage(app_webroot, logged_as="alice") @@ -941,14 +1133,18 @@ def test_permission_app_propagation_on_ssowat(): @pytest.mark.other_domains(number=1) def test_permission_legacy_app_propagation_on_ssowat(): - app_install(os.path.join(get_test_apps_dir(), "legacy_app_ynh"), - args="domain=%s&domain_2=%s&path=%s" % (maindomain, other_domains[0], "/legacy"), force=True) + app_install( + os.path.join(get_test_apps_dir(), "legacy_app_ynh"), + args="domain=%s&domain_2=%s&path=%s" + % (maindomain, other_domains[0], "/legacy"), + force=True, + ) # App is configured as public by default using the legacy unprotected_uri mechanics # It should automatically be migrated during the install - res = user_permission_list(full=True)['permissions'] - assert "visitors" in res['legacy_app.main']['allowed'] - assert "all_users" in res['legacy_app.main']['allowed'] + res = user_permission_list(full=True)["permissions"] + assert "visitors" in res["legacy_app.main"]["allowed"] + assert "all_users" in res["legacy_app.main"]["allowed"] app_webroot = "https://%s/legacy" % maindomain @@ -956,7 +1152,9 @@ def test_permission_legacy_app_propagation_on_ssowat(): assert can_access_webpage(app_webroot, logged_as="alice") # Try to update the permission and check that permissions are still consistent - user_permission_update("legacy_app.main", remove=["visitors", "all_users"], add="bob") + user_permission_update( + "legacy_app.main", remove=["visitors", "all_users"], add="bob" + ) assert not can_access_webpage(app_webroot, logged_as=None) assert not can_access_webpage(app_webroot, logged_as="alice") diff --git a/src/yunohost/tests/test_regenconf.py b/src/yunohost/tests/test_regenconf.py index c9a3e68f5..f454f33e3 100644 --- a/src/yunohost/tests/test_regenconf.py +++ b/src/yunohost/tests/test_regenconf.py @@ -2,7 +2,12 @@ import os from .conftest import message from yunohost.domain import domain_add, domain_remove, domain_list -from yunohost.regenconf import regen_conf, manually_modified_files, _get_conf_hashes, _force_clear_hashes +from yunohost.regenconf import ( + regen_conf, + manually_modified_files, + _get_conf_hashes, + _force_clear_hashes, +) TEST_DOMAIN = "secondarydomain.test" TEST_DOMAIN_NGINX_CONFIG = "/etc/nginx/conf.d/%s.conf" % TEST_DOMAIN @@ -39,7 +44,7 @@ def clean(): assert TEST_DOMAIN_NGINX_CONFIG not in _get_conf_hashes("nginx") assert TEST_DOMAIN_NGINX_CONFIG not in manually_modified_files() - regen_conf(['ssh'], force=True) + regen_conf(["ssh"], force=True) def test_add_domain(): @@ -107,7 +112,7 @@ def test_ssh_conf_unmanaged_and_manually_modified(mocker): assert SSHD_CONFIG in _get_conf_hashes("ssh") assert SSHD_CONFIG in manually_modified_files() - regen_conf(['ssh'], force=True) + regen_conf(["ssh"], force=True) assert SSHD_CONFIG in _get_conf_hashes("ssh") assert SSHD_CONFIG not in manually_modified_files() @@ -158,6 +163,7 @@ def test_stale_hashes_if_file_manually_deleted(): assert not os.path.exists(TEST_DOMAIN_DNSMASQ_CONFIG) assert TEST_DOMAIN_DNSMASQ_CONFIG not in _get_conf_hashes("dnsmasq") + # This test only works if you comment the part at the end of the regen-conf in # dnsmasq that auto-flag /etc/dnsmasq.d/foo.bar as "to be removed" (using touch) # ... But we want to keep it because they also possibly flag files that were diff --git a/src/yunohost/tests/test_service.py b/src/yunohost/tests/test_service.py index 0c65a864a..1f82dc8fd 100644 --- a/src/yunohost/tests/test_service.py +++ b/src/yunohost/tests/test_service.py @@ -2,7 +2,14 @@ import os from .conftest import raiseYunohostError -from yunohost.service import _get_services, _save_services, service_status, service_add, service_remove, service_log +from yunohost.service import ( + _get_services, + _save_services, + service_status, + service_add, + service_remove, + service_log, +) def setup_function(function): @@ -55,7 +62,7 @@ def test_service_log(): def test_service_status_unknown_service(mocker): - with raiseYunohostError(mocker, 'service_unknown'): + with raiseYunohostError(mocker, "service_unknown"): service_status(["ssh", "doesnotexists"]) @@ -83,7 +90,7 @@ def test_service_remove_service_that_doesnt_exists(mocker): assert "dummyservice" not in service_status().keys() - with raiseYunohostError(mocker, 'service_unknown'): + with raiseYunohostError(mocker, "service_unknown"): service_remove("dummyservice") assert "dummyservice" not in service_status().keys() diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py index 0a717192c..b402a9ef5 100644 --- a/src/yunohost/tests/test_settings.py +++ b/src/yunohost/tests/test_settings.py @@ -4,9 +4,17 @@ import pytest from yunohost.utils.error import YunohostError -from yunohost.settings import settings_get, settings_list, _get_settings, \ - settings_set, settings_reset, settings_reset_all, \ - SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH, DEFAULTS +from yunohost.settings import ( + settings_get, + settings_list, + _get_settings, + settings_set, + settings_reset, + settings_reset_all, + SETTINGS_PATH_OTHER_LOCATION, + SETTINGS_PATH, + DEFAULTS, +) DEFAULTS["example.bool"] = {"type": "bool", "default": True} DEFAULTS["example.int"] = {"type": "int", "default": 42} @@ -27,7 +35,12 @@ def test_settings_get_bool(): def test_settings_get_full_bool(): - assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Dummy bool setting"} + assert settings_get("example.bool", True) == { + "type": "bool", + "value": True, + "default": True, + "description": "Dummy bool setting", + } def test_settings_get_int(): @@ -35,7 +48,12 @@ def test_settings_get_int(): def test_settings_get_full_int(): - assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Dummy int setting"} + assert settings_get("example.int", True) == { + "type": "int", + "value": 42, + "default": 42, + "description": "Dummy int setting", + } def test_settings_get_string(): @@ -43,7 +61,12 @@ def test_settings_get_string(): def test_settings_get_full_string(): - assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Dummy string setting"} + assert settings_get("example.string", True) == { + "type": "string", + "value": "yolo swag", + "default": "yolo swag", + "description": "Dummy string setting", + } def test_settings_get_enum(): @@ -51,7 +74,13 @@ def test_settings_get_enum(): def test_settings_get_full_enum(): - assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Dummy enum setting", "choices": ["a", "b", "c"]} + assert settings_get("example.enum", True) == { + "type": "enum", + "value": "a", + "default": "a", + "description": "Dummy enum setting", + "choices": ["a", "b", "c"], + } def test_settings_get_doesnt_exists(): @@ -120,7 +149,12 @@ def test_settings_set_bad_value_enum(): def test_settings_list_modified(): settings_set("example.int", 21) - assert settings_list()["example.int"] == {'default': 42, 'description': 'Dummy int setting', 'type': 'int', 'value': 21} + assert settings_list()["example.int"] == { + "default": 42, + "description": "Dummy int setting", + "type": "int", + "value": 21, + } def test_reset(): diff --git a/src/yunohost/tests/test_user-group.py b/src/yunohost/tests/test_user-group.py index 31131e939..251029796 100644 --- a/src/yunohost/tests/test_user-group.py +++ b/src/yunohost/tests/test_user-group.py @@ -2,8 +2,17 @@ import pytest from .conftest import message, raiseYunohostError -from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ - user_group_list, user_group_create, user_group_delete, user_group_update +from yunohost.user import ( + user_list, + user_info, + user_create, + user_delete, + user_update, + user_group_list, + user_group_create, + user_group_delete, + user_group_update, +) from yunohost.domain import _get_maindomain from yunohost.tests.test_permission import check_LDAP_db_integrity @@ -12,10 +21,10 @@ maindomain = "" def clean_user_groups(): - for u in user_list()['users']: + for u in user_list()["users"]: user_delete(u) - for g in user_group_list()['groups']: + for g in user_group_list()["groups"]: if g not in ["all_users", "visitors"]: user_group_delete(g) @@ -46,13 +55,14 @@ def check_LDAP_db_integrity_call(): yield check_LDAP_db_integrity() + # # List functions # def test_list_users(): - res = user_list()['users'] + res = user_list()["users"] assert "alice" in res assert "bob" in res @@ -60,7 +70,7 @@ def test_list_users(): def test_list_groups(): - res = user_group_list()['groups'] + res = user_group_list()["groups"] assert "all_users" in res assert "alice" in res @@ -68,8 +78,9 @@ def test_list_groups(): assert "jack" in res for u in ["alice", "bob", "jack"]: assert u in res - assert u in res[u]['members'] - assert u in res["all_users"]['members'] + assert u in res[u]["members"] + assert u in res["all_users"]["members"] + # # Create - Remove functions @@ -81,11 +92,11 @@ def test_create_user(mocker): with message(mocker, "user_created"): user_create("albert", "Albert", "Good", maindomain, "test123Ynh") - group_res = user_group_list()['groups'] - assert "albert" in user_list()['users'] + group_res = user_group_list()["groups"] + assert "albert" in user_list()["users"] assert "albert" in group_res - assert "albert" in group_res['albert']['members'] - assert "albert" in group_res['all_users']['members'] + assert "albert" in group_res["albert"]["members"] + assert "albert" in group_res["all_users"]["members"] def test_del_user(mocker): @@ -93,10 +104,10 @@ def test_del_user(mocker): with message(mocker, "user_deleted"): user_delete("alice") - group_res = user_group_list()['groups'] + group_res = user_group_list()["groups"] assert "alice" not in user_list() assert "alice" not in group_res - assert "alice" not in group_res['all_users']['members'] + assert "alice" not in group_res["all_users"]["members"] def test_create_group(mocker): @@ -104,9 +115,9 @@ def test_create_group(mocker): with message(mocker, "group_created", group="adminsys"): user_group_create("adminsys") - group_res = user_group_list()['groups'] + group_res = user_group_list()["groups"] assert "adminsys" in group_res - assert "members" in group_res['adminsys'].keys() + assert "members" in group_res["adminsys"].keys() assert group_res["adminsys"]["members"] == [] @@ -115,9 +126,10 @@ def test_del_group(mocker): with message(mocker, "group_deleted", group="dev"): user_group_delete("dev") - group_res = user_group_list()['groups'] + group_res = user_group_list()["groups"] assert "dev" not in group_res + # # Error on create / remove function # @@ -174,6 +186,7 @@ def test_del_group_that_does_not_exist(mocker): with raiseYunohostError(mocker, "group_unknown"): user_group_delete("doesnt_exist") + # # Update function # @@ -184,40 +197,41 @@ def test_update_user(mocker): user_update("alice", firstname="NewName", lastname="NewLast") info = user_info("alice") - assert info['firstname'] == "NewName" - assert info['lastname'] == "NewLast" + assert info["firstname"] == "NewName" + assert info["lastname"] == "NewLast" def test_update_group_add_user(mocker): with message(mocker, "group_updated", group="dev"): user_group_update("dev", add=["bob"]) - group_res = user_group_list()['groups'] - assert set(group_res['dev']['members']) == set(["alice", "bob"]) + group_res = user_group_list()["groups"] + assert set(group_res["dev"]["members"]) == set(["alice", "bob"]) def test_update_group_add_user_already_in(mocker): with message(mocker, "group_user_already_in_group", user="bob", group="apps"): user_group_update("apps", add=["bob"]) - group_res = user_group_list()['groups'] - assert group_res['apps']['members'] == ["bob"] + group_res = user_group_list()["groups"] + assert group_res["apps"]["members"] == ["bob"] def test_update_group_remove_user(mocker): with message(mocker, "group_updated", group="apps"): user_group_update("apps", remove=["bob"]) - group_res = user_group_list()['groups'] - assert group_res['apps']['members'] == [] + group_res = user_group_list()["groups"] + assert group_res["apps"]["members"] == [] def test_update_group_remove_user_not_already_in(mocker): with message(mocker, "group_user_not_in_group", user="jack", group="apps"): user_group_update("apps", remove=["jack"]) - group_res = user_group_list()['groups'] - assert group_res['apps']['members'] == ["bob"] + group_res = user_group_list()["groups"] + assert group_res["apps"]["members"] == ["bob"] + # # Error on update functions diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 405143395..b1d74883e 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -35,21 +35,30 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output, call_async_output from moulinette.utils.filesystem import read_yaml, write_to_yaml -from yunohost.app import _update_apps_catalog, app_info, app_upgrade, _initialize_apps_catalog_system +from yunohost.app import ( + _update_apps_catalog, + app_info, + app_upgrade, + _initialize_apps_catalog_system, +) from yunohost.domain import domain_add from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.firewall import firewall_upnp from yunohost.service import service_start, service_enable from yunohost.regenconf import regen_conf -from yunohost.utils.packages import _dump_sources_list, _list_upgradable_apt_packages, ynh_packages_version +from yunohost.utils.packages import ( + _dump_sources_list, + _list_upgradable_apt_packages, + ynh_packages_version, +) from yunohost.utils.error import YunohostError from yunohost.log import is_unit_operation, OperationLogger # 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.yaml" -logger = getActionLogger('yunohost.tools') +logger = getActionLogger("yunohost.tools") def tools_versions(): @@ -61,63 +70,72 @@ def tools_ldapinit(): YunoHost LDAP initialization """ - with open('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') as f: + with open("/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml") as f: ldap_map = yaml.load(f) from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() - for rdn, attr_dict in ldap_map['parents'].items(): + for rdn, attr_dict in ldap_map["parents"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: - logger.warn("Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, 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(): + for rdn, attr_dict in ldap_map["children"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: - logger.warn("Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) + logger.warn( + "Error when trying to inject '%s' -> '%s' into ldap: %s" + % (rdn, attr_dict, e) + ) - for rdn, attr_dict in ldap_map['depends_children'].items(): + for rdn, attr_dict in ldap_map["depends_children"].items(): try: ldap.add(rdn, attr_dict) except Exception as e: - logger.warn("Error when trying to inject '%s' -> '%s' into ldap: %s" % (rdn, attr_dict, e)) + logger.warn( + "Error when trying to inject '%s' -> '%s' into ldap: %s" + % (rdn, attr_dict, e) + ) admin_dict = { - 'cn': ['admin'], - 'uid': ['admin'], - 'description': ['LDAP Administrator'], - 'gidNumber': ['1007'], - 'uidNumber': ['1007'], - 'homeDirectory': ['/home/admin'], - 'loginShell': ['/bin/bash'], - 'objectClass': ['organizationalRole', 'posixAccount', 'simpleSecurityObject'], - 'userPassword': ['yunohost'] + "cn": ["admin"], + "uid": ["admin"], + "description": ["LDAP Administrator"], + "gidNumber": ["1007"], + "uidNumber": ["1007"], + "homeDirectory": ["/home/admin"], + "loginShell": ["/bin/bash"], + "objectClass": ["organizationalRole", "posixAccount", "simpleSecurityObject"], + "userPassword": ["yunohost"], } - ldap.update('cn=admin', admin_dict) + ldap.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"]) # Check admin actually exists now try: pwd.getpwnam("admin") except KeyError: - logger.error(m18n.n('ldap_init_failed_to_create_admin')) - raise YunohostError('installation_failed') + logger.error(m18n.n("ldap_init_failed_to_create_admin")) + raise YunohostError("installation_failed") try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", "admin"]) except subprocess.CalledProcessError: - if not os.path.isdir('/home/{0}'.format("admin")): - logger.warning(m18n.n('user_home_creation_failed'), - exc_info=1) + if not os.path.isdir("/home/{0}".format("admin")): + logger.warning(m18n.n("user_home_creation_failed"), exc_info=1) - logger.success(m18n.n('ldap_initialized')) + logger.success(m18n.n("ldap_initialized")) def tools_adminpw(new_password, check_strength=True): @@ -138,43 +156,60 @@ def tools_adminpw(new_password, check_strength=True): # UNIX seems to not like password longer than 127 chars ... # e.g. SSH login gets broken (or even 'su admin' when entering the password) if len(new_password) >= 127: - raise YunohostError('admin_password_too_long') + raise YunohostError("admin_password_too_long") new_hash = _hash_user_password(new_password) from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() try: - ldap.update("cn=admin", {"userPassword": [new_hash], }) + ldap.update( + "cn=admin", + { + "userPassword": [new_hash], + }, + ) except: - logger.error('unable to change admin password') - raise YunohostError('admin_password_change_failed') + logger.error("unable to change admin password") + raise YunohostError("admin_password_change_failed") else: # Write as root password try: hash_root = spwd.getspnam("root").sp_pwd - with open('/etc/shadow', 'r') as before_file: + with open("/etc/shadow", "r") as before_file: before = before_file.read() - with open('/etc/shadow', 'w') as after_file: - after_file.write(before.replace("root:" + hash_root, - "root:" + new_hash.replace('{CRYPT}', ''))) + with open("/etc/shadow", "w") as after_file: + after_file.write( + before.replace( + "root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "") + ) + ) # An IOError may be thrown if for some reason we can't read/write /etc/passwd # A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?) # (c.f. the line about getspnam) except (IOError, KeyError): - logger.warning(m18n.n('root_password_desynchronized')) + logger.warning(m18n.n("root_password_desynchronized")) return logger.info(m18n.n("root_password_replaced_by_admin_password")) - logger.success(m18n.n('admin_password_changed')) + logger.success(m18n.n("admin_password_changed")) def tools_maindomain(new_main_domain=None): from yunohost.domain import domain_main_domain - logger.warning(m18n.g("deprecated_command_alias", prog="yunohost", old="tools maindomain", new="domain main-domain")) + + logger.warning( + m18n.g( + "deprecated_command_alias", + prog="yunohost", + old="tools maindomain", + new="domain main-domain", + ) + ) return domain_main_domain(new_main_domain=new_main_domain) @@ -187,26 +222,24 @@ 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"]) # Then call hostnamectl commands = [ "hostnamectl --static set-hostname".split() + [hostname], "hostnamectl --transient set-hostname".split() + [hostname], - "hostnamectl --pretty set-hostname".split() + [pretty_hostname] + "hostnamectl --pretty set-hostname".split() + [pretty_hostname], ] for command in commands: - p = subprocess.Popen(command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out, _ = p.communicate() if p.returncode != 0: logger.warning(command) logger.warning(out) - logger.error(m18n.n('domain_hostname_failed')) + logger.error(m18n.n("domain_hostname_failed")) else: logger.debug(out) @@ -217,17 +250,23 @@ def _detect_virt(): You can check the man of the command to have a list of possible outputs... """ - p = subprocess.Popen("systemd-detect-virt".split(), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + p = subprocess.Popen( + "systemd-detect-virt".split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) out, _ = p.communicate() return out.split()[0] @is_unit_operation() -def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, - force_password=False, force_diskspace=False): +def tools_postinstall( + operation_logger, + domain, + password, + ignore_dyndns=False, + force_password=False, + force_diskspace=False, +): """ YunoHost post-install @@ -245,17 +284,22 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, dyndns_provider = "dyndns.yunohost.org" # Do some checks at first - if os.path.isfile('/etc/yunohost/installed'): - raise YunohostError('yunohost_already_installed') + if os.path.isfile("/etc/yunohost/installed"): + raise YunohostError("yunohost_already_installed") if os.path.isdir("/etc/yunohost/apps") and os.listdir("/etc/yunohost/apps") != []: - raise YunohostError("It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...", raw_msg=True) + raise YunohostError( + "It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...", + raw_msg=True, + ) # Check there's at least 10 GB on the rootfs... disk_partitions = sorted(psutil.disk_partitions(), key=lambda k: k.mountpoint) - main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ['/', '/var']] - main_space = sum([psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions]) - GB = 1024**3 + main_disk_partitions = [d for d in disk_partitions if d.mountpoint in ["/", "/var"]] + main_space = sum( + [psutil.disk_usage(d.mountpoint).total for d in main_disk_partitions] + ) + GB = 1024 ** 3 if not force_diskspace and main_space < 10 * GB: raise YunohostError("postinstall_low_rootfsspace") @@ -272,8 +316,9 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # 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)) + 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 @@ -286,17 +331,20 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, dyndns = True # If not, abort the postinstall else: - raise YunohostError('dyndns_unavailable', domain=domain) + raise YunohostError("dyndns_unavailable", domain=domain) else: dyndns = False else: dyndns = False if os.system("iptables -V >/dev/null 2>/dev/null") != 0: - raise YunohostError("iptables/nftables does not seems to be working on your setup. You may be in a container or your kernel does have the proper modules loaded. Sometimes, rebooting the machine may solve the issue.", raw_msg=True) + raise YunohostError( + "iptables/nftables does not seems to be working on your setup. You may be in a container or your kernel does have the proper modules loaded. Sometimes, rebooting the machine may solve the issue.", + raw_msg=True, + ) operation_logger.start() - logger.info(m18n.n('yunohost_installing')) + logger.info(m18n.n("yunohost_installing")) # New domain config domain_add(domain, dyndns) @@ -306,7 +354,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, tools_adminpw(password, check_strength=not force_password) # Enable UPnP silently and reload firewall - firewall_upnp('enable', no_refresh=True) + firewall_upnp("enable", no_refresh=True) # Initialize the apps catalog system _initialize_apps_catalog_system() @@ -322,7 +370,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Init migrations (skip them, no need to run them on a fresh system) _skip_all_migrations() - os.system('touch /etc/yunohost/installed') + os.system("touch /etc/yunohost/installed") # Enable and start YunoHost firewall at boot time service_enable("yunohost-firewall") @@ -339,19 +387,20 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # the initial, existing sshd configuration # instead of YunoHost's recommended conf # - original_sshd_conf = '/etc/ssh/sshd_config.before_yunohost' + original_sshd_conf = "/etc/ssh/sshd_config.before_yunohost" if os.path.exists(original_sshd_conf): - os.rename(original_sshd_conf, '/etc/ssh/sshd_config') + os.rename(original_sshd_conf, "/etc/ssh/sshd_config") regen_conf(force=True) - logger.success(m18n.n('yunohost_configured')) + logger.success(m18n.n("yunohost_configured")) - logger.warning(m18n.n('yunohost_postinstall_end_tip')) + logger.warning(m18n.n("yunohost_postinstall_end_tip")) -def tools_regen_conf(names=[], with_diff=False, force=False, dry_run=False, - list_pending=False): +def tools_regen_conf( + names=[], with_diff=False, force=False, dry_run=False, list_pending=False +): return regen_conf(names, with_diff, force, dry_run, list_pending) @@ -381,7 +430,10 @@ def tools_update(apps=False, system=False): warnings = [] def is_legit_warning(m): - legit_warning = m.rstrip() and "apt does not have a stable CLI interface" not in m.rstrip() + legit_warning = ( + m.rstrip() + and "apt does not have a stable CLI interface" not in m.rstrip() + ) if legit_warning: warnings.append(m) return legit_warning @@ -390,20 +442,29 @@ def tools_update(apps=False, system=False): # stdout goes to debug lambda l: logger.debug(l.rstrip()), # stderr goes to warning except for the boring apt messages - lambda l: logger.warning(l.rstrip()) if is_legit_warning(l) else logger.debug(l.rstrip()) + lambda l: logger.warning(l.rstrip()) + if is_legit_warning(l) + else logger.debug(l.rstrip()), ) - logger.info(m18n.n('updating_apt_cache')) + logger.info(m18n.n("updating_apt_cache")) returncode = call_async_output(command, callbacks, shell=True) if returncode != 0: - raise YunohostError('update_apt_cache_failed', sourceslist='\n'.join(_dump_sources_list())) + raise YunohostError( + "update_apt_cache_failed", sourceslist="\n".join(_dump_sources_list()) + ) elif warnings: - logger.error(m18n.n('update_apt_cache_warning', sourceslist='\n'.join(_dump_sources_list()))) + logger.error( + m18n.n( + "update_apt_cache_warning", + sourceslist="\n".join(_dump_sources_list()), + ) + ) upgradable_system_packages = list(_list_upgradable_apt_packages()) - logger.debug(m18n.n('done')) + logger.debug(m18n.n("done")) upgradable_apps = [] if apps: @@ -415,9 +476,9 @@ def tools_update(apps=False, system=False): upgradable_apps = list(_list_upgradable_apps()) if len(upgradable_apps) == 0 and len(upgradable_system_packages) == 0: - logger.info(m18n.n('already_up_to_date')) + logger.info(m18n.n("already_up_to_date")) - return {'system': upgradable_system_packages, 'apps': upgradable_apps} + return {"system": upgradable_system_packages, "apps": upgradable_apps} def _list_upgradable_apps(): @@ -433,24 +494,32 @@ def _list_upgradable_apps(): # directly in app_info and used to check the upgradability of # the app... current_version = app_dict.get("manifest", {}).get("version", "?") - current_commit = app_dict.get("settings", {}).get("current_revision", "?")[:7] - new_version = app_dict.get("from_catalog", {}).get("manifest", {}).get("version", "?") - new_commit = app_dict.get("from_catalog", {}).get("git", {}).get("revision", "?")[:7] + current_commit = app_dict.get("settings", {}).get("current_revision", "?")[ + :7 + ] + new_version = ( + app_dict.get("from_catalog", {}).get("manifest", {}).get("version", "?") + ) + new_commit = ( + app_dict.get("from_catalog", {}).get("git", {}).get("revision", "?")[:7] + ) if current_version == new_version: current_version += " (" + current_commit + ")" new_version += " (" + new_commit + ")" yield { - 'id': app_id, - 'label': app_dict['label'], - 'current_version': current_version, - 'new_version': new_version + "id": app_id, + "label": app_dict["label"], + "current_version": current_version, + "new_version": new_version, } @is_unit_operation() -def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgrade=True): +def tools_upgrade( + operation_logger, apps=None, system=False, allow_yunohost_upgrade=True +): """ Update apps & package cache, then display changelog @@ -459,6 +528,7 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr system -- True to upgrade system """ from yunohost.utils import packages + if packages.dpkg_is_broken(): raise YunohostError("dpkg_is_broken") @@ -483,7 +553,9 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr upgradable_apps = [app["id"] for app in _list_upgradable_apps()] - if not upgradable_apps or (len(apps) and all(app not in upgradable_apps for app in apps)): + if not upgradable_apps or ( + len(apps) and all(app not in upgradable_apps for app in apps) + ): logger.info(m18n.n("apps_already_up_to_date")) return @@ -492,8 +564,8 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr try: app_upgrade(app=apps) except Exception as e: - logger.warning('unable to upgrade apps: %s' % str(e)) - logger.error(m18n.n('app_upgrade_some_app_failed')) + logger.warning("unable to upgrade apps: %s" % str(e)) + logger.error(m18n.n("app_upgrade_some_app_failed")) return @@ -506,23 +578,29 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr # Check that there's indeed some packages to upgrade upgradables = list(_list_upgradable_apt_packages()) if not upgradables: - logger.info(m18n.n('already_up_to_date')) + logger.info(m18n.n("already_up_to_date")) - logger.info(m18n.n('upgrading_packages')) + logger.info(m18n.n("upgrading_packages")) operation_logger.start() # Critical packages are packages that we can't just upgrade # randomly from yunohost itself... upgrading them is likely to critical_packages = ["moulinette", "yunohost", "yunohost-admin", "ssowat"] - critical_packages_upgradable = [p["name"] for p in upgradables if p["name"] in critical_packages] - noncritical_packages_upgradable = [p["name"] for p in upgradables if p["name"] not in critical_packages] + critical_packages_upgradable = [ + p["name"] for p in upgradables if p["name"] in critical_packages + ] + noncritical_packages_upgradable = [ + p["name"] for p in upgradables if p["name"] not in critical_packages + ] # Prepare dist-upgrade command dist_upgrade = "DEBIAN_FRONTEND=noninteractive" dist_upgrade += " APT_LISTCHANGES_FRONTEND=none" dist_upgrade += " apt-get" - dist_upgrade += " --fix-broken --show-upgraded --assume-yes --quiet -o=Dpkg::Use-Pty=0" + dist_upgrade += ( + " --fix-broken --show-upgraded --assume-yes --quiet -o=Dpkg::Use-Pty=0" + ) for conf_flag in ["old", "miss", "def"]: dist_upgrade += ' -o Dpkg::Options::="--force-conf{}"'.format(conf_flag) dist_upgrade += " dist-upgrade" @@ -542,30 +620,40 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr held_packages = check_output("apt-mark showhold").split("\n") if any(p not in held_packages for p in critical_packages): logger.warning(m18n.n("tools_upgrade_cant_hold_critical_packages")) - operation_logger.error(m18n.n('packages_upgrade_failed')) - raise YunohostError(m18n.n('packages_upgrade_failed')) + operation_logger.error(m18n.n("packages_upgrade_failed")) + raise YunohostError(m18n.n("packages_upgrade_failed")) logger.debug("Running apt command :\n{}".format(dist_upgrade)) def is_relevant(l): irrelevants = [ "service sudo-ldap already provided", - "Reading database ..." + "Reading database ...", ] return all(i not in l.rstrip() for i in irrelevants) callbacks = ( - lambda l: logger.info("+ " + l.rstrip() + "\r") if is_relevant(l) else logger.debug(l.rstrip() + "\r"), - lambda l: logger.warning(l.rstrip()) if is_relevant(l) else logger.debug(l.rstrip()), + lambda l: logger.info("+ " + l.rstrip() + "\r") + if is_relevant(l) + else logger.debug(l.rstrip() + "\r"), + lambda l: logger.warning(l.rstrip()) + if is_relevant(l) + else logger.debug(l.rstrip()), ) returncode = call_async_output(dist_upgrade, callbacks, shell=True) if returncode != 0: upgradables = list(_list_upgradable_apt_packages()) - noncritical_packages_upgradable = [p["name"] for p in upgradables if p["name"] not in critical_packages] - logger.warning(m18n.n('tools_upgrade_regular_packages_failed', - packages_list=', '.join(noncritical_packages_upgradable))) - operation_logger.error(m18n.n('packages_upgrade_failed')) - raise YunohostError(m18n.n('packages_upgrade_failed')) + noncritical_packages_upgradable = [ + p["name"] for p in upgradables if p["name"] not in critical_packages + ] + logger.warning( + m18n.n( + "tools_upgrade_regular_packages_failed", + packages_list=", ".join(noncritical_packages_upgradable), + ) + ) + operation_logger.error(m18n.n("packages_upgrade_failed")) + raise YunohostError(m18n.n("packages_upgrade_failed")) # # Critical packages upgrade @@ -582,8 +670,8 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr held_packages = check_output("apt-mark showhold").split("\n") if any(p in held_packages for p in critical_packages): logger.warning(m18n.n("tools_upgrade_cant_unhold_critical_packages")) - operation_logger.error(m18n.n('packages_upgrade_failed')) - raise YunohostError(m18n.n('packages_upgrade_failed')) + operation_logger.error(m18n.n("packages_upgrade_failed")) + raise YunohostError(m18n.n("packages_upgrade_failed")) # # Here we use a dirty hack to run a command after the current @@ -597,9 +685,19 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr dist_upgrade = dist_upgrade + " 2>&1 | tee -a {}".format(logfile) MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" - wait_until_end_of_yunohost_command = "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) - mark_success = "(echo 'Done!' | tee -a {} && echo 'success: true' >> {})".format(logfile, operation_logger.md_path) - mark_failure = "(echo 'Failed :(' | tee -a {} && echo 'success: false' >> {})".format(logfile, operation_logger.md_path) + wait_until_end_of_yunohost_command = ( + "(while [ -f {} ]; do sleep 2; done)".format(MOULINETTE_LOCK) + ) + mark_success = ( + "(echo 'Done!' | tee -a {} && echo 'success: true' >> {})".format( + logfile, operation_logger.md_path + ) + ) + mark_failure = ( + "(echo 'Failed :(' | tee -a {} && echo 'success: false' >> {})".format( + logfile, operation_logger.md_path + ) + ) update_log_metadata = "sed -i \"s/ended_at: .*$/ended_at: $(date -u +'%Y-%m-%d %H:%M:%S.%N')/\" {}" update_log_metadata = update_log_metadata.format(operation_logger.md_path) @@ -610,18 +708,23 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr # the huge command launched by os.system) operation_logger.ended_at = "notyet" - upgrade_completed = "\n" + m18n.n("tools_upgrade_special_packages_completed") + upgrade_completed = "\n" + m18n.n( + "tools_upgrade_special_packages_completed" + ) command = "({wait} && {dist_upgrade}) && {mark_success} || {mark_failure}; {update_metadata}; echo '{done}'".format( - wait=wait_until_end_of_yunohost_command, - dist_upgrade=dist_upgrade, - mark_success=mark_success, - mark_failure=mark_failure, - update_metadata=update_log_metadata, - done=upgrade_completed) + wait=wait_until_end_of_yunohost_command, + dist_upgrade=dist_upgrade, + mark_success=mark_success, + mark_failure=mark_failure, + update_metadata=update_log_metadata, + done=upgrade_completed, + ) logger.warning(m18n.n("tools_upgrade_special_packages_explanation")) logger.debug("Running command :\n{}".format(command)) - open("/tmp/yunohost-selfupgrade", "w").write("rm /tmp/yunohost-selfupgrade; " + command) + open("/tmp/yunohost-selfupgrade", "w").write( + "rm /tmp/yunohost-selfupgrade; " + command + ) # Using systemd-run --scope is like nohup/disown and &, but more robust somehow # (despite using nohup/disown and &, the self-upgrade process was still getting killed...) # ref: https://unix.stackexchange.com/questions/420594/why-process-killed-with-nohup @@ -630,7 +733,7 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr return else: - logger.success(m18n.n('system_upgraded')) + logger.success(m18n.n("system_upgraded")) operation_logger.success() @@ -640,17 +743,17 @@ def tools_shutdown(operation_logger, force=False): if not shutdown: try: # Ask confirmation for server shutdown - i = msignals.prompt(m18n.n('server_shutdown_confirm', answers='y/N')) + i = msignals.prompt(m18n.n("server_shutdown_confirm", answers="y/N")) except NotImplemented: pass else: - if i.lower() == 'y' or i.lower() == 'yes': + if i.lower() == "y" or i.lower() == "yes": shutdown = True if shutdown: operation_logger.start() - logger.warn(m18n.n('server_shutdown')) - subprocess.check_call(['systemctl', 'poweroff']) + logger.warn(m18n.n("server_shutdown")) + subprocess.check_call(["systemctl", "poweroff"]) @is_unit_operation() @@ -659,16 +762,16 @@ def tools_reboot(operation_logger, force=False): if not reboot: try: # Ask confirmation for restoring - i = msignals.prompt(m18n.n('server_reboot_confirm', answers='y/N')) + i = msignals.prompt(m18n.n("server_reboot_confirm", answers="y/N")) except NotImplemented: pass else: - if i.lower() == 'y' or i.lower() == 'yes': + if i.lower() == "y" or i.lower() == "yes": reboot = True if reboot: operation_logger.start() - logger.warn(m18n.n('server_reboot')) - subprocess.check_call(['systemctl', 'reboot']) + logger.warn(m18n.n("server_reboot")) + subprocess.check_call(["systemctl", "reboot"]) def tools_shell(command=None): @@ -679,6 +782,7 @@ def tools_shell(command=None): """ from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() if command: @@ -688,14 +792,19 @@ def tools_shell(command=None): logger.warn("The \033[1;34mldap\033[0m interface 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( + "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) @@ -708,6 +817,7 @@ def tools_shell(command=None): # # # ############################################ # + def tools_migrations_list(pending=False, done=False): """ List existing migrations @@ -721,13 +831,18 @@ def tools_migrations_list(pending=False, done=False): migrations = _get_migrations_list() # Reduce to dictionnaries - migrations = [{"id": migration.id, - "number": migration.number, - "name": migration.name, - "mode": migration.mode, - "state": migration.state, - "description": migration.description, - "disclaimer": migration.disclaimer} for migration in migrations] + migrations = [ + { + "id": migration.id, + "number": migration.number, + "name": migration.name, + "mode": migration.mode, + "state": migration.state, + "description": migration.description, + "disclaimer": migration.disclaimer, + } + for migration in migrations + ] # If asked, filter pending or done migrations if pending or done: @@ -739,7 +854,9 @@ def tools_migrations_list(pending=False, done=False): return {"migrations": migrations} -def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, accept_disclaimer=False): +def tools_migrations_run( + targets=[], skip=False, auto=False, force_rerun=False, accept_disclaimer=False +): """ Perform migrations @@ -767,7 +884,7 @@ def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, # If no target specified if not targets: # skip, revert or force require explicit targets - if (skip or force_rerun): + if skip or force_rerun: raise YunohostError("migrations_must_provide_explicit_targets") # Otherwise, targets are all pending migrations @@ -780,15 +897,15 @@ def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, pending = [t.id for t in targets if t.state == "pending"] if skip and done: - raise YunohostError("migrations_not_pending_cant_skip", ids=', '.join(done)) + raise YunohostError("migrations_not_pending_cant_skip", ids=", ".join(done)) if force_rerun and pending: - raise YunohostError("migrations_pending_cant_rerun", ids=', '.join(pending)) + raise YunohostError("migrations_pending_cant_rerun", ids=", ".join(pending)) if not (skip or force_rerun) and done: - raise YunohostError("migrations_already_ran", ids=', '.join(done)) + raise YunohostError("migrations_already_ran", ids=", ".join(done)) # So, is there actually something to do ? if not targets: - logger.info(m18n.n('migrations_no_migrations_to_run')) + logger.info(m18n.n("migrations_no_migrations_to_run")) return # Actually run selected migrations @@ -799,19 +916,27 @@ def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, # migrations to be ran manually by the user, stop there and ask the # user to run the migration manually. if auto and migration.mode == "manual": - logger.warn(m18n.n('migrations_to_be_ran_manually', id=migration.id)) + logger.warn(m18n.n("migrations_to_be_ran_manually", id=migration.id)) # We go to the next migration continue # Check for migration dependencies if not skip: - dependencies = [get_matching_migration(dep) for dep in migration.dependencies] - pending_dependencies = [dep.id for dep in dependencies if dep.state == "pending"] + dependencies = [ + get_matching_migration(dep) for dep in migration.dependencies + ] + pending_dependencies = [ + dep.id for dep in dependencies if dep.state == "pending" + ] if pending_dependencies: - logger.error(m18n.n('migrations_dependencies_not_satisfied', - id=migration.id, - dependencies_id=', '.join(pending_dependencies))) + logger.error( + m18n.n( + "migrations_dependencies_not_satisfied", + id=migration.id, + dependencies_id=", ".join(pending_dependencies), + ) + ) continue # If some migrations have disclaimers (and we're not trying to skip them) @@ -819,20 +944,24 @@ def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, # require the --accept-disclaimer option. # Otherwise, go to the next migration if not accept_disclaimer: - logger.warn(m18n.n('migrations_need_to_accept_disclaimer', - id=migration.id, - disclaimer=migration.disclaimer)) + logger.warn( + m18n.n( + "migrations_need_to_accept_disclaimer", + id=migration.id, + disclaimer=migration.disclaimer, + ) + ) continue # --accept-disclaimer will only work for the first migration else: accept_disclaimer = False # Start register change on system - operation_logger = OperationLogger('tools_migrations_migrate_forward') + operation_logger = OperationLogger("tools_migrations_migrate_forward") operation_logger.start() if skip: - logger.warn(m18n.n('migrations_skip_migration', id=migration.id)) + logger.warn(m18n.n("migrations_skip_migration", id=migration.id)) migration.state = "skipped" _write_migration_state(migration.id, "skipped") operation_logger.success() @@ -840,17 +969,18 @@ def tools_migrations_run(targets=[], skip=False, auto=False, force_rerun=False, try: migration.operation_logger = operation_logger - logger.info(m18n.n('migrations_running_forward', id=migration.id)) + logger.info(m18n.n("migrations_running_forward", id=migration.id)) migration.run() except Exception as e: # migration failed, let's stop here but still update state because # we managed to run the previous ones - msg = m18n.n('migrations_migration_has_failed', - exception=e, id=migration.id) + msg = m18n.n( + "migrations_migration_has_failed", exception=e, id=migration.id + ) logger.error(msg, exc_info=1) operation_logger.error(msg) else: - logger.success(m18n.n('migrations_success_forward', id=migration.id)) + logger.success(m18n.n("migrations_success_forward", id=migration.id)) migration.state = "done" _write_migration_state(migration.id, "done") @@ -886,7 +1016,7 @@ def _get_migrations_list(): migrations_path = data_migrations.__path__[0] if not os.path.exists(migrations_path): - logger.warn(m18n.n('migrations_cant_reach_migration_file', migrations_path)) + logger.warn(m18n.n("migrations_cant_reach_migration_file", migrations_path)) return migrations # states is a datastructure that represents the last run migration @@ -900,7 +1030,11 @@ def _get_migrations_list(): # (in particular, pending migrations / not already ran are not listed states = tools_migrations_state()["migrations"] - for migration_file in [x for x in os.listdir(migrations_path) if re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x)]: + for migration_file in [ + x + for x in os.listdir(migrations_path) + if re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x) + ]: m = _load_migration(migration_file) m.state = states.get(m.id, "pending") migrations.append(m) @@ -919,18 +1053,24 @@ def _get_migration_by_name(migration_name): raise AssertionError("Unable to find migration with name %s" % migration_name) migrations_path = data_migrations.__path__[0] - migrations_found = [x for x in os.listdir(migrations_path) if re.match(r"^\d+_%s\.py$" % migration_name, x)] + migrations_found = [ + x + for x in os.listdir(migrations_path) + if re.match(r"^\d+_%s\.py$" % migration_name, x) + ] - assert len(migrations_found) == 1, "Unable to find migration with name %s" % migration_name + assert len(migrations_found) == 1, ( + "Unable to find migration with name %s" % migration_name + ) return _load_migration(migrations_found[0]) def _load_migration(migration_file): - migration_id = migration_file[:-len(".py")] + migration_id = migration_file[: -len(".py")] - logger.debug(m18n.n('migrations_loading_migration', id=migration_id)) + logger.debug(m18n.n("migrations_loading_migration", id=migration_id)) try: # this is python builtin method to import a module using a name, we @@ -940,9 +1080,12 @@ def _load_migration(migration_file): return module.MyMigration(migration_id) except Exception as e: import traceback + traceback.print_exc() - raise YunohostError('migrations_failed_to_load_migration', id=migration_id, error=e) + raise YunohostError( + "migrations_failed_to_load_migration", id=migration_id, error=e + ) def _skip_all_migrations(): diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 3234ece32..f1fab786a 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -41,7 +41,7 @@ from yunohost.utils.error import YunohostError from yunohost.service import service_status from yunohost.log import is_unit_operation -logger = getActionLogger('yunohost.user') +logger = getActionLogger("yunohost.user") def user_list(fields=None): @@ -49,16 +49,16 @@ def user_list(fields=None): from yunohost.utils.ldap import _get_ldap_interface user_attrs = { - 'uid': 'username', - 'cn': 'fullname', - 'mail': 'mail', - 'maildrop': 'mail-forward', - 'loginShell': 'shell', - 'homeDirectory': 'home_path', - 'mailuserquota': 'mailbox-quota' + "uid": "username", + "cn": "fullname", + "mail": "mail", + "maildrop": "mail-forward", + "loginShell": "shell", + "homeDirectory": "home_path", + "mailuserquota": "mailbox-quota", } - attrs = ['uid'] + attrs = ["uid"] users = {} if fields: @@ -67,14 +67,16 @@ def user_list(fields=None): if attr in keys: attrs.append(attr) else: - raise YunohostError('field_invalid', attr) + raise YunohostError("field_invalid", attr) else: - attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] + attrs = ["uid", "cn", "mail", "mailuserquota", "loginShell"] ldap = _get_ldap_interface() - result = ldap.search('ou=users,dc=yunohost,dc=org', - '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', - attrs) + result = ldap.search( + "ou=users,dc=yunohost,dc=org", + "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", + attrs, + ) for user in result: entry = {} @@ -88,15 +90,23 @@ def user_list(fields=None): entry[user_attrs[attr]] = values[0] - uid = entry[user_attrs['uid']] + uid = entry[user_attrs["uid"]] users[uid] = entry - return {'users': users} + return {"users": users} -@is_unit_operation([('username', 'user')]) -def user_create(operation_logger, username, firstname, lastname, domain, password, - mailbox_quota="0", mail=None): +@is_unit_operation([("username", "user")]) +def user_create( + operation_logger, + username, + firstname, + lastname, + domain, + password, + mailbox_quota="0", + mail=None, +): from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback @@ -107,29 +117,33 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor assert_password_is_strong_enough("user", password) if mail is not None: - logger.warning("Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead.") + logger.warning( + "Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead." + ) domain = mail.split("@")[-1] # Validate domain used for email address/xmpp account if domain is None: - if msettings.get('interface') == 'api': - raise YunohostError('Invalide usage, specify domain argument') + if msettings.get("interface") == "api": + raise YunohostError("Invalide usage, specify domain argument") else: # On affiche les differents domaines possibles - msignals.display(m18n.n('domains_available')) - for domain in domain_list()['domains']: + msignals.display(m18n.n("domains_available")) + for domain in domain_list()["domains"]: msignals.display("- {}".format(domain)) maindomain = _get_maindomain() - domain = msignals.prompt(m18n.n('ask_user_domain') + ' (default: %s)' % maindomain) + domain = msignals.prompt( + m18n.n("ask_user_domain") + " (default: %s)" % maindomain + ) if not domain: domain = maindomain # Check that the domain exists - if domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + if domain not in domain_list()["domains"]: + raise YunohostError("domain_name_unknown", domain=domain) - mail = username + '@' + domain + mail = username + "@" + domain ldap = _get_ldap_interface() if username in user_list()["users"]: @@ -137,30 +151,26 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor # Validate uniqueness of username and mail in LDAP try: - ldap.validate_uniqueness({ - 'uid': username, - 'mail': mail, - 'cn': username - }) + ldap.validate_uniqueness({"uid": username, "mail": mail, "cn": username}) except Exception as e: - raise YunohostError('user_creation_failed', user=username, error=e) + raise YunohostError("user_creation_failed", user=username, error=e) # Validate uniqueness of username in system users all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: - raise YunohostError('system_username_exists') + raise YunohostError("system_username_exists") main_domain = _get_maindomain() aliases = [ - 'root@' + main_domain, - 'admin@' + main_domain, - 'webmaster@' + main_domain, - 'postmaster@' + main_domain, - 'abuse@' + main_domain, + "root@" + main_domain, + "admin@" + main_domain, + "webmaster@" + main_domain, + "postmaster@" + main_domain, + "abuse@" + main_domain, ] if mail in aliases: - raise YunohostError('mail_unavailable') + raise YunohostError("mail_unavailable") operation_logger.start() @@ -175,49 +185,53 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor uid_guid_found = uid not in all_uid and uid not in all_gid # Adapt values for LDAP - fullname = '%s %s' % (firstname, lastname) + fullname = "%s %s" % (firstname, lastname) attr_dict = { - 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'], - 'givenName': [firstname], - 'sn': [lastname], - 'displayName': [fullname], - 'cn': [fullname], - 'uid': [username], - 'mail': mail, # NOTE: this one seems to be already a list - 'maildrop': [username], - 'mailuserquota': [mailbox_quota], - 'userPassword': [_hash_user_password(password)], - 'gidNumber': [uid], - 'uidNumber': [uid], - 'homeDirectory': ['/home/' + username], - 'loginShell': ['/bin/false'] + "objectClass": [ + "mailAccount", + "inetOrgPerson", + "posixAccount", + "userPermissionYnh", + ], + "givenName": [firstname], + "sn": [lastname], + "displayName": [fullname], + "cn": [fullname], + "uid": [username], + "mail": mail, # NOTE: this one seems to be already a list + "maildrop": [username], + "mailuserquota": [mailbox_quota], + "userPassword": [_hash_user_password(password)], + "gidNumber": [uid], + "uidNumber": [uid], + "homeDirectory": ["/home/" + username], + "loginShell": ["/bin/false"], } # If it is the first user, add some aliases - if not ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): - attr_dict['mail'] = [attr_dict['mail']] + aliases + if not ldap.search(base="ou=users,dc=yunohost,dc=org", filter="uid=*"): + attr_dict["mail"] = [attr_dict["mail"]] + aliases try: - ldap.add('uid=%s,ou=users' % username, attr_dict) + ldap.add("uid=%s,ou=users" % username, attr_dict) except Exception as e: - raise YunohostError('user_creation_failed', user=username, error=e) + raise YunohostError("user_creation_failed", user=username, error=e) # Invalidate passwd and group to take user and group creation into account - subprocess.call(['nscd', '-i', 'passwd']) - subprocess.call(['nscd', '-i', 'group']) + subprocess.call(["nscd", "-i", "passwd"]) + subprocess.call(["nscd", "-i", "group"]) try: # Attempt to create user home folder subprocess.check_call(["mkhomedir_helper", username]) except subprocess.CalledProcessError: - if not os.path.isdir('/home/{0}'.format(username)): - logger.warning(m18n.n('user_home_creation_failed'), - exc_info=1) + if not os.path.isdir("/home/{0}".format(username)): + logger.warning(m18n.n("user_home_creation_failed"), exc_info=1) # Create group for user and add to group 'all_users' user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False) - user_group_update(groupname='all_users', add=username, force=True, sync_perm=True) + user_group_update(groupname="all_users", add=username, force=True, sync_perm=True) # Trigger post_user_create hooks env_dict = { @@ -225,18 +239,18 @@ def user_create(operation_logger, username, firstname, lastname, domain, passwor "YNH_USER_MAIL": mail, "YNH_USER_PASSWORD": password, "YNH_USER_FIRSTNAME": firstname, - "YNH_USER_LASTNAME": lastname + "YNH_USER_LASTNAME": lastname, } - hook_callback('post_user_create', args=[username, mail], env=env_dict) + hook_callback("post_user_create", args=[username, mail], env=env_dict) # TODO: Send a welcome mail to user - logger.success(m18n.n('user_created')) + logger.success(m18n.n("user_created")) - return {'fullname': fullname, 'username': username, 'mail': mail} + return {"fullname": fullname, "username": username, "mail": mail} -@is_unit_operation([('username', 'user')]) +@is_unit_operation([("username", "user")]) def user_delete(operation_logger, username, purge=False): """ Delete user @@ -250,7 +264,7 @@ def user_delete(operation_logger, username, purge=False): from yunohost.utils.ldap import _get_ldap_interface if username not in user_list()["users"]: - raise YunohostError('user_unknown', user=username) + raise YunohostError("user_unknown", user=username) operation_logger.start() @@ -266,31 +280,41 @@ def user_delete(operation_logger, username, purge=False): # Delete primary group if it exists (why wouldnt it exists ? because some # epic bug happened somewhere else and only a partial removal was # performed...) - if username in user_group_list()['groups'].keys(): + if username in user_group_list()["groups"].keys(): user_group_delete(username, force=True, sync_perm=True) ldap = _get_ldap_interface() try: - ldap.remove('uid=%s,ou=users' % username) + ldap.remove("uid=%s,ou=users" % username) except Exception as e: - raise YunohostError('user_deletion_failed', user=username, error=e) + raise YunohostError("user_deletion_failed", user=username, error=e) # Invalidate passwd to take user deletion into account - subprocess.call(['nscd', '-i', 'passwd']) + subprocess.call(["nscd", "-i", "passwd"]) if purge: - subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) - subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)]) + subprocess.call(["rm", "-rf", "/home/{0}".format(username)]) + subprocess.call(["rm", "-rf", "/var/mail/{0}".format(username)]) - hook_callback('post_user_delete', args=[username, purge]) + hook_callback("post_user_delete", args=[username, purge]) - logger.success(m18n.n('user_deleted')) + logger.success(m18n.n("user_deleted")) -@is_unit_operation([('username', 'user')], exclude=['change_password']) -def user_update(operation_logger, 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): +@is_unit_operation([("username", "user")], exclude=["change_password"]) +def user_update( + operation_logger, + 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, +): """ Update user informations @@ -312,130 +336,142 @@ def user_update(operation_logger, username, firstname=None, lastname=None, mail= from yunohost.utils.ldap import _get_ldap_interface from yunohost.hook import hook_callback - domains = domain_list()['domains'] + domains = domain_list()["domains"] # Populate user informations ldap = _get_ldap_interface() - attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] - result = ldap.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) + attrs_to_fetch = ["givenName", "sn", "mail", "maildrop"] + result = ldap.search( + base="ou=users,dc=yunohost,dc=org", + filter="uid=" + username, + attrs=attrs_to_fetch, + ) if not result: - raise YunohostError('user_unknown', user=username) + raise YunohostError("user_unknown", user=username) user = result[0] - env_dict = { - "YNH_USER_USERNAME": username - } + env_dict = {"YNH_USER_USERNAME": username} # Get modifications from arguments new_attr_dict = {} if firstname: - new_attr_dict['givenName'] = [firstname] # TODO: Validate - new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + user['sn'][0]] + new_attr_dict["givenName"] = [firstname] # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = [ + firstname + " " + user["sn"][0] + ] env_dict["YNH_USER_FIRSTNAME"] = firstname if lastname: - new_attr_dict['sn'] = [lastname] # TODO: Validate - new_attr_dict['cn'] = new_attr_dict['displayName'] = [user['givenName'][0] + ' ' + lastname] + new_attr_dict["sn"] = [lastname] # TODO: Validate + new_attr_dict["cn"] = new_attr_dict["displayName"] = [ + user["givenName"][0] + " " + lastname + ] env_dict["YNH_USER_LASTNAME"] = lastname if lastname and firstname: - new_attr_dict['cn'] = new_attr_dict['displayName'] = [firstname + ' ' + lastname] + new_attr_dict["cn"] = new_attr_dict["displayName"] = [ + firstname + " " + lastname + ] # change_password is None if user_update is not called to change the password if change_password is not None: # when in the cli interface if the option to change the password is called # without a specified value, change_password will be set to the const 0. # In this case we prompt for the new password. - if msettings.get('interface') == 'cli' and not change_password: + if msettings.get("interface") == "cli" and not change_password: change_password = msignals.prompt(m18n.n("ask_password"), True, True) # Ensure sufficiently complex password assert_password_is_strong_enough("user", change_password) - new_attr_dict['userPassword'] = [_hash_user_password(change_password)] + new_attr_dict["userPassword"] = [_hash_user_password(change_password)] env_dict["YNH_USER_PASSWORD"] = change_password if mail: main_domain = _get_maindomain() 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, ] try: - ldap.validate_uniqueness({'mail': mail}) + ldap.validate_uniqueness({"mail": mail}) except Exception as e: - raise YunohostError('user_update_failed', user=username, error=e) - if mail[mail.find('@') + 1:] not in domains: - raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) + raise YunohostError("user_update_failed", user=username, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] + ) if mail in aliases: - raise YunohostError('mail_unavailable') + raise YunohostError("mail_unavailable") - del user['mail'][0] - new_attr_dict['mail'] = [mail] + user['mail'] + del user["mail"][0] + new_attr_dict["mail"] = [mail] + user["mail"] if add_mailalias: if not isinstance(add_mailalias, list): add_mailalias = [add_mailalias] for mail in add_mailalias: try: - ldap.validate_uniqueness({'mail': mail}) + ldap.validate_uniqueness({"mail": mail}) except Exception as e: - raise YunohostError('user_update_failed', user=username, error=e) - if mail[mail.find('@') + 1:] not in domains: - raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) - user['mail'].append(mail) - new_attr_dict['mail'] = user['mail'] + raise YunohostError("user_update_failed", user=username, error=e) + if mail[mail.find("@") + 1 :] not in domains: + raise YunohostError( + "mail_domain_unknown", 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] for mail in remove_mailalias: - if len(user['mail']) > 1 and mail in user['mail'][1:]: - user['mail'].remove(mail) + if len(user["mail"]) > 1 and mail in user["mail"][1:]: + user["mail"].remove(mail) else: - raise YunohostError('mail_alias_remove_failed', mail=mail) - new_attr_dict['mail'] = user['mail'] + raise YunohostError("mail_alias_remove_failed", mail=mail) + new_attr_dict["mail"] = user["mail"] - if 'mail' in new_attr_dict: - env_dict["YNH_USER_MAILS"] = ','.join(new_attr_dict['mail']) + if "mail" in new_attr_dict: + env_dict["YNH_USER_MAILS"] = ",".join(new_attr_dict["mail"]) if add_mailforward: if not isinstance(add_mailforward, list): add_mailforward = [add_mailforward] for mail in add_mailforward: - if mail in user['maildrop'][1:]: + if mail in user["maildrop"][1:]: continue - user['maildrop'].append(mail) - new_attr_dict['maildrop'] = user['maildrop'] + user["maildrop"].append(mail) + new_attr_dict["maildrop"] = user["maildrop"] if remove_mailforward: if not isinstance(remove_mailforward, list): remove_mailforward = [remove_mailforward] for mail in remove_mailforward: - if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: - user['maildrop'].remove(mail) + if len(user["maildrop"]) > 1 and mail in user["maildrop"][1:]: + user["maildrop"].remove(mail) else: - raise YunohostError('mail_forward_remove_failed', mail=mail) - new_attr_dict['maildrop'] = user['maildrop'] + raise YunohostError("mail_forward_remove_failed", mail=mail) + new_attr_dict["maildrop"] = user["maildrop"] - if 'maildrop' in new_attr_dict: - env_dict["YNH_USER_MAILFORWARDS"] = ','.join(new_attr_dict['maildrop']) + if "maildrop" in new_attr_dict: + env_dict["YNH_USER_MAILFORWARDS"] = ",".join(new_attr_dict["maildrop"]) if mailbox_quota is not None: - new_attr_dict['mailuserquota'] = [mailbox_quota] + new_attr_dict["mailuserquota"] = [mailbox_quota] env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota operation_logger.start() try: - ldap.update('uid=%s,ou=users' % username, new_attr_dict) + ldap.update("uid=%s,ou=users" % username, new_attr_dict) except Exception as e: - raise YunohostError('user_update_failed', user=username, error=e) + raise YunohostError("user_update_failed", user=username, error=e) # Trigger post_user_update hooks - hook_callback('post_user_update', env=env_dict) + hook_callback("post_user_update", env=env_dict) - logger.success(m18n.n('user_updated')) + logger.success(m18n.n("user_updated")) app_ssowatconf() return user_info(username) @@ -452,53 +488,51 @@ def user_info(username): ldap = _get_ldap_interface() - user_attrs = [ - 'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn', 'mailuserquota' - ] + user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"] - if len(username.split('@')) == 2: - filter = 'mail=' + username + if len(username.split("@")) == 2: + filter = "mail=" + username else: - filter = 'uid=' + username + filter = "uid=" + username - result = ldap.search('ou=users,dc=yunohost,dc=org', filter, user_attrs) + result = ldap.search("ou=users,dc=yunohost,dc=org", filter, user_attrs) if result: user = result[0] else: - raise YunohostError('user_unknown', user=username) + raise YunohostError("user_unknown", user=username) result_dict = { - 'username': user['uid'][0], - 'fullname': user['cn'][0], - 'firstname': user['givenName'][0], - 'lastname': user['sn'][0], - 'mail': user['mail'][0] + "username": user["uid"][0], + "fullname": user["cn"][0], + "firstname": user["givenName"][0], + "lastname": user["sn"][0], + "mail": user["mail"][0], } - if len(user['mail']) > 1: - result_dict['mail-aliases'] = user['mail'][1:] + if len(user["mail"]) > 1: + result_dict["mail-aliases"] = user["mail"][1:] - if len(user['maildrop']) > 1: - result_dict['mail-forward'] = user['maildrop'][1:] + if len(user["maildrop"]) > 1: + result_dict["mail-forward"] = user["maildrop"][1:] - if 'mailuserquota' in user: - userquota = user['mailuserquota'][0] + if "mailuserquota" in user: + 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 = '?' + is_limited = not re.match("0[bkMGT]?", userquota) + storage_use = "?" if service_status("dovecot")["status"] != "running": - logger.warning(m18n.n('mailbox_used_space_dovecot_down')) + logger.warning(m18n.n("mailbox_used_space_dovecot_down")) elif username not in user_permission_info("mail.main")["corresponding_users"]: - logger.warning(m18n.n('mailbox_disabled', user=username)) + logger.warning(m18n.n("mailbox_disabled", user=username)) else: try: - cmd = 'doveadm -f flow quota get -u %s' % user['uid'][0] + cmd = "doveadm -f flow quota get -u %s" % user["uid"][0] cmd_result = check_output(cmd) except Exception as e: cmd_result = "" @@ -507,22 +541,22 @@ def user_info(username): # 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%%)' % percentage + storage_use += " (%s%%)" % percentage - result_dict['mailbox-quota'] = { - 'limit': userquota if is_limited else m18n.n('unlimit'), - 'use': storage_use + result_dict["mailbox-quota"] = { + "limit": userquota if is_limited else m18n.n("unlimit"), + "use": storage_use, } return result_dict @@ -547,10 +581,13 @@ def user_group_list(short=False, full=False, include_primary_groups=True): # Fetch relevant informations from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract + ldap = _get_ldap_interface() - groups_infos = ldap.search('ou=groups,dc=yunohost,dc=org', - '(objectclass=groupOfNamesYnh)', - ["cn", "member", "permission"]) + groups_infos = ldap.search( + "ou=groups,dc=yunohost,dc=org", + "(objectclass=groupOfNamesYnh)", + ["cn", "member", "permission"], + ) # Parse / organize information to be outputed @@ -565,19 +602,25 @@ def user_group_list(short=False, full=False, include_primary_groups=True): groups[name] = {} - groups[name]["members"] = [_ldap_path_extract(p, "uid") for p in infos.get("member", [])] + groups[name]["members"] = [ + _ldap_path_extract(p, "uid") for p in infos.get("member", []) + ] if full: - groups[name]["permissions"] = [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])] + groups[name]["permissions"] = [ + _ldap_path_extract(p, "cn") for p in infos.get("permission", []) + ] if short: groups = list(groups.keys()) - return {'groups': groups} + return {"groups": groups} -@is_unit_operation([('groupname', 'group')]) -def user_group_create(operation_logger, groupname, gid=None, primary_group=False, sync_perm=True): +@is_unit_operation([("groupname", "group")]) +def user_group_create( + operation_logger, groupname, gid=None, primary_group=False, sync_perm=True +): """ Create group @@ -591,20 +634,24 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False ldap = _get_ldap_interface() # Validate uniqueness of groupname in LDAP - conflict = ldap.get_conflict({ - 'cn': groupname - }, base_dn='ou=groups,dc=yunohost,dc=org') + conflict = ldap.get_conflict( + {"cn": groupname}, base_dn="ou=groups,dc=yunohost,dc=org" + ) if conflict: - raise YunohostError('group_already_exist', group=groupname) + raise YunohostError("group_already_exist", group=groupname) # Validate uniqueness of groupname in system group all_existing_groupnames = {x.gr_name for x in grp.getgrall()} if groupname in all_existing_groupnames: if primary_group: - logger.warning(m18n.n('group_already_exist_on_system_but_removing_it', group=groupname)) - subprocess.check_call("sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True) + logger.warning( + m18n.n("group_already_exist_on_system_but_removing_it", group=groupname) + ) + subprocess.check_call( + "sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True + ) else: - raise YunohostError('group_already_exist_on_system', group=groupname) + raise YunohostError("group_already_exist_on_system", group=groupname) if not gid: # Get random GID @@ -616,9 +663,9 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False uid_guid_found = gid not in all_gid attr_dict = { - 'objectClass': ['top', 'groupOfNamesYnh', 'posixGroup'], - 'cn': groupname, - 'gidNumber': gid, + "objectClass": ["top", "groupOfNamesYnh", "posixGroup"], + "cn": groupname, + "gidNumber": gid, } # Here we handle the creation of a primary group @@ -629,22 +676,22 @@ def user_group_create(operation_logger, groupname, gid=None, primary_group=False operation_logger.start() try: - ldap.add('cn=%s,ou=groups' % groupname, attr_dict) + ldap.add("cn=%s,ou=groups" % groupname, attr_dict) except Exception as e: - raise YunohostError('group_creation_failed', group=groupname, error=e) + raise YunohostError("group_creation_failed", group=groupname, error=e) if sync_perm: permission_sync_to_user() if not primary_group: - logger.success(m18n.n('group_created', group=groupname)) + logger.success(m18n.n("group_created", group=groupname)) else: - logger.debug(m18n.n('group_created', group=groupname)) + logger.debug(m18n.n("group_created", group=groupname)) - return {'name': groupname} + return {"name": groupname} -@is_unit_operation([('groupname', 'group')]) +@is_unit_operation([("groupname", "group")]) def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): """ Delete user @@ -656,37 +703,39 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface - existing_groups = list(user_group_list()['groups'].keys()) + existing_groups = list(user_group_list()["groups"].keys()) if groupname not in existing_groups: - raise YunohostError('group_unknown', group=groupname) + raise YunohostError("group_unknown", group=groupname) # Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam') # without the force option... # # We also can't delete "all_users" because that's a special group... - existing_users = list(user_list()['users'].keys()) + existing_users = list(user_list()["users"].keys()) undeletable_groups = existing_users + ["all_users", "visitors"] if groupname in undeletable_groups and not force: - raise YunohostError('group_cannot_be_deleted', group=groupname) + raise YunohostError("group_cannot_be_deleted", group=groupname) operation_logger.start() ldap = _get_ldap_interface() try: - ldap.remove('cn=%s,ou=groups' % groupname) + ldap.remove("cn=%s,ou=groups" % groupname) except Exception as e: - raise YunohostError('group_deletion_failed', group=groupname, error=e) + raise YunohostError("group_deletion_failed", group=groupname, error=e) if sync_perm: permission_sync_to_user() if groupname not in existing_users: - logger.success(m18n.n('group_deleted', group=groupname)) + logger.success(m18n.n("group_deleted", group=groupname)) else: - logger.debug(m18n.n('group_deleted', group=groupname)) + logger.debug(m18n.n("group_deleted", group=groupname)) -@is_unit_operation([('groupname', 'group')]) -def user_group_update(operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True): +@is_unit_operation([("groupname", "group")]) +def user_group_update( + operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True +): """ Update user informations @@ -700,18 +749,18 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= from yunohost.permission import permission_sync_to_user from yunohost.utils.ldap import _get_ldap_interface - existing_users = list(user_list()['users'].keys()) + existing_users = list(user_list()["users"].keys()) # Refuse to edit a primary group of a user (e.g. group 'sam' related to user 'sam') # Those kind of group should only ever contain the user (e.g. sam) and only this one. # We also can't edit "all_users" without the force option because that's a special group... if not force: if groupname == "all_users": - raise YunohostError('group_cannot_edit_all_users') + raise YunohostError("group_cannot_edit_all_users") elif groupname == "visitors": - raise YunohostError('group_cannot_edit_visitors') + raise YunohostError("group_cannot_edit_visitors") elif groupname in existing_users: - raise YunohostError('group_cannot_edit_primary_group', group=groupname) + raise YunohostError("group_cannot_edit_primary_group", group=groupname) # We extract the uid for each member of the group to keep a simple flat list of members current_group = user_group_info(groupname)["members"] @@ -722,12 +771,14 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= for user in users_to_add: if user not in existing_users: - raise YunohostError('user_unknown', user=user) + raise YunohostError("user_unknown", user=user) if user in current_group: - logger.warning(m18n.n('group_user_already_in_group', user=user, group=groupname)) + logger.warning( + m18n.n("group_user_already_in_group", user=user, group=groupname) + ) else: - operation_logger.related_to.append(('user', user)) + operation_logger.related_to.append(("user", user)) new_group += users_to_add @@ -736,28 +787,35 @@ def user_group_update(operation_logger, groupname, add=None, remove=None, force= for user in users_to_remove: if user not in current_group: - logger.warning(m18n.n('group_user_not_in_group', user=user, group=groupname)) + logger.warning( + m18n.n("group_user_not_in_group", user=user, group=groupname) + ) else: - operation_logger.related_to.append(('user', user)) + operation_logger.related_to.append(("user", user)) # Remove users_to_remove from new_group # Kinda like a new_group -= users_to_remove new_group = [u for u in new_group if u not in users_to_remove] - new_group_dns = ["uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group] + new_group_dns = [ + "uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group + ] if set(new_group) != set(current_group): operation_logger.start() ldap = _get_ldap_interface() try: - ldap.update('cn=%s,ou=groups' % groupname, {"member": set(new_group_dns), "memberUid": set(new_group)}) + ldap.update( + "cn=%s,ou=groups" % groupname, + {"member": set(new_group_dns), "memberUid": set(new_group)}, + ) except Exception as e: - raise YunohostError('group_update_failed', group=groupname, error=e) + raise YunohostError("group_update_failed", group=groupname, error=e) if groupname != "all_users": - logger.success(m18n.n('group_updated', group=groupname)) + logger.success(m18n.n("group_updated", group=groupname)) else: - logger.debug(m18n.n('group_updated', group=groupname)) + logger.debug(m18n.n("group_updated", group=groupname)) if sync_perm: permission_sync_to_user() @@ -774,23 +832,28 @@ def user_group_info(groupname): """ from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract + ldap = _get_ldap_interface() # Fetch info for this group - result = ldap.search('ou=groups,dc=yunohost,dc=org', - "cn=" + groupname, - ["cn", "member", "permission"]) + result = ldap.search( + "ou=groups,dc=yunohost,dc=org", + "cn=" + groupname, + ["cn", "member", "permission"], + ) if not result: - raise YunohostError('group_unknown', group=groupname) + raise YunohostError("group_unknown", group=groupname) infos = result[0] # Format data return { - 'members': [_ldap_path_extract(p, "uid") for p in infos.get("member", [])], - 'permissions': [_ldap_path_extract(p, "cn") for p in infos.get("permission", [])] + "members": [_ldap_path_extract(p, "uid") for p in infos.get("member", [])], + "permissions": [ + _ldap_path_extract(p, "cn") for p in infos.get("permission", []) + ], } @@ -798,27 +861,37 @@ def user_group_info(groupname): # Permission subcategory # + def user_permission_list(short=False, full=False): import yunohost.permission + return yunohost.permission.user_permission_list(short, full, absolute_urls=True) -def user_permission_update(permission, add=None, remove=None, label=None, show_tile=None, sync_perm=True): +def user_permission_update( + permission, add=None, remove=None, label=None, show_tile=None, sync_perm=True +): import yunohost.permission - return yunohost.permission.user_permission_update(permission, - add=add, remove=remove, - label=label, show_tile=show_tile, - sync_perm=sync_perm) + + return yunohost.permission.user_permission_update( + permission, + add=add, + remove=remove, + label=label, + show_tile=show_tile, + sync_perm=sync_perm, + ) def user_permission_reset(permission, sync_perm=True): import yunohost.permission - return yunohost.permission.user_permission_reset(permission, - sync_perm=sync_perm) + + return yunohost.permission.user_permission_reset(permission, sync_perm=sync_perm) def user_permission_info(permission): import yunohost.permission + return yunohost.permission.user_permission_info(permission) @@ -847,17 +920,18 @@ def user_ssh_add_key(username, key, comment): def user_ssh_remove_key(username, key): return yunohost.ssh.user_ssh_remove_key(username, key) + # # End SSH subcategory # -def _convertSize(num, suffix=''): - for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: +def _convertSize(num, suffix=""): + 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 - return "%.1f%s%s" % (num, 'Yi', suffix) + return "%.1f%s%s" % (num, "Yi", suffix) def _hash_user_password(password): @@ -883,7 +957,7 @@ 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 = "".join([random.SystemRandom().choice(char_set) for x in range(16)]) - salt = '$6$' + salt + '$' - return '{CRYPT}' + crypt.crypt(str(password), salt) + salt = "$6$" + salt + "$" + return "{CRYPT}" + crypt.crypt(str(password), salt) diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index 78077b42a..3000a52f8 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -48,7 +48,4 @@ class YunohostError(MoulinetteError): if not self.log_ref: return super(YunohostError, self).content() else: - return { - "error": self.strerror, - "log_ref": self.log_ref - } + return {"error": self.strerror, "log_ref": self.log_ref} diff --git a/src/yunohost/utils/ldap.py b/src/yunohost/utils/ldap.py index f16472e28..85bca34d7 100644 --- a/src/yunohost/utils/ldap.py +++ b/src/yunohost/utils/ldap.py @@ -36,18 +36,23 @@ def _get_ldap_interface(): if _ldap_interface is None: - conf = {"vendor": "ldap", - "name": "as-root", - "parameters": {'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi', - 'base_dn': 'dc=yunohost,dc=org', - 'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth'}, - "extra": {} - } + conf = { + "vendor": "ldap", + "name": "as-root", + "parameters": { + "uri": "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi", + "base_dn": "dc=yunohost,dc=org", + "user_rdn": "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth", + }, + "extra": {}, + } try: _ldap_interface = ldap.Authenticator(**conf) except MoulinetteLdapIsDownError: - raise YunohostError("Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'") + raise YunohostError( + "Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'" + ) assert_slapd_is_running() @@ -58,7 +63,9 @@ def assert_slapd_is_running(): # Assert slapd is running... if not os.system("pgrep slapd >/dev/null") == 0: - raise YunohostError("Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'") + raise YunohostError( + "Service slapd is not running but is required to perform this action ... You can try to investigate what's happening with 'systemctl status slapd'" + ) # We regularly want to extract stuff like 'bar' in ldap path like @@ -68,10 +75,11 @@ def assert_slapd_is_running(): # e.g. using _ldap_path_extract(path, "foo") on the previous example will # return bar + def _ldap_path_extract(path, info): for element in path.split(","): if element.startswith(info + "="): - return element[len(info + "="):] + return element[len(info + "=") :] # Add this to properly close / delete the ldap interface / authenticator diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 73125dcb8..c84817f98 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -5,18 +5,28 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_json, read_yaml from yunohost.user import user_list, user_group_create, user_group_update -from yunohost.app import app_setting, _installed_apps, _get_app_settings, _set_app_settings -from yunohost.permission import permission_create, user_permission_list, user_permission_update, permission_sync_to_user +from yunohost.app import ( + app_setting, + _installed_apps, + _get_app_settings, + _set_app_settings, +) +from yunohost.permission import ( + permission_create, + user_permission_list, + user_permission_update, + permission_sync_to_user, +) -logger = getActionLogger('yunohost.legacy') +logger = getActionLogger("yunohost.legacy") -class SetupGroupPermissions(): - +class SetupGroupPermissions: @staticmethod def remove_if_exists(target): from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() try: @@ -34,7 +44,9 @@ class SetupGroupPermissions(): try: ldap.remove(dn) except Exception as e: - raise YunohostError("migration_0011_failed_to_remove_stale_object", dn=dn, error=e) + raise YunohostError( + "migration_0011_failed_to_remove_stale_object", dn=dn, error=e + ) @staticmethod def migrate_LDAP_db(): @@ -42,27 +54,30 @@ class SetupGroupPermissions(): logger.info(m18n.n("migration_0011_update_LDAP_database")) from yunohost.utils.ldap import _get_ldap_interface + ldap = _get_ldap_interface() - ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml') + ldap_map = read_yaml( + "/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml" + ) try: SetupGroupPermissions.remove_if_exists("ou=permission") - SetupGroupPermissions.remove_if_exists('ou=groups') + SetupGroupPermissions.remove_if_exists("ou=groups") - attr_dict = ldap_map['parents']['ou=permission'] - ldap.add('ou=permission', attr_dict) + attr_dict = ldap_map["parents"]["ou=permission"] + ldap.add("ou=permission", attr_dict) - attr_dict = ldap_map['parents']['ou=groups'] - ldap.add('ou=groups', attr_dict) + attr_dict = ldap_map["parents"]["ou=groups"] + ldap.add("ou=groups", attr_dict) - attr_dict = ldap_map['children']['cn=all_users,ou=groups'] - ldap.add('cn=all_users,ou=groups', attr_dict) + attr_dict = ldap_map["children"]["cn=all_users,ou=groups"] + ldap.add("cn=all_users,ou=groups", attr_dict) - attr_dict = ldap_map['children']['cn=visitors,ou=groups'] - ldap.add('cn=visitors,ou=groups', attr_dict) + attr_dict = ldap_map["children"]["cn=visitors,ou=groups"] + ldap.add("cn=visitors,ou=groups", attr_dict) - for rdn, attr_dict in ldap_map['depends_children'].items(): + for rdn, attr_dict in ldap_map["depends_children"].items(): ldap.add(rdn, attr_dict) except Exception as e: raise YunohostError("migration_0011_LDAP_update_failed", error=e) @@ -70,15 +85,33 @@ class SetupGroupPermissions(): logger.info(m18n.n("migration_0011_create_group")) # Create a group for each yunohost user - user_list = ldap.search('ou=users,dc=yunohost,dc=org', - '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))', - ['uid', 'uidNumber']) + user_list = ldap.search( + "ou=users,dc=yunohost,dc=org", + "(&(objectclass=person)(!(uid=root))(!(uid=nobody)))", + ["uid", "uidNumber"], + ) for user_info in user_list: - username = user_info['uid'][0] - ldap.update('uid=%s,ou=users' % username, - {'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh']}) - user_group_create(username, gid=user_info['uidNumber'][0], primary_group=True, sync_perm=False) - user_group_update(groupname='all_users', add=username, force=True, sync_perm=False) + username = user_info["uid"][0] + ldap.update( + "uid=%s,ou=users" % username, + { + "objectClass": [ + "mailAccount", + "inetOrgPerson", + "posixAccount", + "userPermissionYnh", + ] + }, + ) + user_group_create( + username, + gid=user_info["uidNumber"][0], + primary_group=True, + sync_perm=False, + ) + user_group_update( + groupname="all_users", add=username, force=True, sync_perm=False + ) @staticmethod def migrate_app_permission(app=None): @@ -88,64 +121,99 @@ class SetupGroupPermissions(): if app: if app not in apps: - logger.error("Can't migrate permission for app %s because it ain't installed..." % app) + logger.error( + "Can't migrate permission for app %s because it ain't installed..." + % app + ) apps = [] else: apps = [app] for app in apps: - permission = app_setting(app, 'allowed_users') - path = app_setting(app, 'path') - domain = app_setting(app, 'domain') + permission = app_setting(app, "allowed_users") + path = app_setting(app, "path") + domain = app_setting(app, "domain") url = "/" if domain and path else None if permission: known_users = list(user_list()["users"].keys()) - allowed = [user for user in permission.split(',') if user in known_users] + allowed = [ + user for user in permission.split(",") if user in known_users + ] else: allowed = ["all_users"] - permission_create(app + ".main", url=url, allowed=allowed, show_tile=True, protected=False, sync_perm=False) + permission_create( + app + ".main", + url=url, + allowed=allowed, + show_tile=True, + protected=False, + sync_perm=False, + ) - app_setting(app, 'allowed_users', delete=True) + app_setting(app, "allowed_users", delete=True) # Migrate classic public app still using the legacy unprotected_uris - if app_setting(app, "unprotected_uris") == "/" or app_setting(app, "skipped_uris") == "/": + if ( + app_setting(app, "unprotected_uris") == "/" + or app_setting(app, "skipped_uris") == "/" + ): user_permission_update(app + ".main", add="visitors", sync_perm=False) permission_sync_to_user() LEGACY_PERMISSION_LABEL = { - ("nextcloud", "skipped"): "api", # .well-known - ("libreto", "skipped"): "pad access", # /[^/]+ - ("leed", "skipped"): "api", # /action.php, for cron task ... - ("mailman", "protected"): "admin", # /admin - ("prettynoemiecms", "protected"): "admin", # /admin - ("etherpad_mypads", "skipped"): "admin", # /admin - ("baikal", "protected"): "admin", # /admin/ - ("couchpotato", "unprotected"): "api", # /api - ("freshrss", "skipped"): "api", # /api/, - ("portainer", "skipped"): "api", # /api/webhooks/ - ("jeedom", "unprotected"): "api", # /core/api/jeeApi.php - ("bozon", "protected"): "user interface", # /index.php - ("limesurvey", "protected"): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts - ("kanboard", "unprotected"): "api", # /jsonrpc.php - ("seafile", "unprotected"): "medias", # /media - ("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish - ("libreerp", "protected"): "admin", # /web/database/manager - ("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.* - ("radicale", "skipped"): "?", # $domain$path_url - ("jirafeau", "protected"): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$ - ("opensondage", "protected"): "admin", # $domain$path_url/admin/ - ("lstu", "protected"): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$ - ("lutim", "protected"): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$ - ("lufi", "protected"): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$ - ("gogs", "skipped"): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs + ("nextcloud", "skipped"): "api", # .well-known + ("libreto", "skipped"): "pad access", # /[^/]+ + ("leed", "skipped"): "api", # /action.php, for cron task ... + ("mailman", "protected"): "admin", # /admin + ("prettynoemiecms", "protected"): "admin", # /admin + ("etherpad_mypads", "skipped"): "admin", # /admin + ("baikal", "protected"): "admin", # /admin/ + ("couchpotato", "unprotected"): "api", # /api + ("freshrss", "skipped"): "api", # /api/, + ("portainer", "skipped"): "api", # /api/webhooks/ + ("jeedom", "unprotected"): "api", # /core/api/jeeApi.php + ("bozon", "protected"): "user interface", # /index.php + ( + "limesurvey", + "protected", + ): "admin", # /index.php?r=admin,/index.php?r=plugins,/scripts + ("kanboard", "unprotected"): "api", # /jsonrpc.php + ("seafile", "unprotected"): "medias", # /media + ("ttrss", "skipped"): "api", # /public.php,/api,/opml.php?op=publish + ("libreerp", "protected"): "admin", # /web/database/manager + ("z-push", "skipped"): "api", # $domain/[Aa]uto[Dd]iscover/.* + ("radicale", "skipped"): "?", # $domain$path_url + ( + "jirafeau", + "protected", + ): "user interface", # $domain$path_url/$","$domain$path_url/admin.php.*$ + ("opensondage", "protected"): "admin", # $domain$path_url/admin/ + ( + "lstu", + "protected", + ): "user interface", # $domain$path_url/login$","$domain$path_url/logout$","$domain$path_url/api$","$domain$path_url/extensions$","$domain$path_url/stats$","$domain$path_url/d/.*$","$domain$path_url/a$","$domain$path_url/$ + ( + "lutim", + "protected", + ): "user interface", # $domain$path_url/stats/?$","$domain$path_url/manifest.webapp/?$","$domain$path_url/?$","$domain$path_url/[d-m]/.*$ + ( + "lufi", + "protected", + ): "user interface", # $domain$path_url/stats$","$domain$path_url/manifest.webapp$","$domain$path_url/$","$domain$path_url/d/.*$","$domain$path_url/m/.*$ + ( + "gogs", + "skipped", + ): "api", # $excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-receive%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/git%-upload%-pack,$excaped_domain$excaped_path/[%w-.]*/[%w-.]*/info/refs } def legacy_permission_label(app, permission_type): - return LEGACY_PERMISSION_LABEL.get((app, permission_type), "Legacy %s urls" % permission_type) + return LEGACY_PERMISSION_LABEL.get( + (app, permission_type), "Legacy %s urls" % permission_type + ) def migrate_legacy_permission_settings(app=None): @@ -155,7 +223,10 @@ def migrate_legacy_permission_settings(app=None): if app: if app not in apps: - logger.error("Can't migrate permission for app %s because it ain't installed..." % app) + logger.error( + "Can't migrate permission for app %s because it ain't installed..." + % app + ) apps = [] else: apps = [app] @@ -164,33 +235,55 @@ def migrate_legacy_permission_settings(app=None): settings = _get_app_settings(app) or {} if settings.get("label"): - user_permission_update(app + ".main", label=settings["label"], sync_perm=False) + user_permission_update( + app + ".main", label=settings["label"], sync_perm=False + ) del settings["label"] def _setting(name): s = settings.get(name) - return s.split(',') if s else [] + return s.split(",") if s else [] - skipped_urls = [uri for uri in _setting('skipped_uris') if uri != '/'] - skipped_urls += ['re:' + regex for regex in _setting('skipped_regex')] - unprotected_urls = [uri for uri in _setting('unprotected_uris') if uri != '/'] - unprotected_urls += ['re:' + regex for regex in _setting('unprotected_regex')] - protected_urls = [uri for uri in _setting('protected_uris') if uri != '/'] - protected_urls += ['re:' + regex for regex in _setting('protected_regex')] + skipped_urls = [uri for uri in _setting("skipped_uris") if uri != "/"] + skipped_urls += ["re:" + regex for regex in _setting("skipped_regex")] + unprotected_urls = [uri for uri in _setting("unprotected_uris") if uri != "/"] + unprotected_urls += ["re:" + regex for regex in _setting("unprotected_regex")] + protected_urls = [uri for uri in _setting("protected_uris") if uri != "/"] + protected_urls += ["re:" + regex for regex in _setting("protected_regex")] if skipped_urls != []: - permission_create(app + ".legacy_skipped_uris", additional_urls=skipped_urls, - auth_header=False, label=legacy_permission_label(app, "skipped"), - show_tile=False, allowed='visitors', protected=True, sync_perm=False) + permission_create( + app + ".legacy_skipped_uris", + additional_urls=skipped_urls, + auth_header=False, + label=legacy_permission_label(app, "skipped"), + show_tile=False, + allowed="visitors", + protected=True, + sync_perm=False, + ) if unprotected_urls != []: - permission_create(app + ".legacy_unprotected_uris", additional_urls=unprotected_urls, - auth_header=True, label=legacy_permission_label(app, "unprotected"), - show_tile=False, allowed='visitors', protected=True, sync_perm=False) + permission_create( + app + ".legacy_unprotected_uris", + additional_urls=unprotected_urls, + auth_header=True, + label=legacy_permission_label(app, "unprotected"), + show_tile=False, + allowed="visitors", + protected=True, + sync_perm=False, + ) if protected_urls != []: - permission_create(app + ".legacy_protected_uris", additional_urls=protected_urls, - auth_header=True, label=legacy_permission_label(app, "protected"), - show_tile=False, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], - protected=True, sync_perm=False) + permission_create( + app + ".legacy_protected_uris", + additional_urls=protected_urls, + auth_header=True, + label=legacy_permission_label(app, "protected"), + show_tile=False, + allowed=user_permission_list()["permissions"][app + ".main"]["allowed"], + protected=True, + sync_perm=False, + ) legacy_permission_settings = [ "skipped_uris", @@ -198,7 +291,7 @@ def migrate_legacy_permission_settings(app=None): "protected_uris", "skipped_regex", "unprotected_regex", - "protected_regex" + "protected_regex", ] for key in legacy_permission_settings: if key in settings: @@ -224,7 +317,7 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): "protected_urls", "skipped_regex", "unprotected_regex", - "protected_regex" + "protected_regex", ] if not any(legacy_rule in persistent for legacy_rule in legacy_rules): @@ -233,9 +326,15 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): if not isinstance(persistent.get("permissions"), dict): persistent["permissions"] = {} - skipped_urls = persistent.get("skipped_urls", []) + ["re:" + r for r in persistent.get("skipped_regex", [])] - protected_urls = persistent.get("protected_urls", []) + ["re:" + r for r in persistent.get("protected_regex", [])] - unprotected_urls = persistent.get("unprotected_urls", []) + ["re:" + r for r in persistent.get("unprotected_regex", [])] + skipped_urls = persistent.get("skipped_urls", []) + [ + "re:" + r for r in persistent.get("skipped_regex", []) + ] + protected_urls = persistent.get("protected_urls", []) + [ + "re:" + r for r in persistent.get("protected_regex", []) + ] + unprotected_urls = persistent.get("unprotected_urls", []) + [ + "re:" + r for r in persistent.get("unprotected_regex", []) + ] known_users = list(user_list()["users"].keys()) @@ -244,35 +343,40 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): del persistent[legacy_rule] if skipped_urls: - persistent["permissions"]['custom_skipped'] = { + persistent["permissions"]["custom_skipped"] = { "users": [], "label": "Custom permissions - skipped", "show_tile": False, "auth_header": False, "public": True, - "uris": skipped_urls + persistent["permissions"].get("custom_skipped", {}).get("uris", []), + "uris": skipped_urls + + persistent["permissions"].get("custom_skipped", {}).get("uris", []), } if unprotected_urls: - persistent["permissions"]['custom_unprotected'] = { + persistent["permissions"]["custom_unprotected"] = { "users": [], "label": "Custom permissions - unprotected", "show_tile": False, "auth_header": True, "public": True, - "uris": unprotected_urls + persistent["permissions"].get("custom_unprotected", {}).get("uris", []), + "uris": unprotected_urls + + persistent["permissions"].get("custom_unprotected", {}).get("uris", []), } if protected_urls: - persistent["permissions"]['custom_protected'] = { + persistent["permissions"]["custom_protected"] = { "users": known_users, "label": "Custom permissions - protected", "show_tile": False, "auth_header": True, "public": False, - "uris": protected_urls + persistent["permissions"].get("custom_protected", {}).get("uris", []), + "uris": protected_urls + + persistent["permissions"].get("custom_protected", {}).get("uris", []), } write_to_json(persistent_file_name, persistent, sort_keys=True, indent=4) - logger.warning("Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system") + logger.warning( + "Yunohost automatically translated some legacy rules in /etc/ssowat/conf.json.persistent to match the new permission system" + ) diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index 909e14784..d96151fa4 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -28,16 +28,21 @@ from moulinette.utils.filesystem import read_file, write_to_file from moulinette.utils.network import download_text from moulinette.utils.process import check_output -logger = logging.getLogger('yunohost.utils.network') +logger = logging.getLogger("yunohost.utils.network") def get_public_ip(protocol=4): - assert protocol in [4, 6], "Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol + assert protocol in [4, 6], ( + "Invalid protocol version for get_public_ip: %s, expected 4 or 6" % protocol + ) cache_file = "/var/cache/yunohost/ipv%s" % protocol cache_duration = 120 # 2 min - if os.path.exists(cache_file) and abs(os.path.getctime(cache_file) - time.time()) < cache_duration: + if ( + os.path.exists(cache_file) + and abs(os.path.getctime(cache_file) - time.time()) < cache_duration + ): ip = read_file(cache_file).strip() ip = ip if ip else None # Empty file (empty string) means there's no IP logger.debug("Reusing IPv%s from cache: %s" % (protocol, ip)) @@ -53,7 +58,9 @@ def get_public_ip_from_remote_server(protocol=4): # We can know that ipv6 is not available directly if this file does not exists if protocol == 6 and not os.path.exists("/proc/net/if_inet6"): - logger.debug("IPv6 appears not at all available on the system, so assuming there's no IP address for that version") + logger.debug( + "IPv6 appears not at all available on the system, so assuming there's no IP address for that version" + ) return None # If we are indeed connected in ipv4 or ipv6, we should find a default route @@ -64,12 +71,18 @@ def get_public_ip_from_remote_server(protocol=4): # But of course IPv6 is more complex ... e.g. on internet cube there's # no default route but a /3 which acts as a default-like route... # e.g. 2000:/3 dev tun0 ... - return r.startswith("default") or (":" in r and re.match(r".*/[0-3]$", r.split()[0])) + return r.startswith("default") or ( + ":" in r and re.match(r".*/[0-3]$", r.split()[0]) + ) + if not any(is_default_route(r) for r in routes): - logger.debug("No default route for IPv%s, so assuming there's no IP address for that version" % protocol) + logger.debug( + "No default route for IPv%s, so assuming there's no IP address for that version" + % protocol + ) return None - url = 'https://ip%s.yunohost.org' % (protocol if protocol != 4 else '') + url = "https://ip%s.yunohost.org" % (protocol if protocol != 4 else "") logger.debug("Fetching IP from %s " % url) try: @@ -83,23 +96,27 @@ def get_network_interfaces(): # Get network devices and their addresses (raw infos from 'ip addr') devices_raw = {} - output = check_output('ip addr show') - for d in re.split(r'^(?:[0-9]+: )', output, flags=re.MULTILINE): + output = check_output("ip addr show") + for d in re.split(r"^(?:[0-9]+: )", output, flags=re.MULTILINE): # Extract device name (1) and its addresses (2) - m = re.match(r'([^\s@]+)(?:@[\S]+)?: (.*)', d, flags=re.DOTALL) + m = re.match(r"([^\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"} + devices = { + name: _extract_inet(addrs) + for name, addrs in devices_raw.items() + if name != "lo" + } return devices def get_gateway(): - output = check_output('ip route show') - m = re.search(r'default via (.*) dev ([a-z]+[0-9]?)', output) + output = check_output("ip route show") + m = re.search(r"default via (.*) dev ([a-z]+[0-9]?)", output) if not m: return None @@ -118,7 +135,9 @@ def external_resolvers(): if not external_resolvers_: resolv_dnsmasq_conf = read_file("/etc/resolv.dnsmasq.conf").split("\n") - external_resolvers_ = [r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver")] + external_resolvers_ = [ + r.split(" ")[1] for r in resolv_dnsmasq_conf if r.startswith("nameserver") + ] # We keep only ipv4 resolvers, otherwise on IPv4-only instances, IPv6 # will be tried anyway resulting in super-slow dig requests that'll wait # until timeout... @@ -127,7 +146,9 @@ def external_resolvers(): return external_resolvers_ -def dig(qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False): +def dig( + qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_answers=False +): """ Do a quick DNS request and avoid the "search" trap inside /etc/resolv.conf """ @@ -151,10 +172,12 @@ def dig(qname, rdtype="A", timeout=5, resolvers="local", edns_size=1500, full_an resolver.timeout = timeout try: answers = resolver.query(qname, rdtype) - except (dns.resolver.NXDOMAIN, - dns.resolver.NoNameservers, - dns.resolver.NoAnswer, - dns.exception.Timeout) as e: + except ( + dns.resolver.NXDOMAIN, + dns.resolver.NoNameservers, + dns.resolver.NoAnswer, + dns.exception.Timeout, + ) as e: return ("nok", (e.__class__.__name__, e)) if not full_answers: @@ -178,28 +201,30 @@ def _extract_inet(string, skip_netmask=False, skip_loopback=True): A dict of {protocol: address} with protocol one of 'ipv4' or 'ipv6' """ - ip4_pattern = r'((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 = r'(((?:[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 += r'/[0-9]{1,2})' if not skip_netmask else ')' - ip6_pattern += r'/[0-9]{1,3})' if not skip_netmask else ')' + ip4_pattern = ( + r"((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 = r"(((?:[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 += r"/[0-9]{1,2})" if not skip_netmask else ")" + ip6_pattern += r"/[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.'): + if skip_loopback and addr.startswith("127."): continue # Limit to only one result - result['ipv4'] = addr + result["ipv4"] = addr break for m in re.finditer(ip6_pattern, string): addr = m.group(1) - if skip_loopback and addr == '::1': + if skip_loopback and addr == "::1": continue # Limit to only one result - result['ipv6'] = addr + result["ipv6"] = addr break return result diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 5672a7233..6cbcf00d5 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -25,9 +25,9 @@ import logging from moulinette.utils.process import check_output from packaging import version -logger = logging.getLogger('yunohost.utils.packages') +logger = logging.getLogger("yunohost.utils.packages") -YUNOHOST_PACKAGES = ['yunohost', 'yunohost-admin', 'moulinette', 'ssowat'] +YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] def get_ynh_package_version(package): @@ -45,8 +45,7 @@ def get_ynh_package_version(package): return {"version": "?", "repo": "?"} out = check_output(cmd).split() # Output looks like : "yunohost (1.2.3) testing; urgency=medium" - return {"version": out[1].strip("()"), - "repo": out[2].strip(";")} + return {"version": out[1].strip("()"), "repo": out[2].strip(";")} def meets_version_specifier(pkg_name, specifier): @@ -63,11 +62,11 @@ def meets_version_specifier(pkg_name, specifier): # context assert pkg_name in YUNOHOST_PACKAGES pkg_version = get_ynh_package_version(pkg_name)["version"] - pkg_version = re.split(r'\~|\+|\-', pkg_version)[0] + pkg_version = re.split(r"\~|\+|\-", pkg_version)[0] pkg_version = version.parse(pkg_version) # Extract operator and version specifier - op, req_version = re.search(r'(<<|<=|=|>=|>>) *([\d\.]+)', specifier).groups() + op, req_version = re.search(r"(<<|<=|=|>=|>>) *([\d\.]+)", specifier).groups() req_version = version.parse(req_version) # Python2 had a builtin that returns (-1, 0, 1) depending on comparison @@ -80,7 +79,7 @@ def meets_version_specifier(pkg_name, specifier): "<=": lambda v1, v2: cmp(v1, v2) in [-1, 0], "=": lambda v1, v2: cmp(v1, v2) in [0], ">=": lambda v1, v2: cmp(v1, v2) in [0, 1], - ">>": lambda v1, v2: cmp(v1, v2) in [1] + ">>": lambda v1, v2: cmp(v1, v2) in [1], } return deb_operators[op](pkg_version, req_version) @@ -92,6 +91,7 @@ def ynh_packages_version(*args, **kwargs): # they don't seem to serve any purpose """Return the version of each YunoHost package""" from collections import OrderedDict + packages = OrderedDict() for package in YUNOHOST_PACKAGES: packages[package] = get_ynh_package_version(package) @@ -106,8 +106,7 @@ def dpkg_is_broken(): # ref: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174 if not os.path.isdir("/var/lib/dpkg/updates/"): return False - return any(re.match("^[0-9]+$", f) - for f in os.listdir("/var/lib/dpkg/updates/")) + return any(re.match("^[0-9]+$", f) for f in os.listdir("/var/lib/dpkg/updates/")) def dpkg_lock_available(): @@ -132,7 +131,7 @@ def _list_upgradable_apt_packages(): # yunohost/stable 3.5.0.2+201903211853 all [upgradable from: 3.4.2.4+201903080053] line = line.split() if len(line) != 6: - logger.warning("Failed to parse this line : %s" % ' '.join(line)) + logger.warning("Failed to parse this line : %s" % " ".join(line)) continue yield { diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 10f2cd9ad..dce337f84 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -25,10 +25,18 @@ import json import string import subprocess -SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", - "root", "test", "rpi"] +SMALL_PWD_LIST = [ + "yunohost", + "olinuxino", + "olinux", + "raspberry", + "admin", + "root", + "test", + "rpi", +] -MOST_USED_PASSWORDS = '/usr/share/yunohost/other/password/100000-most-used.txt' +MOST_USED_PASSWORDS = "/usr/share/yunohost/other/password/100000-most-used.txt" # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ @@ -44,7 +52,6 @@ def assert_password_is_strong_enough(profile, password): class PasswordValidator(object): - def __init__(self, profile): """ Initialize a password validator. @@ -60,7 +67,7 @@ class PasswordValidator(object): # from settings.py because this file is also meant to be # use as a script by ssowat. # (or at least that's my understanding -- Alex) - settings = json.load(open('/etc/yunohost/settings.json', "r")) + settings = json.load(open("/etc/yunohost/settings.json", "r")) setting_key = "security.password." + profile + ".strength" self.validation_strength = int(settings[setting_key]["value"]) except Exception: @@ -173,20 +180,21 @@ class PasswordValidator(object): # stdin to avoid it being shown in ps -ef --forest... command = "grep -q -F -f - %s" % MOST_USED_PASSWORDS p = subprocess.Popen(command.split(), stdin=subprocess.PIPE) - p.communicate(input=password.encode('utf-8')) + p.communicate(input=password.encode("utf-8")) return not bool(p.returncode) # This file is also meant to be used as an executable by # SSOwat to validate password from the portal when an user # change its password. -if __name__ == '__main__': +if __name__ == "__main__": if len(sys.argv) < 2: import getpass + pwd = getpass.getpass("") # print("usage: password.py PASSWORD") else: pwd = sys.argv[1] - status, msg = PasswordValidator('user').validation_summary(pwd) + status, msg = PasswordValidator("user").validation_summary(pwd) print(msg) sys.exit(0) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 5afb865cd..683be94ef 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -8,7 +8,7 @@ from yunohost.domain import _get_maindomain, domain_list from yunohost.utils.network import get_public_ip from yunohost.utils.error import YunohostError -logger = logging.getLogger('yunohost.utils.yunopaste') +logger = logging.getLogger("yunohost.utils.yunopaste") def yunopaste(data): @@ -18,28 +18,41 @@ def yunopaste(data): try: data = anonymize(data) except Exception as e: - logger.warning("For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s" % e) + logger.warning( + "For some reason, YunoHost was not able to anonymize the pasted data. Sorry about that. Be careful about sharing the link, as it may contain somewhat private infos like domain names or IP addresses. Error: %s" + % e + ) data = data.encode() try: r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: - raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e), raw_msg=True) + raise YunohostError( + "Something wrong happened while trying to paste data on paste.yunohost.org : %s" + % str(e), + raw_msg=True, + ) if r.status_code != 200: - raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text), raw_msg=True) + raise YunohostError( + "Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" + % (r.status_code, r.text), + raw_msg=True, + ) try: url = json.loads(r.text)["key"] except: - raise YunohostError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, raw_msg=True) + raise YunohostError( + "Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, + raw_msg=True, + ) return "%s/raw/%s" % (paste_server, url) def anonymize(data): - def anonymize_domain(data, domain, redact): data = data.replace(domain, redact) # This stuff appears sometimes because some folder in diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py index 6d1d085c6..3c13d13ec 100644 --- a/src/yunohost/vendor/acme_tiny/acme_tiny.py +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -1,28 +1,41 @@ #!/usr/bin/env python # Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging -try: - from urllib.request import urlopen, Request # Python 3 -except ImportError: - from urllib2 import urlopen, Request # Python 2 -DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD +try: + from urllib.request import urlopen, Request # Python 3 +except ImportError: + from urllib2 import urlopen, Request # Python 2 + +DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory" 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, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None): - directory, acct_headers, alg, jwk = None, None, None, None # global variables + +def get_crt( + account_key, + csr, + acme_dir, + log=LOGGER, + CA=DEFAULT_CA, + disable_check=False, + directory_url=DEFAULT_DIRECTORY_URL, + contact=None, +): + directory, acct_headers, alg, jwk = None, None, None, None # global variables # helper functions - base64 encode for jose spec def _b64(b): - return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + return base64.urlsafe_b64encode(b).decode("utf8").replace("=", "") # helper function - run external commands def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"): - proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc = subprocess.Popen( + cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) out, err = proc.communicate(cmd_input) if proc.returncode != 0: raise IOError("{0}\n{1}".format(err_msg, err)) @@ -31,50 +44,87 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check # helper function - make request and automatically parse json response def _do_request(url, data=None, err_msg="Error", depth=0): try: - resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"})) - resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers + resp = urlopen( + Request( + url, + data=data, + headers={ + "Content-Type": "application/jose+json", + "User-Agent": "acme-tiny", + }, + ) + ) + resp_data, code, headers = ( + resp.read().decode("utf8"), + resp.getcode(), + resp.headers, + ) except IOError as e: resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e) code, headers = getattr(e, "code", None), {} try: - resp_data = json.loads(resp_data) # try to parse json results + resp_data = json.loads(resp_data) # try to parse json results except ValueError: - pass # ignore json parsing errors - if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce": - raise IndexError(resp_data) # allow 100 retrys for bad nonces + pass # ignore json parsing errors + if ( + depth < 100 + and code == 400 + and resp_data["type"] == "urn:ietf:params:acme:error:badNonce" + ): + raise IndexError(resp_data) # allow 100 retrys for bad nonces if code not in [200, 201, 204]: - raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data)) + raise ValueError( + "{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format( + err_msg, url, data, code, resp_data + ) + ) return resp_data, code, headers # helper function - make signed requests def _send_signed_request(url, payload, err_msg, depth=0): - payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8')) - new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce'] + payload64 = "" if payload is None else _b64(json.dumps(payload).encode("utf8")) + new_nonce = _do_request(directory["newNonce"])[2]["Replay-Nonce"] protected = {"url": url, "alg": alg, "nonce": new_nonce} - protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) - protected64 = _b64(json.dumps(protected).encode('utf8')) - protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8') - out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error") - data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)}) + protected.update( + {"jwk": jwk} if acct_headers is None else {"kid": acct_headers["Location"]} + ) + protected64 = _b64(json.dumps(protected).encode("utf8")) + protected_input = "{0}.{1}".format(protected64, payload64).encode("utf8") + out = _cmd( + ["openssl", "dgst", "-sha256", "-sign", account_key], + stdin=subprocess.PIPE, + cmd_input=protected_input, + err_msg="OpenSSL Error", + ) + data = json.dumps( + {"protected": protected64, "payload": payload64, "signature": _b64(out)} + ) try: - return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth) - except IndexError: # retry bad nonces (they raise IndexError) + return _do_request( + url, data=data.encode("utf8"), err_msg=err_msg, depth=depth + ) + except IndexError: # retry bad nonces (they raise IndexError) return _send_signed_request(url, payload, err_msg, depth=(depth + 1)) # helper function - poll until complete def _poll_until_not(url, pending_statuses, err_msg): result, t0 = None, time.time() - while result is None or result['status'] in pending_statuses: - assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout + while result is None or result["status"] in pending_statuses: + assert time.time() - t0 < 3600, "Polling timeout" # 1 hour timeout time.sleep(0 if result is None else 2) result, _, _ = _send_signed_request(url, None, err_msg) return result # parse account key to get public key log.info("Parsing account key...") - out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") + out = _cmd( + ["openssl", "rsa", "-in", account_key, "-noout", "-text"], + err_msg="OpenSSL Error", + ) pub_pattern = r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)" - pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + pub_hex, pub_exp = re.search( + pub_pattern, 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 alg = "RS256" @@ -83,17 +133,24 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check "kty": "RSA", "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), } - accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':')) - thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + accountkey_json = json.dumps(jwk, sort_keys=True, separators=(",", ":")) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode("utf8")).digest()) # find domains log.info("Parsing CSR...") - out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr)) + out = _cmd( + ["openssl", "req", "-in", csr, "-noout", "-text"], + err_msg="Error loading {0}".format(csr), + ) domains = set([]) - common_name = re.search(r"Subject:.*? CN\s?=\s?([^\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: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) + subject_alt_names = re.search( + r"X509v3 Subject Alternative Name: (?:critical)?\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:"): @@ -102,34 +159,48 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check # get the ACME directory of urls log.info("Getting directory...") - directory_url = CA + "/directory" if CA != DEFAULT_CA else directory_url # backwards compatibility with deprecated CA kwarg + directory_url = ( + CA + "/directory" if CA != DEFAULT_CA else directory_url + ) # backwards compatibility with deprecated CA kwarg directory, _, _ = _do_request(directory_url, err_msg="Error getting directory") log.info("Directory found!") # create account, update contact details (if any), and set the global key identifier log.info("Registering account...") reg_payload = {"termsOfServiceAgreed": True} - account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") + account, code, acct_headers = _send_signed_request( + directory["newAccount"], reg_payload, "Error registering" + ) log.info("Registered!" if code == 201 else "Already registered!") if contact is not None: - account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details") - log.info("Updated contact details:\n{0}".format("\n".join(account['contact']))) + account, _, _ = _send_signed_request( + acct_headers["Location"], + {"contact": contact}, + "Error updating contact details", + ) + log.info("Updated contact details:\n{0}".format("\n".join(account["contact"]))) # create a new order log.info("Creating new order...") order_payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]} - order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order") + order, _, order_headers = _send_signed_request( + directory["newOrder"], order_payload, "Error creating new order" + ) log.info("Order created!") # get the authorizations that need to be completed - for auth_url in order['authorizations']: - authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges") - domain = authorization['identifier']['value'] + for auth_url in order["authorizations"]: + authorization, _, _ = _send_signed_request( + auth_url, None, "Error getting challenges" + ) + domain = authorization["identifier"]["value"] log.info("Verifying {0}...".format(domain)) # find the http-01 challenge and write the challenge file - challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0] - token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + challenge = [c for c in authorization["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: @@ -137,38 +208,64 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check # check that the file is in place try: - wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) - assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization) + wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format( + domain, token + ) + assert disable_check or _do_request(wellknown_url)[0] == keyauthorization except (AssertionError, ValueError) as e: - raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) + raise ValueError( + "Wrote file to {0}, but couldn't download {1}: {2}".format( + wellknown_path, wellknown_url, e + ) + ) # say the challenge is done - _send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain)) - authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain)) - if authorization['status'] != "valid": - raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization)) + _send_signed_request( + challenge["url"], {}, "Error submitting challenges: {0}".format(domain) + ) + authorization = _poll_until_not( + auth_url, + ["pending"], + "Error checking challenge status for {0}".format(domain), + ) + if authorization["status"] != "valid": + raise ValueError( + "Challenge did not pass for {0}: {1}".format(domain, authorization) + ) os.remove(wellknown_path) log.info("{0} verified!".format(domain)) # finalize the order with the csr log.info("Signing certificate...") - csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error") - _send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order") + csr_der = _cmd( + ["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error" + ) + _send_signed_request( + order["finalize"], {"csr": _b64(csr_der)}, "Error finalizing order" + ) # poll the order to monitor when it's done - order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status") - if order['status'] != "valid": + order = _poll_until_not( + order_headers["Location"], + ["pending", "processing"], + "Error checking order status", + ) + if order["status"] != "valid": raise ValueError("Order failed: {0}".format(order)) # download the certificate - certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed") + certificate_pem, _, _ = _send_signed_request( + order["certificate"], None, "Certificate download failed" + ) log.info("Certificate signed!") return certificate_pem + def main(argv=None): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, - description=textwrap.dedent("""\ + 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. @@ -178,21 +275,64 @@ def main(argv=None): 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_chain.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( + "--disable-check", + default=False, + action="store_true", + help="disable checking if the challenge file is hosted correctly before telling the CA", + ) + parser.add_argument( + "--directory-url", + default=DEFAULT_DIRECTORY_URL, + help="certificate authority directory url, default is Let's Encrypt", + ) + parser.add_argument( + "--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!" + ) + parser.add_argument( + "--contact", + metavar="CONTACT", + default=None, + nargs="*", + help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key", ) - 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("--disable-check", default=False, action="store_true", help="disable checking if the challenge file is hosted correctly before telling the CA") - parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt") - parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!") - parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key") 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, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact) + signed_crt = get_crt( + args.account_key, + args.csr, + args.acme_dir, + log=LOGGER, + CA=args.ca, + disable_check=args.disable_check, + directory_url=args.directory_url, + contact=args.contact, + ) sys.stdout.write(signed_crt) -if __name__ == "__main__": # pragma: no cover + +if __name__ == "__main__": # pragma: no cover main(sys.argv[1:]) diff --git a/tests/remove_stale_translated_strings.py b/tests/remove_stale_translated_strings.py index cfecea348..48f2180e4 100644 --- a/tests/remove_stale_translated_strings.py +++ b/tests/remove_stale_translated_strings.py @@ -12,7 +12,14 @@ reference = json.loads(open(locale_folder + "en.json").read()) for locale_file in locale_files: print(locale_file) - this_locale = json.loads(open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict) + this_locale = json.loads( + open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict + ) this_locale_fixed = {k: v for k, v in this_locale.items() if k in reference} - json.dump(this_locale_fixed, open(locale_folder + locale_file, "w"), indent=4, ensure_ascii=False) + json.dump( + this_locale_fixed, + open(locale_folder + locale_file, "w"), + indent=4, + ensure_ascii=False, + ) diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py index f7455bc8e..6876cbcd8 100644 --- a/tests/test_i18n_keys.py +++ b/tests/test_i18n_keys.py @@ -7,11 +7,13 @@ import json import yaml import subprocess -ignore = ["password_too_simple_", - "password_listed", - "backup_method_", - "backup_applying_method_", - "confirm_app_install_"] +ignore = [ + "password_too_simple_", + "password_listed", + "backup_method_", + "backup_applying_method_", + "confirm_app_install_", +] ############################################################################### # Find used keys in python code # @@ -49,7 +51,7 @@ def find_expected_string_keys(): # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries) # Also we expect to have "diagnosis_description_" for each diagnosis - p3 = re.compile(r'[\"\'](diagnosis_[a-z]+_\w+)[\"\']') + p3 = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']") for python_file in glob.glob("data/hooks/diagnosis/*.py"): content = open(python_file).read() for m in p3.findall(content): @@ -57,7 +59,9 @@ def find_expected_string_keys(): # Ignore some name fragments which are actually concatenated with other stuff.. continue yield m - yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[-1] + yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[ + -1 + ] # For each migration, expect to find "migration_description_" for path in glob.glob("src/yunohost/data_migrations/*.py"): @@ -66,7 +70,9 @@ def find_expected_string_keys(): yield "migration_description_" + os.path.basename(path)[:-3] # For each default service, expect to find "service_description_" - for service, info in yaml.safe_load(open("data/templates/yunohost/services.yml")).items(): + for service, info in yaml.safe_load( + open("data/templates/yunohost/services.yml") + ).items(): if info is None: continue yield "service_description_" + service @@ -75,7 +81,9 @@ def find_expected_string_keys(): # A unit operation is created either using the @is_unit_operation decorator # or using OperationLogger( cmd = "grep -hr '@is_unit_operation' src/yunohost/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'" - for funcname in subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n"): + for funcname in ( + subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n") + ): yield "log_" + funcname p4 = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']") @@ -88,7 +96,9 @@ def find_expected_string_keys(): # Will be on a line like : ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", ... p5 = re.compile(r" \(\n*\s*[\"\'](\w[\w\.]+)[\"\'],") content = open("src/yunohost/settings.py").read() - for m in ("global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content)): + for m in ( + "global_settings_setting_" + s.replace(".", "_") for s in p5.findall(content) + ): yield m # Keys for the actionmap ... @@ -134,13 +144,21 @@ def find_expected_string_keys(): for i in [1, 2, 3, 4]: yield "password_too_simple_%s" % i - checks = ["outgoing_port_25_ok", "ehlo_ok", "fcrdns_ok", - "blacklist_ok", "queue_ok", "ehlo_bad_answer", - "ehlo_unreachable", "ehlo_bad_answer_details", - "ehlo_unreachable_details", ] + checks = [ + "outgoing_port_25_ok", + "ehlo_ok", + "fcrdns_ok", + "blacklist_ok", + "queue_ok", + "ehlo_bad_answer", + "ehlo_unreachable", + "ehlo_bad_answer_details", + "ehlo_unreachable_details", + ] for check in checks: yield "diagnosis_mail_%s" % check + ############################################################################### # Load en locale json keys # ############################################################################### @@ -149,6 +167,7 @@ def find_expected_string_keys(): def keys_defined_for_en(): return json.loads(open("locales/en.json").read()).keys() + ############################################################################### # Compare keys used and keys defined # ############################################################################### @@ -163,8 +182,10 @@ def test_undefined_i18n_keys(): undefined_keys = sorted(undefined_keys) if undefined_keys: - raise Exception("Those i18n keys should be defined in en.json:\n" - " - " + "\n - ".join(undefined_keys)) + raise Exception( + "Those i18n keys should be defined in en.json:\n" + " - " + "\n - ".join(undefined_keys) + ) def test_unused_i18n_keys(): @@ -173,5 +194,6 @@ def test_unused_i18n_keys(): unused_keys = sorted(unused_keys) if unused_keys: - raise Exception("Those i18n keys appears unused:\n" - " - " + "\n - ".join(unused_keys)) + raise Exception( + "Those i18n keys appears unused:\n" " - " + "\n - ".join(unused_keys) + ) diff --git a/tests/test_translation_format_consistency.py b/tests/test_translation_format_consistency.py index 81e98f3d5..86d1c3279 100644 --- a/tests/test_translation_format_consistency.py +++ b/tests/test_translation_format_consistency.py @@ -27,7 +27,9 @@ def find_inconsistencies(locale_file): # should also be in the translated string, otherwise the .format # will trigger an exception! subkeys_in_ref = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)) - subkeys_in_this_locale = set(k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])) + subkeys_in_this_locale = set( + k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key]) + ) if any(k not in subkeys_in_ref for k in subkeys_in_this_locale): yield """\n @@ -35,11 +37,16 @@ def find_inconsistencies(locale_file): Format inconsistency for string {key} in {locale_file}:" en.json -> {string} {locale_file} -> {translated_string} -""".format(key=key, string=string.encode("utf-8"), locale_file=locale_file, translated_string=this_locale[key].encode("utf-8")) +""".format( + key=key, + string=string.encode("utf-8"), + locale_file=locale_file, + translated_string=this_locale[key].encode("utf-8"), + ) -@pytest.mark.parametrize('locale_file', locale_files) +@pytest.mark.parametrize("locale_file", locale_files) def test_translation_format_consistency(locale_file): inconsistencies = list(find_inconsistencies(locale_file)) if inconsistencies: - raise Exception(''.join(inconsistencies)) + raise Exception("".join(inconsistencies)) From 6500ac01ae7c0880faff653af776e52feccb8764 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Feb 2021 05:55:19 +0100 Subject: [PATCH 224/363] Make sure that dir exists :/ --- data/hooks/conf_regen/02-ssl | 1 + 1 file changed, 1 insertion(+) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index e65cfca60..e104e94a2 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -58,6 +58,7 @@ do_init_regen() { chmod 640 $LOGFILE # Make sure this conf exists + mkdir -p ${ssl_dir} cp /usr/share/yunohost/templates/ssl/openssl.cnf ${ssl_dir}/openssl.ca.cnf # create default certificates From ce1045a26a3aa289c8124954eb1c1daa7c23f32e Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 2 Feb 2021 11:10:52 +0100 Subject: [PATCH 225/363] fix the postinstall with double quotes --- data/hooks/conf_regen/02-ssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index e104e94a2..6536e7280 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -27,7 +27,7 @@ regen_local_ca() { rm -f index.txt touch index.txt cp /usr/share/yunohost/templates/ssl/openssl.cnf openssl.ca.cnf - sed -i 's/yunohost.org/${domain}/g' openssl.ca.cnf + sed -i "s/yunohost.org/${domain}/g" openssl.ca.cnf openssl req -x509 \ -new \ -config openssl.ca.cnf \ From 5ac2355882a18a42699bb2682231f98163bdc1e3 Mon Sep 17 00:00:00 2001 From: Kayou Date: Tue, 2 Feb 2021 14:14:36 +0100 Subject: [PATCH 226/363] ignore some flake8 errors --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7607c4a41..c25d8bf8f 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ deps = py37-{lint,invalidcode}: flake8 py37-black-{run,check}: black commands = - py37-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor + py37-lint: flake8 src doc data tests --ignore E402,E501,E203,W503 --exclude src/yunohost/vendor py37-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F py37-black-check: black --check --diff src doc data tests py37-black-run: black src doc data tests From 97f26015c6792e54e20fa7a77db0644ca563e486 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Tue, 2 Feb 2021 14:40:29 +0100 Subject: [PATCH 227/363] [fix] kill all bare exceptions --- src/yunohost/backup.py | 12 ++++++------ src/yunohost/certificate.py | 2 +- src/yunohost/dyndns.py | 2 +- src/yunohost/firewall.py | 14 +++++++------- src/yunohost/regenconf.py | 6 +++--- src/yunohost/service.py | 4 ++-- src/yunohost/settings.py | 2 +- src/yunohost/tools.py | 4 ++-- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 0392619de..50765ba5f 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -735,7 +735,7 @@ class BackupManager: } write_to_yaml("%s/permissions.yml" % settings_dir, this_app_permissions) - except: + except Exception: abs_tmp_app_dir = os.path.join(self.work_dir, "apps/", app) shutil.rmtree(abs_tmp_app_dir, ignore_errors=True) logger.error(m18n.n("backup_app_failed", app=app)) @@ -1507,7 +1507,7 @@ class RestoreManager: raise_on_error=True, env=env_dict, )[0] - except: + except Exception: msg = m18n.n("restore_app_failed", app=app_instance_name) logger.error(msg) operation_logger.error(msg) @@ -1944,7 +1944,7 @@ class TarBackupMethod(BackupMethod): self._archive_file, "w:gz" if self._archive_file.endswith(".gz") else "w", ) - except: + except Exception: logger.debug( "unable to open '%s' for writing", self._archive_file, exc_info=1 ) @@ -1995,7 +1995,7 @@ class TarBackupMethod(BackupMethod): self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r", ) - except: + except Exception: logger.debug( "cannot open backup archive '%s'", self._archive_file, exc_info=1 ) @@ -2466,7 +2466,7 @@ def backup_info(name, with_details=False, human_readable=False): with open(info_file) as f: # Retrieve backup info info = json.load(f) - except: + except Exception: logger.debug("unable to load '%s'", info_file, exc_info=1) raise YunohostError( "backup_archive_cant_retrieve_info_json", archive=archive_file @@ -2552,7 +2552,7 @@ def backup_delete(name): for backup_file in files_to_delete: try: os.remove(backup_file) - except: + except Exception: logger.debug("unable to delete '%s'", backup_file, exc_info=1) logger.warning(m18n.n("backup_delete_error", path=backup_file)) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 566c56df1..556af267b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -107,7 +107,7 @@ def certificate_status(domain_list, full=False): try: _check_domain_is_ready_for_ACME(domain) status["ACME_eligible"] = True - except: + except Exception: status["ACME_eligible"] = False del status["domain"] diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 50fa8cce1..d94748881 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -182,7 +182,7 @@ def dyndns_subscribe( os.system("rm -f %s" % key_file) try: error = json.loads(r.text)["error"] - except: + except Exception: error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text) raise YunohostError("dyndns_registration_failed", error=error) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index f9b872c10..28fa768de 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -335,7 +335,7 @@ def firewall_upnp(action="status", no_refresh=False): try: # Remove old cron job os.remove("/etc/cron.d/yunohost-firewall") - except: + except Exception: pass action = "status" no_refresh = False @@ -360,7 +360,7 @@ def firewall_upnp(action="status", no_refresh=False): try: # Remove cron job os.remove(UPNP_CRON_JOB) - except: + except Exception: pass enabled = False if action == "status": @@ -384,7 +384,7 @@ def firewall_upnp(action="status", no_refresh=False): try: # Select UPnP device upnpc.selectigd() - except: + except Exception: logger.debug("unable to select UPnP device", exc_info=1) enabled = False else: @@ -396,7 +396,7 @@ def firewall_upnp(action="status", no_refresh=False): if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) - except: + except Exception: pass firewall["uPnP"][protocol + "_TO_CLOSE"] = [] @@ -405,7 +405,7 @@ def firewall_upnp(action="status", no_refresh=False): if upnpc.getspecificportmapping(port, protocol): try: upnpc.deleteportmapping(port, protocol) - except: + except Exception: pass if not enabled: continue @@ -419,7 +419,7 @@ def firewall_upnp(action="status", no_refresh=False): "yunohost firewall: port %d" % port, "", ) - except: + except Exception: logger.debug( "unable to add port %d using UPnP", port, exc_info=1 ) @@ -488,7 +488,7 @@ def _get_ssh_port(default=22): m = searchf(r"^Port[ \t]+([0-9]+)$", "/etc/ssh/sshd_config", count=-1) if m: return int(m) - except: + except Exception: pass return default diff --git a/src/yunohost/regenconf.py b/src/yunohost/regenconf.py index c99f34a1c..924818e44 100644 --- a/src/yunohost/regenconf.py +++ b/src/yunohost/regenconf.py @@ -445,7 +445,7 @@ def _get_regenconf_infos(): try: with open(REGEN_CONF_FILE, "r") as f: return yaml.load(f) - except: + except Exception: return {} @@ -498,7 +498,7 @@ def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): try: next(diff) next(diff) - except: + except Exception: pass if as_string: @@ -683,7 +683,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): # 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: + except Exception: copy_succeed = False finally: if not copy_succeed: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 00eeb1bd9..bc72301da 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -673,7 +673,7 @@ def _get_services(): try: with open("/etc/yunohost/services.yml", "r") as f: services = yaml.load(f) or {} - except: + except Exception: return {} # some services are marked as None to remove them from YunoHost @@ -807,7 +807,7 @@ def _get_journalctl_logs(service, number="all"): systemd_service, number ) ) - except: + except Exception: import traceback return ( diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index a56e40aad..9bf75ff1d 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -157,7 +157,7 @@ def settings_set(key, value): if isinstance(value, str): try: value = int(value) - except: + except Exception: raise YunohostError( "global_settings_bad_type_for_setting", setting=key, diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index b1d74883e..696dc4ece 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -171,7 +171,7 @@ def tools_adminpw(new_password, check_strength=True): "userPassword": [new_hash], }, ) - except: + except Exception: logger.error("unable to change admin password") raise YunohostError("admin_password_change_failed") else: @@ -315,7 +315,7 @@ def tools_postinstall( # 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: + except Exception: logger.warning( m18n.n("dyndns_provider_unreachable", provider=dyndns_provider) ) From bfd7257a8c886081d5a54880215e2f839772b34b Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 2 Feb 2021 16:16:07 +0100 Subject: [PATCH 228/363] fix linter, remove allow_failure for corresponding tests --- .gitlab/ci/lint.gitlab-ci.yml | 2 -- data/hooks/diagnosis/10-ip.py | 10 +++++----- data/hooks/diagnosis/21-web.py | 2 +- doc/generate_helper_doc.py | 2 +- src/yunohost/app.py | 7 +++---- src/yunohost/data_migrations/0015_migrate_to_buster.py | 4 ++-- src/yunohost/domain.py | 8 ++++---- src/yunohost/hook.py | 8 ++++---- src/yunohost/log.py | 2 +- src/yunohost/permission.py | 2 +- src/yunohost/tests/test_apps_arguments_parsing.py | 2 +- src/yunohost/tests/test_appurl.py | 4 ++-- src/yunohost/tests/test_permission.py | 4 ++-- src/yunohost/tools.py | 4 ++-- src/yunohost/utils/packages.py | 2 +- src/yunohost/utils/yunopaste.py | 2 +- 16 files changed, 31 insertions(+), 34 deletions(-) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 94be52f7d..3ebcf4ee7 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -7,7 +7,6 @@ lint37: stage: lint image: "before-install" needs: [] - allow_failure: true script: - tox -e py37-lint @@ -22,7 +21,6 @@ format-check: stage: lint image: "before-install" needs: [] - allow_failure: true script: - tox -e py37-black-check diff --git a/data/hooks/diagnosis/10-ip.py b/data/hooks/diagnosis/10-ip.py index 718cc56ad..408019668 100644 --- a/data/hooks/diagnosis/10-ip.py +++ b/data/hooks/diagnosis/10-ip.py @@ -193,11 +193,11 @@ class IPDiagnoser(Diagnoser): content = read_file("/etc/resolv.conf").strip().split("\n") # Ignore comments and empty lines content = [ - l.strip() - for l in content - if l.strip() - and not l.strip().startswith("#") - and not l.strip().startswith("search") + line.strip() + for line in content + if line.strip() + and not line.strip().startswith("#") + and not line.strip().startswith("search") ] # We should only find a "nameserver 127.0.0.1" return len(content) == 1 and content[0].split() == ["nameserver", "127.0.0.1"] diff --git a/data/hooks/diagnosis/21-web.py b/data/hooks/diagnosis/21-web.py index f7d8b9a16..81c4d6e48 100644 --- a/data/hooks/diagnosis/21-web.py +++ b/data/hooks/diagnosis/21-web.py @@ -82,7 +82,7 @@ class WebDiagnoser(Diagnoser): summary="diagnosis_http_hairpinning_issue", details=["diagnosis_http_hairpinning_issue_details"], ) - except: + except Exception: # Well I dunno what to do if that's another exception # type... That'll most probably *not* be an hairpinning # issue but something else super weird ... diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index a07f51ffe..1d3a95e39 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -124,7 +124,7 @@ class Parser: # Then we keep this bloc and start a new one # (we ignore helpers containing [internal] ...) - if not "[internal]" in current_block["comments"]: + if "[internal]" not in current_block["comments"]: self.blocks.append(current_block) current_block = { "name": None, diff --git a/src/yunohost/app.py b/src/yunohost/app.py index a5055a246..3d1d16f3c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -30,10 +30,9 @@ import shutil import yaml import time import re -import urllib.parse import subprocess import glob -import urllib.request, urllib.parse, urllib.error +import urllib.parse from collections import OrderedDict from moulinette import msignals, m18n, msettings @@ -912,7 +911,7 @@ def app_install( args_odict = _parse_args_from_manifest(manifest, "install", args=args_dict) # Validate domain / path availability for webapps - _validate_and_normalize_webpath(manifest, args_odict, extracted_app_folder) + _validate_and_normalize_webpath(args_odict, extracted_app_folder) # Attempt to patch legacy helpers ... _patch_legacy_helpers(extracted_app_folder) @@ -3040,7 +3039,7 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions): return parsed_answers_dict -def _validate_and_normalize_webpath(manifest, args_dict, app_folder): +def _validate_and_normalize_webpath(args_dict, app_folder): # If there's only one "domain" and "path", validate that domain/path # is an available url and normalize the path. diff --git a/src/yunohost/data_migrations/0015_migrate_to_buster.py b/src/yunohost/data_migrations/0015_migrate_to_buster.py index 549d0088c..e87c83087 100644 --- a/src/yunohost/data_migrations/0015_migrate_to_buster.py +++ b/src/yunohost/data_migrations/0015_migrate_to_buster.py @@ -229,8 +229,8 @@ class MyMigration(Migration): os.system("apt-mark unhold {}".format(package)) def apt_install(self, cmd): - def is_relevant(l): - return "Reading database ..." not in l.rstrip() + def is_relevant(line): + return "Reading database ..." not in line.rstrip() callbacks = ( lambda l: logger.info("+ " + l.rstrip() + "\r") diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 920000ab0..c51039559 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -607,9 +607,9 @@ def _get_DKIM(domain): dkim = re.match( ( r"^(?P[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+" - '[^"]*"v=(?P[^";]+);' + r'[^"]*"v=(?P[^";]+);' r'[\s"]*k=(?P[^";]+);' - '[\s"]*p=(?P

[^";]+)' + r'[\s"]*p=(?P

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

[^";]+)' + r'[\s"]*p=(?P

[^";]+)' ), dkim_content, re.M | re.S, diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 30f607455..e9857e4f9 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -165,8 +165,8 @@ def hook_list(action, list_by="name", show_info=False): def _append_hook(d, priority, name, path): # Use the name as key and a list of hooks info - the # executed ones with this name - as value - l = d.get(name, list()) - for h in l: + name_list = d.get(name, list()) + for h in name_list: # Only one priority for the hook is accepted if h["priority"] == priority: # Custom hooks overwrite system ones and they @@ -174,8 +174,8 @@ def hook_list(action, list_by="name", show_info=False): if h["path"] != path: h["path"] = path return - l.append({"priority": priority, "path": path}) - d[name] = l + name_list.append({"priority": priority, "path": path}) + d[name] = name_list else: if list_by == "name": diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 1489f109a..d9cb86242 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -168,7 +168,7 @@ def log_show( def _filter_lines(lines, filters=[]): filters = [re.compile(f) for f in filters] - return [l for l in lines if not any(f.search(l.strip()) for f in filters)] + return [line for line in lines if not any(f.search(line.strip()) for f in filters)] # Normalize log/metadata paths and filenames abs_path = path diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index da0ae29ad..3cd67b148 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -839,7 +839,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ We can also have less-trivial regexes like: - re:^\/api\/.*|\/scripts\/api.js$ + re:^/api/.*|/scripts/api.js$ """ from yunohost.domain import domain_list diff --git a/src/yunohost/tests/test_apps_arguments_parsing.py b/src/yunohost/tests/test_apps_arguments_parsing.py index 7ce07f5dd..98dd280ff 100644 --- a/src/yunohost/tests/test_apps_arguments_parsing.py +++ b/src/yunohost/tests/test_apps_arguments_parsing.py @@ -7,7 +7,7 @@ from collections import OrderedDict from moulinette import msignals -from yunohost import domain, user, app +from yunohost import domain, user from yunohost.app import _parse_args_in_yunohost_format, PasswordArgumentParser from yunohost.utils.error import YunohostError diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index be68dd743..f15ed391f 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -16,7 +16,7 @@ def setup_function(function): try: app_remove("register_url_app") - except: + except Exception: pass @@ -24,7 +24,7 @@ def teardown_function(function): try: app_remove("register_url_app") - except: + except Exception: pass diff --git a/src/yunohost/tests/test_permission.py b/src/yunohost/tests/test_permission.py index 1e26d9492..b33c2f213 100644 --- a/src/yunohost/tests/test_permission.py +++ b/src/yunohost/tests/test_permission.py @@ -199,11 +199,11 @@ def teardown_function(function): try: app_remove("permissions_app") - except: + except Exception: pass try: app_remove("legacy_app") - except: + except Exception: pass diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 696dc4ece..e1ebe1307 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -625,12 +625,12 @@ def tools_upgrade( logger.debug("Running apt command :\n{}".format(dist_upgrade)) - def is_relevant(l): + def is_relevant(line): irrelevants = [ "service sudo-ldap already provided", "Reading database ...", ] - return all(i not in l.rstrip() for i in irrelevants) + return all(i not in line.rstrip() for i in irrelevants) callbacks = ( lambda l: logger.info("+ " + l.rstrip() + "\r") diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 6cbcf00d5..4fdf93f9a 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -120,7 +120,7 @@ def _list_upgradable_apt_packages(): upgradable_raw = check_output("LC_ALL=C apt list --upgradable") # Dirty parsing of the output - upgradable_raw = [l.strip() for l in upgradable_raw.split("\n") if l.strip()] + upgradable_raw = [line.strip() for line in upgradable_raw.split("\n") if line.strip()] for line in upgradable_raw: # Remove stupid warning and verbose messages >.> diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 683be94ef..0c3e3c998 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -43,7 +43,7 @@ def yunopaste(data): try: url = json.loads(r.text)["key"] - except: + except Exception: raise YunohostError( "Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, raw_msg=True, From 3bd314e940194b18db949e6d3d22c38414e3cc98 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 2 Feb 2021 17:13:36 +0100 Subject: [PATCH 229/363] readd allow_failure --- .gitlab/ci/lint.gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab/ci/lint.gitlab-ci.yml b/.gitlab/ci/lint.gitlab-ci.yml index 3ebcf4ee7..72faaaf2c 100644 --- a/.gitlab/ci/lint.gitlab-ci.yml +++ b/.gitlab/ci/lint.gitlab-ci.yml @@ -7,6 +7,7 @@ lint37: stage: lint image: "before-install" needs: [] + allow_failure: true script: - tox -e py37-lint @@ -20,6 +21,7 @@ invalidcode37: format-check: stage: lint image: "before-install" + allow_failure: true needs: [] script: - tox -e py37-black-check From 9faf2d89a2765dbb7131233dfcfb798f0b0a8abe Mon Sep 17 00:00:00 2001 From: Kay0u Date: Tue, 2 Feb 2021 17:15:45 +0100 Subject: [PATCH 230/363] fix format --- src/yunohost/log.py | 4 +++- src/yunohost/utils/packages.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d9cb86242..24ecc6713 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -168,7 +168,9 @@ def log_show( def _filter_lines(lines, filters=[]): filters = [re.compile(f) for f in filters] - return [line for line in lines if not any(f.search(line.strip()) for f in filters)] + return [ + line for line in lines if not any(f.search(line.strip()) for f in filters) + ] # Normalize log/metadata paths and filenames abs_path = path diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 4fdf93f9a..3105bc4c7 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -120,7 +120,9 @@ def _list_upgradable_apt_packages(): upgradable_raw = check_output("LC_ALL=C apt list --upgradable") # Dirty parsing of the output - upgradable_raw = [line.strip() for line in upgradable_raw.split("\n") if line.strip()] + upgradable_raw = [ + line.strip() for line in upgradable_raw.split("\n") if line.strip() + ] for line in upgradable_raw: # Remove stupid warning and verbose messages >.> From 717cd26927a07b9414243f41bfd78c144ca98d3e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 3 Feb 2021 03:09:32 +0100 Subject: [PATCH 231/363] Yo dawg i heard you like exceptions so i created an exception in your exception so you can debug while you debug --- 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 556af267b..c48af2c07 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -445,7 +445,7 @@ def certificate_renew( if email: logger.error("Sending email with details to root ...") - _email_renewing_failed(domain, msg + "\n" + e, stack.getvalue()) + _email_renewing_failed(domain, msg + "\n" + str(e), stack.getvalue()) else: logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) operation_logger.success() From dfa0b21fcefc9994e8e93f5f8496816e9e6632a1 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 3 Feb 2021 14:33:10 +0100 Subject: [PATCH 232/363] fixing my mess about firewall _TO_CLOSE --- src/yunohost/firewall.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 28fa768de..1b708a626 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -90,7 +90,7 @@ def firewall_allow( if not no_upnp and port not in firewall["uPnP"][p]: firewall["uPnP"][p].append(port) if ( - firewall["uPnP"][p + "_TO_CLOSE"] + p + "_TO_CLOSE" in firewall["uPnP"] and port in firewall["uPnP"][p + "_TO_CLOSE"] ): firewall["uPnP"][p + "_TO_CLOSE"].remove(port) @@ -158,7 +158,7 @@ def firewall_disallow( # Remove port forwarding with UPnP if upnp and port in firewall["uPnP"][p]: firewall["uPnP"][p].remove(port) - if not firewall["uPnP"][p + "_TO_CLOSE"]: + if p + "_TO_CLOSE" not in firewall["uPnP"]: firewall["uPnP"][p + "_TO_CLOSE"] = [] firewall["uPnP"][p + "_TO_CLOSE"].append(port) @@ -390,7 +390,7 @@ def firewall_upnp(action="status", no_refresh=False): else: # Iterate over ports for protocol in ["TCP", "UDP"]: - if firewall["uPnP"][protocol + "_TO_CLOSE"]: + if protocol + "_TO_CLOSE" in firewall["uPnP"]: for port in firewall["uPnP"][protocol + "_TO_CLOSE"]: # Clean the mapping of this port if upnpc.getspecificportmapping(port, protocol): From ac38e53a7cecbea389c8da55ab9571c97b035416 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 3 Feb 2021 16:50:04 +0100 Subject: [PATCH 233/363] Remove getopts from ynh_exec_as --- data/helpers.d/user | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/data/helpers.d/user b/data/helpers.d/user index f5d4b1680..201444f32 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -166,24 +166,16 @@ ynh_system_user_delete () { # Execute a command as another user # -# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] -# | arg: -u, --user= - the user that will execute the command -# | arg: -n, --command= - the command to be executed +# usage: ynh_exec_as $USER COMMAND [ARG ...] # # Requires YunoHost version 4.1.7 or higher. -ynh_exec_as() -{ - # Declare an array to define the options of this helper. - local legacy_args=uc - local -A args_array=( [u]=user= [c]=command= ) - local user - local command - # Manage arguments with getopts - ynh_handle_getopts_args "$@" +ynh_exec_as() { + local user=$1 + shift 1 if [[ $user = $(whoami) ]]; then - eval "$command" + eval "$@" else - sudo -u "$user" "$command" + sudo -u "$user" "$@" fi } From 6efbc03d43654af562bc52f560dff6e16e786022 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 3 Feb 2021 16:50:04 +0100 Subject: [PATCH 234/363] Remove getopts from ynh_exec_as --- data/helpers.d/user | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/data/helpers.d/user b/data/helpers.d/user index f5d4b1680..201444f32 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -166,24 +166,16 @@ ynh_system_user_delete () { # Execute a command as another user # -# usage: ynh_exec_as --user=USER --command=COMMAND [ARG ...] -# | arg: -u, --user= - the user that will execute the command -# | arg: -n, --command= - the command to be executed +# usage: ynh_exec_as $USER COMMAND [ARG ...] # # Requires YunoHost version 4.1.7 or higher. -ynh_exec_as() -{ - # Declare an array to define the options of this helper. - local legacy_args=uc - local -A args_array=( [u]=user= [c]=command= ) - local user - local command - # Manage arguments with getopts - ynh_handle_getopts_args "$@" +ynh_exec_as() { + local user=$1 + shift 1 if [[ $user = $(whoami) ]]; then - eval "$command" + eval "$@" else - sudo -u "$user" "$command" + sudo -u "$user" "$@" fi } From e1062f145777bddbe6294932f7758c557306838c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 3 Feb 2021 16:59:56 +0100 Subject: [PATCH 235/363] Update changelog for 4.1.7.1 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 4ac5b6cbf..4e893dee0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.1.7.1) stable; urgency=low + + - [enh] helpers: Fix ynh_exec_as regression (ac38e53a7) + + -- Alexandre Aubin Wed, 03 Feb 2021 16:59:05 +0100 + yunohost (4.1.7) stable; urgency=low - [fix] diagnosis: Handle case where DKIM record is split into several pieces (4b876ff0) From 59d3e387f16fdad9fcc51fdd89f7e457e28c2ec0 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Sun, 7 Feb 2021 15:52:50 +0100 Subject: [PATCH 236/363] Update template for the new doc (grav) --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- doc/generate_helper_doc.py | 4 +- doc/helper_doc_template.html | 112 ----------------------------------- doc/helper_doc_template.md | 59 ++++++++++++++++++ 4 files changed, 62 insertions(+), 115 deletions(-) delete mode 100644 doc/helper_doc_template.html create mode 100644 doc/helper_doc_template.md diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index 7227b8acb..c2ad255ba 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -14,7 +14,7 @@ generate-helpers-doc: - cd doc - python generate_helper_doc.py - hub clone https://$GITHUB_TOKEN:x-oauth-basic@github.com/YunoHost/doc.git doc_repo - - cp helpers.html doc_repo/packaging_apps_helpers.md + - cp helpers.md doc_repo/pages/02.contribute/04.packaging_apps/11.helpers/packaging_apps_helpers.md - cd doc_repo # replace ${CI_COMMIT_REF_NAME} with ${CI_COMMIT_TAG} ? - hub checkout -b "${CI_COMMIT_REF_NAME}" diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 1d3a95e39..f2d5bf444 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -39,7 +39,7 @@ def render(helpers): def shell_to_html(shell): return conv.convert(shell, False) - template = open("helper_doc_template.html", "r").read() + template = open("helper_doc_template.md", "r").read() t = Template(template) t.globals["now"] = datetime.datetime.utcnow result = t.render( @@ -48,7 +48,7 @@ def render(helpers): convert=shell_to_html, shell_css=shell_css, ) - open("helpers.html", "w").write(result) + open("helpers.md", "w").write(result) ############################################################################## diff --git a/doc/helper_doc_template.html b/doc/helper_doc_template.html deleted file mode 100644 index 60bfe0ecd..000000000 --- a/doc/helper_doc_template.html +++ /dev/null @@ -1,112 +0,0 @@ - - -

App helpers

- -

Doc auto-generated by this script on {{data.date}} (Yunohost version {{data.version}})

- -{% for category, helpers in data.helpers %} - -

{{ category }}

- -{% for h in helpers %} - -
-
-
-
{{ h.name }}
-
{{ h.brief }}
-
-
-
-

- {% if not '\n' in h.usage %} - Usage: {{ h.usage }} - {% else %} - Usage: {{ h.usage }} - {% endif %} -

- {% if h.args %} -

- Arguments: -

    - {% for infos in h.args %} - {% if infos|length == 2 %} -
  • {{ infos[0] }} : {{ infos[1] }}
  • - {% else %} -
  • {{ infos[0] }}, {{ infos[1] }} : {{ infos[2] }}
  • - {% endif %} - {% endfor %} -
-

- {% endif %} - {% if h.ret %} -

- Returns: {{ h.ret }} -

- {% endif %} - {% if "example" in h.keys() %} -

- Example: {{ h.example }} -

- {% endif %} - {% if "examples" in h.keys() %} -

- Examples:

    - {% for example in h.examples %} - {% if not example.strip().startswith("# ") %} - {{ example }} - {% else %} - {{ example.strip("# ") }} - {% endif %} -
    - {% endfor %} -
-

- {% endif %} - {% if h.details %} -

- Details: -

- {{ h.details.replace('\n', '
') }} -

-

- {% endif %} -

- Dude, show me the code ! -

- -
-
- -
- -{% endfor %} -{% endfor %} - - diff --git a/doc/helper_doc_template.md b/doc/helper_doc_template.md new file mode 100644 index 000000000..1ae6095a3 --- /dev/null +++ b/doc/helper_doc_template.md @@ -0,0 +1,59 @@ +--- +title: App helpers +template: docs +taxonomy: + category: docs +routes: + default: '/packaging_apps_helpers' +--- + +Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/doc/generate_helper_doc.py) on {{data.date}} (Yunohost version {{data.version}}) + +{% for category, helpers in data.helpers %} +### {{ category.upper() }} +{% for h in helpers %} +**{{ h.name }}** +[details summary="{{ h.brief }}" class="helper-card-subtitle text-muted"] +

+ +**Usage**: `{{ h.usage }}` + {% if h.args %} + +**Arguments**: + {% for infos in h.args %} + {% if infos|length == 2 %} +- `{{ infos[0] }}`: {{ infos[1] }} + {% else %} +- `{{ infos[0] }}`, `{{ infos[1] }}`: {{ infos[2] }} + {% endif %} + {% endfor %} + {% endif %} + {% if h.ret %} + +**Returns**: {{ h.ret }} + {% endif %} + {% if "example" in h.keys() %} +**Example**: `{{ h.example }}` + {% endif %} + {% if "examples" in h.keys() %} + +**Examples**: + {% for example in h.examples %} + {% if not example.strip().startswith("# ") %} +- `{{ example }}` + {% else %} +- `{{ example.strip("# ") }}` + {% endif %} + {% endfor %} + {% endif %} + {% if h.details %} + +**Details**: +{{ h.details.replace('\n', '
').replace('_', '\_') }} + {% endif %} + +[Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/data/helpers.d/{{ category }}#L{{ h.line + 1 }}) +[/details] +---------------- +{% endfor %} +{% endfor %} From 933b73400bd76e5befd9f056c56f1b61f0f5c8ab Mon Sep 17 00:00:00 2001 From: Kay0u Date: Sun, 7 Feb 2021 15:54:01 +0100 Subject: [PATCH 237/363] change artifacts filename --- .gitlab/ci/doc.gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/ci/doc.gitlab-ci.yml b/.gitlab/ci/doc.gitlab-ci.yml index c2ad255ba..3b161dc08 100644 --- a/.gitlab/ci/doc.gitlab-ci.yml +++ b/.gitlab/ci/doc.gitlab-ci.yml @@ -22,6 +22,6 @@ generate-helpers-doc: - hub pull-request -m "[CI] Helper for ${CI_COMMIT_REF_NAME}" -p # GITHUB_USER and GITHUB_TOKEN registered here https://gitlab.com/yunohost/yunohost/-/settings/ci_cd artifacts: paths: - - doc/helpers.html + - doc/helpers.md only: - tags From 958c052f1ec72a4445dc342a50f0741380278b25 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 11 Feb 2021 18:35:35 +0100 Subject: [PATCH 238/363] [fix] Avoid admin part of apps to be reachable from visitors --- src/yunohost/utils/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index c84817f98..7479d12c4 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -280,7 +280,7 @@ def migrate_legacy_permission_settings(app=None): auth_header=True, label=legacy_permission_label(app, "protected"), show_tile=False, - allowed=user_permission_list()["permissions"][app + ".main"]["allowed"], + allowed=[], protected=True, sync_perm=False, ) From 6d625d457d4beeb2ac983353b58ae917b261c38e Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 11 Feb 2021 19:42:55 +0100 Subject: [PATCH 239/363] [fix] Unused import --- src/yunohost/utils/legacy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index 7479d12c4..728fcaefc 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -13,7 +13,6 @@ from yunohost.app import ( ) from yunohost.permission import ( permission_create, - user_permission_list, user_permission_update, permission_sync_to_user, ) From 8003288a67536212e5ff9c54461e8cc4cc036b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Tue, 16 Feb 2021 14:14:40 +0100 Subject: [PATCH 240/363] Enhance the jinja template for bash helpers --- doc/helper_doc_template.md | 51 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/doc/helper_doc_template.md b/doc/helper_doc_template.md index 1ae6095a3..1b9fa873d 100644 --- a/doc/helper_doc_template.md +++ b/doc/helper_doc_template.md @@ -12,45 +12,46 @@ Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{{ {% for category, helpers in data.helpers %} ### {{ category.upper() }} {% for h in helpers %} -**{{ h.name }}** +**{{ h.name }}**
[details summary="{{ h.brief }}" class="helper-card-subtitle text-muted"]

**Usage**: `{{ h.usage }}` - {% if h.args %} +{%- if h.args %} -**Arguments**: - {% for infos in h.args %} - {% if infos|length == 2 %} +**Arguments**: + {%- for infos in h.args %} + {%- if infos|length == 2 %} - `{{ infos[0] }}`: {{ infos[1] }} - {% else %} + {%- else %} - `{{ infos[0] }}`, `{{ infos[1] }}`: {{ infos[2] }} - {% endif %} - {% endfor %} - {% endif %} - {% if h.ret %} + {%- endif %} + {%- endfor %} +{%- endif %} +{%- if h.ret %} **Returns**: {{ h.ret }} - {% endif %} - {% if "example" in h.keys() %} +{%- endif %} +{%- if "example" in h.keys() %} + **Example**: `{{ h.example }}` - {% endif %} - {% if "examples" in h.keys() %} +{%- endif %} +{%- if "examples" in h.keys() %} -**Examples**: - {% for example in h.examples %} - {% if not example.strip().startswith("# ") %} +**Examples**: + {% for example in h.examples %} + {% if not example.strip().startswith("# ") %} - `{{ example }}` - {% else %} + {% else %} - `{{ example.strip("# ") }}` - {% endif %} - {% endfor %} - {% endif %} - {% if h.details %} + {% endif %} + {% endfor %} +{%- endif %} +{%- if h.details %} -**Details**: -{{ h.details.replace('\n', '
').replace('_', '\_') }} - {% endif %} +**Details**:
+{{ h.details }} +{%- endif %} [Dude, show me the code!](https://github.com/YunoHost/yunohost/blob/{{ current_commit }}/data/helpers.d/{{ category }}#L{{ h.line + 1 }}) [/details] From aa92954f1b07a23c38abf277638cddf171b44e1b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 20 Feb 2021 17:07:38 +0100 Subject: [PATCH 241/363] Translation typo.. --- locales/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 29f1db1e9..9f8f6a167 100644 --- a/locales/it.json +++ b/locales/it.json @@ -591,7 +591,7 @@ "log_user_permission_update": "Aggiorna gli accessi del permesso '{}'", "log_user_group_update": "Aggiorna il gruppo '{}'", "log_user_group_delete": "Cancella il gruppo '{}'", - "log_user_group_create": "Crea il gruppo '[}'", + "log_user_group_create": "Crea il gruppo '{}'", "log_permission_url": "Aggiorna l'URL collegato al permesso '{}'", "log_permission_delete": "Cancella permesso '{}'", "log_permission_create": "Crea permesso '{}'", From c7c1eaca4ed8086439bb2d528318ff2223790611 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 21 Feb 2021 05:03:07 +0100 Subject: [PATCH 242/363] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) --- data/hooks/conf_regen/34-mysql | 63 ++++++++++++++-------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index ac2395f34..d9374bbf5 100755 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -15,6 +15,31 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + if [[ ! -d /var/lib/mysql/mysql ]] + then + # dpkg-reconfigure will initialize mysql (if it ain't already) + # It enabled auth_socket for root, so no need to define any root password... + # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 + dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 + + systemctl -q is-active mariadb.service \ + || systemctl start mariadb + + sleep 5 + + echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" + fi + + if [ ! -e /etc/yunohost/mysql ] + then + # Dummy password that's not actually used nor meaningful ... + # (because mysql is supposed to be configured to use unix_socket on new setups) + # but keeping it for legacy + # until we merge https://github.com/YunoHost/yunohost/pull/912 ... + ynh_string_random 10 > /etc/yunohost/mysql + chmod 400 /etc/yunohost/mysql + fi + # mysql is supposed to be an alias to mariadb... but in some weird case is not # c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661 # Playing with enable/disable allows to recreate the proper symlinks. @@ -27,44 +52,6 @@ do_post_regen() { systemctl is-active mariadb -q || systemctl start mariadb fi - if [ ! -f /etc/yunohost/mysql ]; then - - # ensure that mysql is running - systemctl -q is-active mysql.service \ - || service mysql start - - # generate and set new root password - mysql_password=$(ynh_string_random 10) - mysqladmin -s -u root -pyunohost password "$mysql_password" || { - if [ $FORCE -eq 1 ]; then - echo "It seems that you have already configured MySQL." \ - "YunoHost needs to have a root access to MySQL to runs its" \ - "applications, and is going to reset the MySQL root password." \ - "You can find this new password in /etc/yunohost/mysql." >&2 - - # set new password with debconf - debconf-set-selections << EOF -$MYSQL_PKG mysql-server/root_password password $mysql_password -$MYSQL_PKG mysql-server/root_password_again password $mysql_password -EOF - - # reconfigure Debian package - 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" \ - "applications, but the MySQL root password is unknown." \ - "You must either pass --force to reset the password or" \ - "put the current one into the file /etc/yunohost/mysql." >&2 - exit 1 - fi - } - - # store new root password - echo "$mysql_password" | tee /etc/yunohost/mysql - chmod 400 /etc/yunohost/mysql - fi - [[ -z "$regen_conf_files" ]] \ || service mysql restart } From 0f8a44028c4283248267991759eef59c20864724 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 21 Feb 2021 05:12:38 +0100 Subject: [PATCH 243/363] Replace \t in conf.json.persistent... --- src/yunohost/utils/legacy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index c84817f98..0067fe05e 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -308,6 +308,9 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): if not os.path.exists(persistent_file_name): return + # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... + os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") + # Ugly hack to try not to misarably fail migration persistent = read_yaml(persistent_file_name) From 29bd3c4a26c3d77c2a09f126c9720867678b0300 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 11 Feb 2021 18:35:35 +0100 Subject: [PATCH 244/363] [fix] Avoid admin part of apps to be reachable from visitors --- src/yunohost/utils/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index f3269cce1..ebc7b65de 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -189,7 +189,7 @@ def migrate_legacy_permission_settings(app=None): if protected_urls != []: permission_create(app + ".legacy_protected_uris", additional_urls=protected_urls, auth_header=True, label=legacy_permission_label(app, "protected"), - show_tile=False, allowed=user_permission_list()['permissions'][app + ".main"]['allowed'], + show_tile=False, allowed=[], protected=True, sync_perm=False) legacy_permission_settings = [ From cd4fdb2b61a64d99c270a370bb33cdebd1cd07c9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 21 Feb 2021 05:03:07 +0100 Subject: [PATCH 245/363] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) --- data/hooks/conf_regen/34-mysql | 63 ++++++++++++++-------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index ac2395f34..d9374bbf5 100755 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -15,6 +15,31 @@ do_pre_regen() { do_post_regen() { regen_conf_files=$1 + if [[ ! -d /var/lib/mysql/mysql ]] + then + # dpkg-reconfigure will initialize mysql (if it ain't already) + # It enabled auth_socket for root, so no need to define any root password... + # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 + dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 + + systemctl -q is-active mariadb.service \ + || systemctl start mariadb + + sleep 5 + + echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" + fi + + if [ ! -e /etc/yunohost/mysql ] + then + # Dummy password that's not actually used nor meaningful ... + # (because mysql is supposed to be configured to use unix_socket on new setups) + # but keeping it for legacy + # until we merge https://github.com/YunoHost/yunohost/pull/912 ... + ynh_string_random 10 > /etc/yunohost/mysql + chmod 400 /etc/yunohost/mysql + fi + # mysql is supposed to be an alias to mariadb... but in some weird case is not # c.f. https://forum.yunohost.org/t/mysql-ne-fonctionne-pas/11661 # Playing with enable/disable allows to recreate the proper symlinks. @@ -27,44 +52,6 @@ do_post_regen() { systemctl is-active mariadb -q || systemctl start mariadb fi - if [ ! -f /etc/yunohost/mysql ]; then - - # ensure that mysql is running - systemctl -q is-active mysql.service \ - || service mysql start - - # generate and set new root password - mysql_password=$(ynh_string_random 10) - mysqladmin -s -u root -pyunohost password "$mysql_password" || { - if [ $FORCE -eq 1 ]; then - echo "It seems that you have already configured MySQL." \ - "YunoHost needs to have a root access to MySQL to runs its" \ - "applications, and is going to reset the MySQL root password." \ - "You can find this new password in /etc/yunohost/mysql." >&2 - - # set new password with debconf - debconf-set-selections << EOF -$MYSQL_PKG mysql-server/root_password password $mysql_password -$MYSQL_PKG mysql-server/root_password_again password $mysql_password -EOF - - # reconfigure Debian package - 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" \ - "applications, but the MySQL root password is unknown." \ - "You must either pass --force to reset the password or" \ - "put the current one into the file /etc/yunohost/mysql." >&2 - exit 1 - fi - } - - # store new root password - echo "$mysql_password" | tee /etc/yunohost/mysql - chmod 400 /etc/yunohost/mysql - fi - [[ -z "$regen_conf_files" ]] \ || service mysql restart } From f398f463f4ef2e72a9f7fddac91a3c9118f4ff43 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 21 Feb 2021 05:12:38 +0100 Subject: [PATCH 246/363] Replace \t in conf.json.persistent... --- src/yunohost/utils/legacy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/utils/legacy.py b/src/yunohost/utils/legacy.py index ebc7b65de..c3f7ab5a9 100644 --- a/src/yunohost/utils/legacy.py +++ b/src/yunohost/utils/legacy.py @@ -215,6 +215,9 @@ def translate_legacy_rules_in_ssowant_conf_json_persistent(): if not os.path.exists(persistent_file_name): return + # Ugly hack because for some reason so many people have tabs in their conf.json.persistent ... + os.system(r"sed -i 's/\t/ /g' /etc/ssowat/conf.json.persistent") + # Ugly hack to try not to misarably fail migration persistent = read_yaml(persistent_file_name) From 1846c3a07b314d25f7a58e1fa79a92a1c8c12d99 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 21 Feb 2021 05:28:30 +0100 Subject: [PATCH 247/363] Update changelog for 4.1.7.2 --- debian/changelog | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/debian/changelog b/debian/changelog index 4e893dee0..95cca2eb8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +yunohost (4.1.7.2) testing; urgency=low + + - [fix] When migration legacy protected permissions, all users were allowed on the new perm (29bd3c4a) + - [fix] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) (cd4fdb2b) + - [fix] Replace \t when converting legacy conf.json.persistent... (f398f463) + + Thanks to all contributors <3 ! (ljf) + + -- Alexandre Aubin Sun, 21 Feb 2021 05:25:49 +0100 + yunohost (4.1.7.1) stable; urgency=low - [enh] helpers: Fix ynh_exec_as regression (ac38e53a7) From 1adff77e3ae241b18bd383ce27d9e04a1c12d099 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 3 Jan 2021 18:19:30 +0100 Subject: [PATCH 248/363] Add multimedia helpers and hooks --- data/helpers.d/multimedia | 87 ++++++++++++++++++++++ data/hooks/post_user_create/ynh_multimedia | 28 +++++++ data/hooks/post_user_delete/ynh_multimedia | 8 ++ debian/control | 1 + 4 files changed, 124 insertions(+) create mode 100644 data/helpers.d/multimedia create mode 100644 data/hooks/post_user_create/ynh_multimedia create mode 100644 data/hooks/post_user_delete/ynh_multimedia diff --git a/data/helpers.d/multimedia b/data/helpers.d/multimedia new file mode 100644 index 000000000..5517917b5 --- /dev/null +++ b/data/helpers.d/multimedia @@ -0,0 +1,87 @@ +readonly MEDIA_GROUP=multimedia +readonly MEDIA_DIRECTORY=/home/yunohost.multimedia + +# Initialize the multimedia directory system +# +# usage: ynh_multimedia_build_main_dir +ynh_multimedia_build_main_dir() { + + ## Création du groupe multimedia + groupadd -f $MEDIA_GROUP + + ## Création des dossiers génériques + mkdir -p "$MEDIA_DIRECTORY" + mkdir -p "$MEDIA_DIRECTORY/share" + mkdir -p "$MEDIA_DIRECTORY/share/Music" + mkdir -p "$MEDIA_DIRECTORY/share/Picture" + mkdir -p "$MEDIA_DIRECTORY/share/Video" + mkdir -p "$MEDIA_DIRECTORY/share/eBook" + + ## Création des dossiers utilisateurs + for user in $(yunohost user list --output-as json | jq -r '.users | keys[]') + do + mkdir -p "$MEDIA_DIRECTORY/$user" + mkdir -p "$MEDIA_DIRECTORY/$user/Music" + mkdir -p "$MEDIA_DIRECTORY/$user/Picture" + mkdir -p "$MEDIA_DIRECTORY/$user/Video" + mkdir -p "$MEDIA_DIRECTORY/$user/eBook" + ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share" + # Création du lien symbolique dans le home de l'utilisateur. + ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia" + # Propriétaires des dossiers utilisateurs. + chown -R $user "$MEDIA_DIRECTORY/$user" + done + # Default yunohost hooks for post_user_create,delete will take care + # of creating/deleting corresponding multimedia folders when users + # are created/deleted in the future... + + ## Application des droits étendus sur le dossier multimedia. + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" + # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY" + # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. + setfacl -RL -m m::rwx "$MEDIA_DIRECTORY" +} + +# Add a directory in yunohost.multimedia +# This "directory" will be a symbolic link to a existing directory. +# +# usage: ynh_multimedia_addfolder "Source directory" "Destination directory" +# +# | arg: -s, --source_dir= - Source directory - The real directory which contains your medias. +# | arg: -d, --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia" +ynh_multimedia_addfolder() { + + # Declare an array to define the options of this helper. + declare -Ar args_array=( [s]=source_dir= [d]=dest_dir= ) + local source_dir + local dest_dir + + # Ajout d'un lien symbolique vers le dossier à partager + ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" + + ## Application des droits étendus sur le dossier ajouté + # Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: + setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. + setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$source_dir" + # Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. + setfacl -RL -m m::rwx "$source_dir" +} + +# Allow an user to have an write authorisation in multimedia directories +# +# usage: ynh_multimedia_addaccess user_name +# +# | arg: -u, --user_name= - The name of the user which gain this access. +ynh_multimedia_addaccess () { + # Declare an array to define the options of this helper. + declare -Ar args_array=( [u]=user_name=) + local user_name + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + groupadd -f multimedia + usermod -a -G multimedia $user_name +} diff --git a/data/hooks/post_user_create/ynh_multimedia b/data/hooks/post_user_create/ynh_multimedia new file mode 100644 index 000000000..560e6a293 --- /dev/null +++ b/data/hooks/post_user_create/ynh_multimedia @@ -0,0 +1,28 @@ +#!/bin/bash + +user=$1 + +readonly MEDIA_GROUP=multimedia +readonly MEDIA_DIRECTORY=/home/yunohost.multimedia + +# We only do this if multimedia directory is enabled (= the folder exists) +[ -e "$MEDIA_DIRECTORY" ] || exit + +mkdir -p "$MEDIA_DIRECTORY/$user" +mkdir -p "$MEDIA_DIRECTORY/$user/Music" +mkdir -p "$MEDIA_DIRECTORY/$user/Picture" +mkdir -p "$MEDIA_DIRECTORY/$user/Video" +mkdir -p "$MEDIA_DIRECTORY/$user/eBook" +ln -sfn "$MEDIA_DIRECTORY/share" "$MEDIA_DIRECTORY/$user/Share" +# Création du lien symbolique dans le home de l'utilisateur. +ln -sfn "$MEDIA_DIRECTORY/$user" "/home/$user/Multimedia" +# Propriétaires des dossiers utilisateurs. +chown -R $user "$MEDIA_DIRECTORY/$user" + +## Application des droits étendus sur le dossier multimedia. +# Droit d'écriture pour le groupe et le groupe multimedia en acl et droit de lecture pour other: +setfacl -RnL -m g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY/$user" +# Application de la même règle que précédemment, mais par défaut pour les nouveaux fichiers. +setfacl -RnL -m d:g:$MEDIA_GROUP:rwX,g::rwX,o:r-X "$MEDIA_DIRECTORY/$user" +# Réglage du masque par défaut. Qui garantie (en principe...) un droit maximal à rwx. Donc pas de restriction de droits par l'acl. +setfacl -RL -m m::rwx "$MEDIA_DIRECTORY/$user" diff --git a/data/hooks/post_user_delete/ynh_multimedia b/data/hooks/post_user_delete/ynh_multimedia new file mode 100644 index 000000000..af06e1637 --- /dev/null +++ b/data/hooks/post_user_delete/ynh_multimedia @@ -0,0 +1,8 @@ +#!/bin/bash + +user=$1 +MEDIA_DIRECTORY=/home/yunohost.multimedia + +if [ -n "$user" ] && [ -e "$MEDIA_DIRECTORY/$user" ]; then + sudo rm -r "$MEDIA_DIRECTORY/$user" +fi diff --git a/debian/control b/debian/control index d95b17f4e..7275cb7b1 100644 --- a/debian/control +++ b/debian/control @@ -26,6 +26,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , rspamd, opendkim-tools, postsrsd, procmail, mailutils , redis-server , metronome (>=3.14.0) + , acl , git, curl, wget, cron, unzip, jq, bc , lsb-release, haveged, fake-hwclock, equivs, lsof, whois Recommends: yunohost-admin From 29b511f5e375beb313aa9cf3d37e1ff840cdca2f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Feb 2021 23:42:52 +0100 Subject: [PATCH 249/363] Fix multimedia hook if not media directory yet --- data/hooks/post_user_create/ynh_multimedia | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/post_user_create/ynh_multimedia b/data/hooks/post_user_create/ynh_multimedia index 560e6a293..441212bbc 100644 --- a/data/hooks/post_user_create/ynh_multimedia +++ b/data/hooks/post_user_create/ynh_multimedia @@ -6,7 +6,7 @@ readonly MEDIA_GROUP=multimedia readonly MEDIA_DIRECTORY=/home/yunohost.multimedia # We only do this if multimedia directory is enabled (= the folder exists) -[ -e "$MEDIA_DIRECTORY" ] || exit +[ -e "$MEDIA_DIRECTORY" ] || exit 0 mkdir -p "$MEDIA_DIRECTORY/$user" mkdir -p "$MEDIA_DIRECTORY/$user/Music" From 59da04e92b172de23f19f1c8546aa20c8be63e26 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Feb 2021 23:46:12 +0100 Subject: [PATCH 250/363] Gotta escape \ during ynh_replace_vars --- data/helpers.d/utils | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 13f84424e..a23d06d2c 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -401,6 +401,7 @@ ynh_replace_vars () { match_string="__${one_var^^}__" match_string=${match_string//${delimit}/"\\${delimit}"} replace_string="${!one_var}" + replace_string=${replace_string//\\/\\\\} replace_string=${replace_string//${delimit}/"\\${delimit}"} # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) From acfea3d76d0a675815e8bf5019e3c69013af58a1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 23 Feb 2021 02:17:04 +0100 Subject: [PATCH 251/363] Define YNH_APP_BASEDIR to be able to properly point to conf folder depending on the app script we're running --- data/helpers.d/fail2ban | 8 ++++---- data/helpers.d/nginx | 6 +++--- data/helpers.d/php | 19 ++++++++----------- data/helpers.d/systemd | 2 +- data/helpers.d/utils | 15 +++++---------- src/yunohost/backup.py | 10 ++++------ 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index da090d2f9..c41226e14 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -87,7 +87,7 @@ port = __PORTS__ filter = __APP__ logpath = __LOGPATH__ maxretry = __MAX_RETRY__ -" > ../conf/f2b_jail.conf +" > $YNH_APP_BASEDIR/conf/f2b_jail.conf echo " [INCLUDES] @@ -95,11 +95,11 @@ before = common.conf [Definition] failregex = __FAILREGEX__ ignoreregex = -" > ../conf/f2b_filter.conf +" > $YNH_APP_BASEDIR/conf/f2b_filter.conf fi - ynh_add_config --template="../conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" - ynh_add_config --template="../conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_jail.conf" --destination="/etc/fail2ban/jail.d/$app.conf" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/f2b_filter.conf" --destination="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload --line_match="(Started|Reloaded) Fail2Ban Service" --log_path=systemd diff --git a/data/helpers.d/nginx b/data/helpers.d/nginx index f7157cd8d..3c6254953 100644 --- a/data/helpers.d/nginx +++ b/data/helpers.d/nginx @@ -22,12 +22,12 @@ ynh_add_nginx_config () { if [ "${path_url:-}" != "/" ] then - ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="../conf/nginx.conf" + ynh_replace_string --match_string="^#sub_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf" else - ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="../conf/nginx.conf" + ynh_replace_string --match_string="^#root_path_only" --replace_string="" --target_file="$YNH_APP_BASEDIR/conf/nginx.conf" fi - ynh_add_config --template="../conf/nginx.conf" --destination="$finalnginxconf" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/nginx.conf" --destination="$finalnginxconf" ynh_systemd_action --service_name=nginx --action=reload diff --git a/data/helpers.d/php b/data/helpers.d/php index 0e1ac48b0..683f252be 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -153,10 +153,7 @@ ynh_add_fpm_config () { if [ $use_template -eq 1 ] then # Usage 1, use the template in conf/php-fpm.conf - local phpfpm_path="../conf/php-fpm.conf" - if [ ! -e "$phpfpm_path" ]; then - phpfpm_path="../settings/conf/php-fpm.conf" # Into the restore script, the PHP-FPM template is not at the same place - fi + local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" # Make sure now that the template indeed exists [ -e "$phpfpm_path" ] || ynh_die --message="Unable to find template to configure PHP-FPM." else @@ -169,7 +166,7 @@ ynh_add_fpm_config () { # Define the values to use for the configuration of PHP. ynh_get_scalable_phpfpm --usage=$usage --footprint=$footprint - local phpfpm_path="../conf/php-fpm.conf" + local phpfpm_path="$YNH_APP_BASEDIR/conf/php-fpm.conf" echo " [__APP__] @@ -204,18 +201,18 @@ pm.process_idle_timeout = 10s fi # Concatene the extra config. - if [ -e ../conf/extra_php-fpm.conf ]; then - cat ../conf/extra_php-fpm.conf >> "$phpfpm_path" + if [ -e $YNH_APP_BASEDIR/conf/extra_php-fpm.conf ]; then + cat $YNH_APP_BASEDIR/conf/extra_php-fpm.conf >> "$phpfpm_path" fi fi local finalphpconf="$fpm_config_dir/pool.d/$app.conf" ynh_add_config --template="$phpfpm_path" --destination="$finalphpconf" - if [ -e "../conf/php-fpm.ini" ] + if [ -e "$YNH_APP_BASEDIR/conf/php-fpm.ini" ] then ynh_print_warn --message="Packagers ! Please do not use a separate php ini file, merge your directives in the pool file instead." - ynh_add_config --template="../conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/php-fpm.ini" --destination="$fpm_config_dir/conf.d/20-$app.ini" fi if [ $dedicated_service -eq 1 ] @@ -228,7 +225,7 @@ pid = /run/php/php__PHPVERSION__-fpm-__APP__.pid error_log = /var/log/php/fpm-php.__APP__.log syslog.ident = php-fpm-__APP__ include = __FINALPHPCONF__ -" > ../conf/php-fpm-$app.conf +" > $YNH_APP_BASEDIR/conf/php-fpm-$app.conf ynh_add_config --template="../config/php-fpm-$app.conf" --destination="$globalphpconf" @@ -245,7 +242,7 @@ ExecReload=/bin/kill -USR2 \$MAINPID [Install] WantedBy=multi-user.target -" > ../conf/$fpm_service +" > $YNH_APP_BASEDIR/conf/$fpm_service # Create this dedicated PHP-FPM service ynh_add_systemd_config --service=$fpm_service --template=$fpm_service diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index 493a724a9..b416e5745 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -22,7 +22,7 @@ ynh_add_systemd_config () { local service="${service:-$app}" local template="${template:-systemd.service}" - ynh_add_config --template="../conf/$template" --destination="/etc/systemd/system/$service.service" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service" systemctl enable $service --quiet systemctl daemon-reload diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 13f84424e..1bdbc98cd 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -1,5 +1,7 @@ #!/bin/bash +YNH_APP_BASEDIR=$([[ "$(basename $0)" =~ ^backup|restore$ ]] && echo '../settings' || echo '..') + # Handle script crashes / failures # # [internal] @@ -112,12 +114,7 @@ ynh_setup_source () { ynh_handle_getopts_args "$@" source_id="${source_id:-app}" # If the argument is not given, source_id equals "app" - local src_file_path="$YNH_CWD/../conf/${source_id}.src" - # In case of restore script the src file is in an other path. - # So try to use the restore path if the general path point to no file. - if [ ! -e "$src_file_path" ]; then - src_file_path="$YNH_CWD/../settings/conf/${source_id}.src" - fi + local src_file_path="$YNH_APP_BASEDIR/conf/${source_id}.src" # Load value from configuration file (see above for a small doc about this file # format) @@ -309,10 +306,8 @@ ynh_add_config () { ynh_handle_getopts_args "$@" local template_path - if [ -f "../conf/$template" ]; then - template_path="../conf/$template" - elif [ -f "../settings/conf/$template" ]; then - template_path="../settings/conf/$template" + if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then + template_path="$YNH_APP_BASEDIR/conf/$template" elif [ -f "$template" ]; then template_path=$template else diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 50765ba5f..408cd6f15 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -704,9 +704,7 @@ class BackupManager: settings_dir = os.path.join(self.work_dir, "apps", app, "settings") logger.info(m18n.n("app_start_backup", app=app)) - tmp_script = ( - None # This is to make sure the var exists later in the 'finally' ... - ) + tmp_folder = tempfile.mkdtemp() try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid="admin") @@ -715,8 +713,8 @@ class BackupManager: 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") + tmp_script = os.path.join(tmp_folder, "backup") subprocess.call(["install", "-Dm555", app_script, tmp_script]) hook_exec( @@ -752,8 +750,8 @@ class BackupManager: # Remove tmp files in all situations finally: - if tmp_script: - filesystem.rm(tmp_script, force=True) + if tmp_folder and os.path.exists(tmp_folder): + shutil.rmtree(tmp_folder) filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) # From 4168a9d19348232beacf03b05a5f1e1705b7e3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Kroul=C3=ADk?= Date: Sat, 23 Jan 2021 20:58:00 +0000 Subject: [PATCH 252/363] Translated using Weblate (Czech) Currently translated at 0.1% (1 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/cs/ --- locales/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 0967ef424..eafada5e6 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -1 +1,3 @@ -{} +{ + "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé" +} From 7f8a4c16aef87cb7ccf0a9e70704cbc1a2d65787 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 25 Jan 2021 19:26:28 +0000 Subject: [PATCH 253/363] Translated using Weblate (German) Currently translated at 63.3% (400 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/de.json b/locales/de.json index efc25f7c5..982de116b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -60,14 +60,14 @@ "dyndns_key_generating": "Generierung des DNS-Schlüssels..., das könnte eine Weile dauern.", "dyndns_registered": "DynDNS Domain registriert", "dyndns_registration_failed": "DynDNS Domain konnte nicht registriert werden: {error:s}", - "dyndns_unavailable": "DynDNS Subdomain ist nicht verfügbar", + "dyndns_unavailable": "Die Domäne {domain:s} ist nicht verfügbar.", "executing_command": "Führe den Behfehl '{command:s}' aus…", "executing_script": "Skript '{script:s}' wird ausgeührt…", - "extracting": "Wird entpackt…", + "extracting": "Wird entpackt...", "field_invalid": "Feld '{:s}' ist unbekannt", "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.", + "firewall_rules_cmd_failed": "Einige Befehle für die Firewallregeln konnten nicht ausgeführt werden. Mehr Informationen sind im Log zu finden.", "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", @@ -204,10 +204,10 @@ "app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}", "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository...", "global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}", - "global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting:s}. Habe '{choice:s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices:s}", + "global_settings_bad_choice_for_enum": "Der Wert dieses Einstellungsparameters {setting:s} ist ungültig. Der Wert den Sie eingegeben haben: '{choice:s}', die gültigen Werte für diese Einstellung: {available_choices:s}", "file_does_not_exist": "Die Datei {path:s} existiert nicht.", "experimental_feature": "Warnung: Diese Funktion ist experimentell und gilt nicht als stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.", - "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domain(s) {domain:s} nicht bereitstellen.", + "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domäne(n) {domain:s} nicht bereitstellen.", "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain:s} auf {provider:s} verfügbar ist.", "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.", "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt Ihnen die * empfohlene * Konfiguration. Die DNS-Konfiguration wird NICHT für Sie eingerichtet. Es liegt in Ihrer Verantwortung, Ihre DNS-Zone in Ihrem Registrar gemäß dieser Empfehlung zu konfigurieren.", From 7d4ef2b14224328b763fa0bdf6e6ebd9745997b1 Mon Sep 17 00:00:00 2001 From: Mathieu Massaviol Date: Thu, 28 Jan 2021 09:44:51 +0000 Subject: [PATCH 254/363] Translated using Weblate (French) Currently translated at 100.0% (631 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index b65268fb7..dfe2e372e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -54,7 +54,7 @@ "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nAfin de pouvoir procéder à la suppression du domaine, vous devez préalablement :\n- soit désinstaller toutes ces applications avec la commande 'yunohost app remove nom-de-l-application' ;\n- soit déplacer toutes ces applications vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application'", + "domain_uninstall_app_first": "Ces applications sont toujours installées sur votre domaine :\n{apps}\n\nVeuillez les désinstaller avec la commande 'yunohost app remove nom-de-l-application' ou les déplacer vers un autre domaine avec la commande 'yunohost app change-url nom-de-l-application' avant de procéder à la suppression du domaine", "domain_unknown": "Domaine inconnu", "done": "Terminé", "downloading": "Téléchargement en cours …", @@ -327,7 +327,7 @@ "password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux", "password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des majuscules, des minuscules et des caractères spéciaux", "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n’a pas pu le propager au mot de passe root !", - "aborting": "Annulation.", + "aborting": "Annulation en cours.", "app_not_upgraded": "L’application {failed_app} n’a pas été mise à jour et par conséquence les applications suivantes n’ont pas été mises à jour : {apps}", "app_start_install": "Installation de {app}...", "app_start_remove": "Suppression de {app}...", From c6974932b44bc07ff3cf0a1f874d47ec6175db9b Mon Sep 17 00:00:00 2001 From: ppr Date: Thu, 28 Jan 2021 09:41:44 +0000 Subject: [PATCH 255/363] Translated using Weblate (French) Currently translated at 100.0% (631 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index dfe2e372e..7d016a70e 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -650,7 +650,7 @@ "migration_description_0018_xtable_to_nftable": "Migrer les anciennes règles de trafic réseau vers le nouveau système basé sur nftables", "service_description_php7.3-fpm": "Exécute les applications écrites en PHP avec NGINX", "migration_0018_failed_to_reset_legacy_rules": "La réinitialisation des règles iptable par défaut a échoué : {error}", - "migration_0018_failed_to_migrate_iptables_rules": "La migration des règles iptables héritées vers nftables a échoué: {error}", + "migration_0018_failed_to_migrate_iptables_rules": "Échec de la migration des anciennes règles iptables vers nftables : {error}", "migration_0017_not_enough_space": "Laissez suffisamment d'espace disponible dans {path} avant de lancer la migration.", "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 est installé mais pas posgreSQL 11 ? Il s'est sans doute passé quelque chose d'étrange sur votre système :(...", "migration_0017_postgresql_96_not_installed": "PostgreSQL n'a pas été installé sur votre système. Aucune opération à effectuer.", From 6880a6f01741f3f88a7aeff0bf42db0002d3b66d Mon Sep 17 00:00:00 2001 From: Yifei Ding Date: Sun, 31 Jan 2021 09:36:34 +0000 Subject: [PATCH 256/363] Translated using Weblate (Chinese (Simplified)) Currently translated at 2.2% (14 of 631 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/zh_Hans/ --- locales/zh_Hans.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/zh_Hans.json b/locales/zh_Hans.json index d11e570d0..e72cd52da 100644 --- a/locales/zh_Hans.json +++ b/locales/zh_Hans.json @@ -2,7 +2,7 @@ "password_too_simple_1": "密码长度至少为8个字符", "backup_created": "备份已创建", "app_start_remove": "正在删除{app}……", - "admin_password_change_failed": "不能修改密码", + "admin_password_change_failed": "无法修改密码", "admin_password_too_long": "请选择一个小于127个字符的密码", "app_upgrade_failed": "不能升级{app:s}:{error}", "app_id_invalid": "无效 app ID", From a7152e2c69ac07aaaed15b1f78303164d3d1f6c7 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 1 Feb 2021 19:22:02 +0000 Subject: [PATCH 257/363] Translated using Weblate (German) Currently translated at 63.6% (402 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 982de116b..23f9bc217 100644 --- a/locales/de.json +++ b/locales/de.json @@ -489,5 +489,7 @@ "global_settings_setting_smtp_relay_port": "SMTP Relay Port", "global_settings_setting_smtp_allow_ipv6": "Erlaube die Nutzung von IPv6 um Mails zu empfangen und zu versenden", "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", - "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen." + "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen.", + "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", + "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen." } From d5f82856c6c0db3665fbdffcb76ff21b08f9eae8 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Tue, 2 Feb 2021 11:15:12 +0000 Subject: [PATCH 258/363] Translated using Weblate (German) Currently translated at 63.6% (402 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/de.json b/locales/de.json index 23f9bc217..13534f5f3 100644 --- a/locales/de.json +++ b/locales/de.json @@ -170,9 +170,9 @@ "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain:s} ist 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 nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", - "certmanager_domain_http_not_working": "Die Domäne {domain:s} scheint über HTTP nicht erreichbar zu sein. Für weitere Informationen überprüfen Sie bitte die Kategorie 'Web' im Diagnose-Bereich. (Wenn Sie wißen was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu überspringen.)", + "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. (Wenn du weißt was du tust, nutze \"--no-checks\" um die überprüfung zu überspringen.)", "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": "Die DNS-Einträge der Domäne {domain:s} unterscheiden sich von der IP dieses Servers. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS Propagation mittels Website überprüfen) (Wenn Sie wißen was Sie tun, können Sie --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. Für weitere Informationen überprüfe bitte die 'DNS records' (basic) Kategorie in der Diagnose. 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_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 Zertifikat installiert.", @@ -377,7 +377,7 @@ "service_reloaded_or_restarted": "Der Dienst '{service:s}' wurde erfolgreich neu geladen oder gestartet", "service_restarted": "Der Dienst '{service:s}' wurde neu gestartet", "service_regen_conf_is_deprecated": "'yunohost service regen-conf' ist veraltet! Bitte verwenden Sie stattdessen 'yunohost tools regen-conf'.", - "certmanager_warning_subdomain_dns_record": "Die Subdomain '{subdomain:s}' löst nicht dieselbe IP wie '{domain:s} auf. Einige Funktionen werden nicht verfügbar sein, solange Sie dies nicht beheben und das Zertifikat erneuern.", + "certmanager_warning_subdomain_dns_record": "Die Subdomäne \"{subdomain:s}\" löst nicht zur gleichen IP Adresse auf wie \"{domain:s}\". Einige Funktionen sind nicht verfügbar bis du dies behebst und die Zertifikate neu erzeugst.", "diagnosis_ports_ok": "Port {port} ist von außen erreichbar.", "diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})", "diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen den ausgehenden Port 25 auf Ihrer Router-Konfigurationsoberfläche oder Ihrer Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbieter kann es sein, daß Sie verlangen, daß man dafür ein Support-Ticket sendet).", From 0ff75e1e2274747fe7ee027ed811c078629a89cc Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 3 Feb 2021 06:40:20 +0000 Subject: [PATCH 259/363] Translated using Weblate (German) Currently translated at 64.5% (408 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/locales/de.json b/locales/de.json index 13534f5f3..a979031b3 100644 --- a/locales/de.json +++ b/locales/de.json @@ -65,9 +65,9 @@ "executing_script": "Skript '{script:s}' wird ausgeührt…", "extracting": "Wird entpackt...", "field_invalid": "Feld '{:s}' ist unbekannt", - "firewall_reload_failed": "Die Firewall konnte nicht neu geladen werden", - "firewall_reloaded": "Die Firewall wurde neu geladen", - "firewall_rules_cmd_failed": "Einige Befehle für die Firewallregeln konnten nicht ausgeführt werden. Mehr Informationen sind im Log zu finden.", + "firewall_reload_failed": "Firewall konnte nicht neu geladen werden", + "firewall_reloaded": "Firewall neu geladen", + "firewall_rules_cmd_failed": "Einige Befehle für die Firewallregeln sind gescheitert. Mehr Informationen im Log.", "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", @@ -203,10 +203,10 @@ "backup_archive_writing_error": "Die Dateien '{source:s} (im Ordner '{dest:s}') konnten nicht in das komprimierte Archiv-Backup '{archive:s}' hinzugefügt werden", "app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}", "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository...", - "global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}", - "global_settings_bad_choice_for_enum": "Der Wert dieses Einstellungsparameters {setting:s} ist ungültig. Der Wert den Sie eingegeben haben: '{choice:s}', die gültigen Werte für diese Einstellung: {available_choices:s}", - "file_does_not_exist": "Die Datei {path:s} existiert nicht.", - "experimental_feature": "Warnung: Diese Funktion ist experimentell und gilt nicht als stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.", + "global_settings_bad_type_for_setting": "Falscher Typ der Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwarteter Typ: {expected_type:s}", + "global_settings_bad_choice_for_enum": "Wert des Einstellungsparameters {setting:s} ungültig. Der Wert den Sie eingegeben haben: '{choice:s}', die gültigen Werte für diese Einstellung: {available_choices:s}", + "file_does_not_exist": "Die Datei {path: s} existiert nicht.", + "experimental_feature": "Warnung: Der Maintainer hat diese Funktion als experimentell gekennzeichnet. Sie ist nicht stabil. Sie sollten sie nur verwenden, wenn Sie wissen, was Sie tun.", "dyndns_domain_not_provided": "Der DynDNS-Anbieter {provider:s} kann die Domäne(n) {domain:s} nicht bereitstellen.", "dyndns_could_not_check_available": "Konnte nicht überprüfen, ob {domain:s} auf {provider:s} verfügbar ist.", "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.", @@ -491,5 +491,6 @@ "global_settings_setting_pop3_enabled": "Aktiviere das POP3 Protokoll für den Mailserver", "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen.", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", - "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen." + "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", + "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden." } From def4115fd7b46b64fdab5ca426348b88cdbabbd7 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 3 Feb 2021 19:24:11 +0000 Subject: [PATCH 260/363] Translated using Weblate (German) Currently translated at 64.7% (409 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index a979031b3..0bacbc39a 100644 --- a/locales/de.json +++ b/locales/de.json @@ -492,5 +492,6 @@ "domain_cannot_remove_main_add_new_one": "Du kannst \"{domain:s}\" nicht entfernen da es die Hauptdomain und deine einzige Domain ist, erst musst erst eine andere Domain hinzufügen indem du eingibst \"yunohost domain add \", setze es dann als deine Hauptdomain indem du eingibst \"yunohost domain main-domain -n \", erst jetzt kannst du die domain \"{domain:s}\" entfernen.", "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", - "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden." + "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden.", + "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen." } From eacc571cc3b4bef24d4a614c02457e831ec73d4e Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 5 Feb 2021 07:17:42 +0000 Subject: [PATCH 261/363] Translated using Weblate (German) Currently translated at 68.3% (432 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/locales/de.json b/locales/de.json index 0bacbc39a..48de3df5f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -68,12 +68,12 @@ "firewall_reload_failed": "Firewall konnte nicht neu geladen werden", "firewall_reloaded": "Firewall neu geladen", "firewall_rules_cmd_failed": "Einige Befehle für die Firewallregeln sind gescheitert. Mehr Informationen im Log.", - "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_exec_failed": "Konnte Skript nicht ausführen: {path:s}", + "hook_exec_not_terminated": "Skript ist nicht normal beendet worden: {path:s}", + "hook_list_by_invalid": "Dieser Wert kann nicht verwendet werden, um Hooks anzuzeigen", "hook_name_unknown": "Hook '{name:s}' ist nicht bekannt", "installation_complete": "Installation vollständig", - "installation_failed": "Installation fehlgeschlagen", + "installation_failed": "Etwas ist mit der Installation falsch gelaufen", "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", @@ -253,14 +253,14 @@ "backup_copying_to_organize_the_archive": "Kopieren von {size:s} MB, um das Archiv zu organisieren", "global_settings_setting_security_ssh_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den SSH-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "group_deleted": "Gruppe '{group}' gelöscht", - "group_deletion_failed": "Kann Gruppe '{group}' nicht löschen", + "group_deletion_failed": "Konnte Gruppe '{group}' nicht löschen", "dyndns_provider_unreachable": "DynDNS-Anbieter {provider} kann nicht erreicht werden: Entweder ist dein YunoHost nicht korrekt mit dem Internet verbunden oder der Dynette-Server ist ausgefallen.", "group_created": "Gruppe '{group}' angelegt", - "group_creation_failed": "Kann Gruppe '{group}' nicht anlegen", + "group_creation_failed": "Konnte Gruppe '{group}' nicht anlegen", "group_unknown": "Die Gruppe '{group:s}' ist unbekannt", "group_updated": "Gruppe '{group:s}' erneuert", "group_update_failed": "Kann Gruppe '{group:s}' nicht aktualisieren: {error}", - "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", + "log_does_exists": "Es gibt kein Operationsprotokoll mit dem Namen'{log}', verwende 'yunohost log list', um alle verfügbaren Operationsprotokolle anzuzeigen", "log_operation_unit_unclosed_properly": "Die Operationseinheit wurde nicht richtig geschlossen", "global_settings_setting_security_postfix_compatibility": "Kompatibilität vs. Sicherheitskompromiss für den Postfix-Server. Beeinflusst die Chiffren (und andere sicherheitsrelevante Aspekte)", "log_category_404": "Die Log-Kategorie '{category}' existiert nicht", @@ -274,28 +274,28 @@ "backup_php5_to_php7_migration_may_fail": "Dein Archiv konnte nicht für PHP 7 konvertiert werden, Du kannst deine PHP-Anwendungen möglicherweise nicht wiederherstellen (Grund: {error:s})", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Erlaubt die Verwendung eines (veralteten) DSA-Hostkeys für die SSH-Daemon-Konfiguration", "global_settings_setting_example_string": "Beispiel einer string Option", - "log_app_remove": "Entferne die Anwendung '{}'", + "log_app_remove": "Entferne die Applikation '{}'", "global_settings_setting_example_int": "Beispiel einer int Option", "global_settings_cant_open_settings": "Einstellungsdatei konnte nicht geöffnet werden, Grund: {reason:s}", "global_settings_cant_write_settings": "Einstellungsdatei konnte nicht gespeichert werden, Grund: {reason:s}", - "log_app_install": "Installiere die Anwendung '{}'", + "log_app_install": "Installiere die Applikation '{}'", "global_settings_reset_success": "Frühere Einstellungen werden nun auf {path:s} gesichert", - "log_app_upgrade": "Upgrade der Anwendung '{}'", - "good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", + "log_app_upgrade": "Upgrade der Applikation '{}'", + "good_practices_about_admin_password": "Sie sind nun dabei, ein neues Administrationspasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein, obwohl es sinnvoll ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "log_corrupted_md_file": "Die mit Protokollen verknüpfte YAML-Metadatendatei ist beschädigt: '{md_file}\nFehler: {error}''", "global_settings_cant_serialize_settings": "Einstellungsdaten konnten nicht serialisiert werden, Grund: {reason:s}", "log_help_to_get_failed_log": "Der Vorgang'{desc}' konnte nicht abgeschlossen werden. Bitte teile das vollständige Protokoll dieser Operation mit dem Befehl 'yunohost log share {name}', um Hilfe zu erhalten", "backup_no_uncompress_archive_dir": "Dieses unkomprimierte Archivverzeichnis gibt es nicht", - "log_app_change_url": "Ändere die URL der Anwendung '{}'", + "log_app_change_url": "Ändere die URL der Applikation '{}'", "global_settings_setting_security_password_user_strength": "Stärke des Benutzerpassworts", - "good_practices_about_user_password": "Du bist nun dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein - obwohl es ratsam ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", + "good_practices_about_user_password": "Sie sind dabei, ein neues Benutzerpasswort zu definieren. Das Passwort sollte mindestens 8 Zeichen lang sein, obwohl es ratsam ist, ein längeres Passwort (z.B. eine Passphrase) und/oder eine Variation von Zeichen (Groß- und Kleinschreibung, Ziffern und Sonderzeichen) zu verwenden.", "global_settings_setting_example_enum": "Beispiel einer enum Option", "log_link_to_failed_log": "Der Vorgang konnte nicht abgeschlossen werden '{desc}'. Bitte gib das vollständige Protokoll dieser Operation mit Klicken Sie hier an, um Hilfe zu erhalten", "backup_cant_mount_uncompress_archive": "Das unkomprimierte Archiv konnte nicht als schreibgeschützt gemountet werden", "backup_csv_addition_failed": "Es konnten keine Dateien zur Sicherung in die CSV-Datei hinzugefügt werden", "global_settings_setting_security_password_admin_strength": "Stärke des Admin-Passworts", "global_settings_key_doesnt_exists": "Der Schlüssel'{settings_key:s}' existiert nicht in den globalen Einstellungen, du kannst alle verfügbaren Schlüssel sehen, indem du 'yunohost settings list' ausführst", - "log_app_makedefault": "Mache '{}' zur Standard-Anwendung", + "log_app_makedefault": "Mache '{}' zur Standard-Applikation", "hook_json_return_error": "Konnte die Rückkehr vom Einsprungpunkt {path:s} nicht lesen. Fehler: {msg:s}. Unformatierter Inhalt: {raw_content}", "app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.", "app_install_failed": "{app} kann nicht installiert werden: {error}", @@ -493,5 +493,13 @@ "diagnosis_rootfstotalspace_critical": "Das Root-Filesystem hat noch freien Speicher von {space}. Das ist besorngiserregend! Der Speicher wird schnell aufgebraucht sein. 16 GB für das Root-Filesystem werden empfohlen.", "diagnosis_rootfstotalspace_warning": "Das Root-Filesystem hat noch freien Speicher von {space}. Möglich, dass das in Ordnung ist. Vielleicht ist er aber auch schneller aufgebraucht. 16 GB für das Root-Filesystem werden empfohlen.", "global_settings_setting_smtp_relay_host": "Zu verwendender SMTP-Relay-Host um E-Mails zu versenden. Er wird anstelle dieser YunoHost-Instanz verwendet. Nützlich, wenn Sie in einer der folgenden Situationen sind: Ihr ISP- oder VPS-Provider hat Ihren Port 25 geblockt, eine Ihrer residentiellen IPs ist auf DUHL gelistet, Sie können keinen Reverse-DNS konfigurieren oder dieser Server ist nicht direkt mit dem Internet verbunden und Sie möchten einen anderen verwenden, um E-Mails zu versenden.", - "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen." + "global_settings_setting_backup_compress_tar_archives": "Beim Erstellen von Backups die Archive komprimieren (.tar.gz) anstelle von unkomprimierten Archiven (.tar). N.B. : Diese Option ergibt leichtere Backup-Archive, aber das initiale Backupprozedere wird länger dauern und mehr CPU brauchen.", + "log_remove_on_failed_restore": "'{}' entfernen nach einer fehlerhaften Wiederherstellung aus einem Backup-Archiv", + "log_backup_restore_app": "'{}' aus einem Backup-Archiv wiederherstellen", + "log_backup_restore_system": "System aus einem Backup-Archiv wiederherstellen", + "log_available_on_yunopaste": "Das Protokoll ist nun via {url} verfügbar", + "log_app_config_apply": "Wende die Konfiguration auf die Applikation '{}' an", + "log_app_config_show_panel": "Zeige das Konfigurations-Panel der Applikation '{}'", + "log_app_action_run": "Führe Aktion der Applikation '{}' aus", + "invalid_regex": "Ungültige Regex:'{regex:s}'" } From 46ceef146770310ee91f55c60f4d96aba01e2a89 Mon Sep 17 00:00:00 2001 From: xaloc33 Date: Mon, 8 Feb 2021 19:09:26 +0000 Subject: [PATCH 262/363] Translated using Weblate (Catalan) Currently translated at 100.0% (632 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/ca/ --- locales/ca.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 7924193d0..b6888d391 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -714,5 +714,8 @@ "additional_urls_already_removed": "URL addicional «{url:s}» ja ha estat eliminada per al permís «{permission:s}»", "additional_urls_already_added": "URL addicional «{url:s}» ja ha estat afegida per al permís «{permission:s}»", "diagnosis_backports_in_sources_list": "Sembla que apt (el gestor de paquets) està configurat per utilitzar el repositori backports. A menys de saber el que esteu fent, recomanem fortament no instal·lar paquets de backports, ja que poder causar inestabilitats o conflictes en el sistema.", - "diagnosis_basesystem_hardware_model": "El model del servidor és {model}" + "diagnosis_basesystem_hardware_model": "El model del servidor és {model}", + "postinstall_low_rootfsspace": "El sistema de fitxers arrel té un total de menys de 10 GB d'espai, el que es preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomana tenir un mínim de 16 GB per al sistema de fitxers arrel. Si voleu instal·lar YunoHost tot i aquest avís, torneu a executar la postinstal·lació amb --force-diskspace", + "diagnosis_rootfstotalspace_critical": "El sistema de fitxers arrel només té {space} en total i és preocupant! És molt probable que us quedeu sense espai ràpidament! Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel.", + "diagnosis_rootfstotalspace_warning": "El sistema de fitxers arrel només té {space} en total. Això no hauria de causar cap problema, però haureu de parar atenció ja que us podrieu quedar sense espai ràpidament… Es recomanar tenir un mínim de 16 GB per al sistema de fitxers arrel." } From 351928f566d06ca4aa9add30563a8cfe65bbf8ec Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 10 Feb 2021 19:25:35 +0000 Subject: [PATCH 263/363] Translated using Weblate (German) Currently translated at 72.1% (456 of 632 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 48de3df5f..40d7ec5e7 100644 --- a/locales/de.json +++ b/locales/de.json @@ -172,7 +172,7 @@ "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft nicht 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. (Wenn du weißt was du tust, nutze \"--no-checks\" um die überprüfung zu überspringen.)", "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. Für weitere Informationen überprüfe bitte die 'DNS records' (basic) Kategorie in der Diagnose. 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_dns_ip_differs_from_public_ip": "Der DNS-A-Eintrag der Domain {domain:s} unterscheidet sich von dieser Server-IP. Für weitere Informationen überprüfen Sie bitte die 'DNS records' (basic) Kategorie in der Diagnose. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS-Propagation mittels Website überprüfen) (Wenn Sie wissen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen.)", "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 Zertifikat installiert.", @@ -501,5 +501,29 @@ "log_app_config_apply": "Wende die Konfiguration auf die Applikation '{}' an", "log_app_config_show_panel": "Zeige das Konfigurations-Panel der Applikation '{}'", "log_app_action_run": "Führe Aktion der Applikation '{}' aus", - "invalid_regex": "Ungültige Regex:'{regex:s}'" + "invalid_regex": "Ungültige Regex:'{regex:s}'", + "migration_description_0016_php70_to_php73_pools": "Migrieren der php7.0-fpm-Konfigurationsdateien zu php7.3", + "mailbox_disabled": "E-Mail für Benutzer {user:s} deaktiviert", + "log_tools_reboot": "Server neustarten", + "log_tools_shutdown": "Server ausschalten", + "log_tools_upgrade": "Systempakete aktualisieren", + "log_tools_postinstall": "Post-Installation des YunoHost-Servers durchführen", + "log_tools_migrations_migrate_forward": "Migrationen durchführen", + "log_domain_main_domain": "Mache '{}' zur Hauptdomäne", + "log_user_permission_reset": "Zurücksetzen der Berechtigung '{}'", + "log_user_permission_update": "Aktualisiere Zugriffe für Berechtigung '{}'", + "log_user_update": "Aktualisiere Information für Benutzer '{}'", + "log_user_group_update": "Aktualisiere Gruppe '{}'", + "log_user_group_delete": "Lösche Gruppe '{}'", + "log_user_group_create": "Erstelle Gruppe '{}'", + "log_user_delete": "Lösche Benutzer '{}'", + "log_user_create": "Füge Benutzer '{}' hinzu", + "log_permission_url": "Aktualisiere URL, die mit der Berechtigung '{}' verknüpft ist", + "log_permission_delete": "Lösche Berechtigung '{}'", + "log_permission_create": "Erstelle Berechtigung '{}'", + "log_dyndns_update": "Die IP, die mit der YunoHost-Subdomain '{}' verbunden ist, aktualisieren", + "log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'", + "log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration", + "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", + "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation" } From 5e6f77c8f724ae8b0fdfe2db7f2d165868ad04da Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 15 Feb 2021 06:31:00 +0000 Subject: [PATCH 264/363] Translated using Weblate (German) Currently translated at 73.4% (465 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 40d7ec5e7..2699ebe74 100644 --- a/locales/de.json +++ b/locales/de.json @@ -525,5 +525,14 @@ "log_dyndns_subscribe": "Für eine YunoHost-Subdomain registrieren '{}'", "log_domain_remove": "Entfernen der Domäne '{}' aus der Systemkonfiguration", "log_domain_add": "Hinzufügen der Domäne '{}' zur Systemkonfiguration", - "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation" + "log_remove_on_failed_install": "Entfernen von '{}' nach einer fehlgeschlagenen Installation", + "migration_0015_still_on_stretch_after_main_upgrade": "Etwas ist schiefgelaufen während dem Haupt-Upgrade. Das System scheint immer noch auf Debian Stretch zu laufen", + "migration_0015_yunohost_upgrade": "Beginne YunoHost-Core-Upgrade...", + "migration_description_0019_extend_permissions_features": "Erweitern und überarbeiten des Applikationsberechtigungs-Managementsystems", + "migrating_legacy_permission_settings": "Migrieren der Legacy-Berechtigungseinstellungen...", + "migration_description_0017_postgresql_9p6_to_11": "Migrieren der Datenbanken von PostgreSQL 9.6 nach 11", + "migration_0015_main_upgrade": "Beginne Haupt-Upgrade...", + "migration_0015_not_stretch": "Die aktuelle Debian-Distribution ist nicht Stretch!", + "migration_0015_not_enough_free_space": "Der freie Speicher in /var/ ist sehr gering! Sie sollten minimal 1GB frei haben, um diese Migration durchzuführen.", + "domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]" } From b050a35b8a27750afcb2f45cb6812ead44965ffc Mon Sep 17 00:00:00 2001 From: MrMorals Date: Fri, 19 Feb 2021 16:46:19 +0000 Subject: [PATCH 265/363] Translated using Weblate (Dutch) Currently translated at 8.3% (53 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/nl/ --- locales/nl.json | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/locales/nl.json b/locales/nl.json index dfee556b2..63ec7bd6d 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -3,15 +3,15 @@ "admin_password": "Administrator wachtwoord", "admin_password_changed": "Het administratie wachtwoord werd gewijzigd", "app_already_installed": "{app:s} is al geïnstalleerd", - "app_argument_invalid": "'{name:s}' bevat ongeldige waarde: {error:s}", + "app_argument_invalid": "Kies een geldige waarde voor '{name:s}': {error:s}", "app_argument_required": "Het '{name:s}' moet ingevuld worden", "app_extraction_failed": "Kan installatiebestanden niet uitpakken", "app_id_invalid": "Ongeldige app-id", - "app_install_files_invalid": "Ongeldige installatiebestanden", + "app_install_files_invalid": "Deze bestanden kunnen niet worden geïnstalleerd", "app_manifest_invalid": "Ongeldig app-manifest", "app_not_installed": "{app:s} is niet geïnstalleerd", "app_removed": "{app:s} succesvol verwijderd", - "app_sources_fetch_failed": "Kan bronbestanden niet ophalen", + "app_sources_fetch_failed": "Kan bronbestanden niet ophalen, klopt de URL?", "app_unknown": "Onbekende app", "app_upgrade_failed": "Kan app {app:s} niet updaten", "app_upgraded": "{app:s} succesvol geüpgraded", @@ -82,7 +82,7 @@ "app_argument_choice_invalid": "Ongeldige keuze voor argument '{name:s}'. Het moet een van de volgende keuzes zijn {choices:s}", "app_not_correctly_installed": "{app:s} schijnt niet juist geïnstalleerd te zijn", "app_not_properly_removed": "{app:s} werd niet volledig verwijderd", - "app_requirements_checking": "Controleer noodzakelijke pakketten...", + "app_requirements_checking": "Noodzakelijke pakketten voor {app} aan het controleren...", "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", "ask_main_domain": "Hoofd-domein", @@ -101,5 +101,25 @@ "already_up_to_date": "Er is niets te doen, alles is al up-to-date.", "admin_password_too_long": "Gelieve een wachtwoord te kiezen met minder dan 127 karakters", "app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).", - "aborting": "Annulatie." -} \ No newline at end of file + "aborting": "Annulatie.", + "app_upgrade_app_name": "Bezig {app} te upgraden...", + "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domein}' wordt al gebruikt door '{other_app}'", + "app_install_failed": "Kan {app} niet installeren: {error}", + "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...", + "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden", + "app_manifest_install_ask_path": "Kies het pad waar deze app geïnstalleerd moet worden", + "app_manifest_install_ask_admin": "Kies een administrator voor deze app", + "app_change_url_failed_nginx_reload": "Kon NGINX niet opnieuw laden. Hier is de output van 'nginx -t':\n{nginx_errors:s}", + "app_change_url_success": "{app:s} URL is nu {domain:s}{path:s}", + "app_full_domain_unavailable": "Sorry, deze app moet op haar eigen domein geïnstalleerd worden, maar andere apps zijn al geïnstalleerd op het domein '{domain}'. U kunt wel een subdomein aan deze app toewijden.", + "app_install_script_failed": "Er is een fout opgetreden in het installatiescript van de app", + "app_location_unavailable": "Deze URL is niet beschikbaar of is in conflict met de al geïnstalleerde app(s):\n{apps:s}", + "app_manifest_install_ask_password": "Kies een administratiewachtwoord voor deze app", + "app_manifest_install_ask_is_public": "Moet deze app zichtbaar zijn voor anomieme bezoekers?", + "app_not_upgraded": "De app '{failed_app}' kon niet upgraden en daardoor zijn de upgrades van de volgende apps geannuleerd: {apps}", + "app_start_install": "{app} installeren...", + "app_start_remove": "{app} verwijderen...", + "app_start_backup": "Bestanden aan het verzamelen voor de backup van {app}...", + "app_start_restore": "{app} herstellen...", + "app_upgrade_several_apps": "De volgende apps zullen worden geüpgraded: {apps}" +} From dc844b5d73167ea923960e6594a08d487039e75b Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 22 Feb 2021 06:45:23 +0000 Subject: [PATCH 266/363] Translated using Weblate (German) Currently translated at 74.7% (473 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 2699ebe74..36f710f07 100644 --- a/locales/de.json +++ b/locales/de.json @@ -534,5 +534,13 @@ "migration_0015_main_upgrade": "Beginne Haupt-Upgrade...", "migration_0015_not_stretch": "Die aktuelle Debian-Distribution ist nicht Stretch!", "migration_0015_not_enough_free_space": "Der freie Speicher in /var/ ist sehr gering! Sie sollten minimal 1GB frei haben, um diese Migration durchzuführen.", - "domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]" + "domain_remove_confirm_apps_removal": "Wenn Sie diese Domäne löschen, werden folgende Applikationen entfernt:\n{apps}\n\nSind Sie sicher? [{answers}]", + "migration_0015_cleaning_up": "Bereinigung des Cache und der Pakete, welche nicht mehr benötigt werden...", + "migration_0017_postgresql_96_not_installed": "PostgreSQL wurde auf ihrem System nicht installiert. Nichts zu tun.", + "migration_0015_system_not_fully_up_to_date": "Ihr System ist nicht vollständig auf dem neuesten Stand. Bitte führen Sie ein reguläres Upgrade durch, bevor Sie die Migration auf Buster durchführen.", + "migration_0015_modified_files": "Bitte beachten Sie, dass die folgenden Dateien als manuell bearbeitet erkannt wurden und beim nächsten Upgrade überschrieben werden könnten: {manually_modified_files}", + "migration_0015_general_warning": "Bitte beachten Sie, dass diese Migration eine heikle Angelegenheit darstellt. Das YunoHost-Team hat alles unternommen, um sie zu testen und zu überarbeiten. Dennoch ist es möglich, dass diese Migration Teile des Systems oder Applikationen beschädigen könnte.\n\nDeshalb ist folgendes zu empfehlen:\n…- Führen Sie ein Backup aller kritischen Daten und Applikationen durch. Mehr unter https://yunohost.org/backup;\n…- Seien Sie geduldig nachdem Sie die Migration gestartet haben: Abhängig von Ihrer Internetverbindung und Ihrer Hardware kann es einige Stunden dauern, bis das Upgrade fertig ist.", + "migration_0015_problematic_apps_warning": "Bitte beachten Sie, dass folgende möglicherweise problematischen Applikationen auf Ihrer Installation erkannt wurden. Es scheint, als ob sie nicht aus dem YunoHost-Applikationskatalog installiert oder nicht als 'working' gekennzeichnet worden sind. Folglich kann nicht garantiert werden, dass sie nach dem Upgrade immer noch funktionieren: {problematic_apps}", + "migration_0015_specific_upgrade": "Start des Upgrades der Systempakete, deren Upgrade separat durchgeführt werden muss...", + "migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}" } From 2ea7b026533055f94848787fc84b950b1ba6d740 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Mon, 22 Feb 2021 19:23:24 +0000 Subject: [PATCH 267/363] Translated using Weblate (German) Currently translated at 78.9% (500 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 36f710f07..77b4fad49 100644 --- a/locales/de.json +++ b/locales/de.json @@ -542,5 +542,32 @@ "migration_0015_general_warning": "Bitte beachten Sie, dass diese Migration eine heikle Angelegenheit darstellt. Das YunoHost-Team hat alles unternommen, um sie zu testen und zu überarbeiten. Dennoch ist es möglich, dass diese Migration Teile des Systems oder Applikationen beschädigen könnte.\n\nDeshalb ist folgendes zu empfehlen:\n…- Führen Sie ein Backup aller kritischen Daten und Applikationen durch. Mehr unter https://yunohost.org/backup;\n…- Seien Sie geduldig nachdem Sie die Migration gestartet haben: Abhängig von Ihrer Internetverbindung und Ihrer Hardware kann es einige Stunden dauern, bis das Upgrade fertig ist.", "migration_0015_problematic_apps_warning": "Bitte beachten Sie, dass folgende möglicherweise problematischen Applikationen auf Ihrer Installation erkannt wurden. Es scheint, als ob sie nicht aus dem YunoHost-Applikationskatalog installiert oder nicht als 'working' gekennzeichnet worden sind. Folglich kann nicht garantiert werden, dass sie nach dem Upgrade immer noch funktionieren: {problematic_apps}", "migration_0015_specific_upgrade": "Start des Upgrades der Systempakete, deren Upgrade separat durchgeführt werden muss...", - "migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}" + "migration_0015_weak_certs": "Die folgenden Zertifikate verwenden immer noch schwache Signierungsalgorithmen und müssen aktualisiert werden um mit der nächsten Version von nginx kompatibel zu sein: {certs}", + "migrations_pending_cant_rerun": "Diese Migrationen sind immer noch anstehend und können deshalb nicht erneut durchgeführt werden: {ids}", + "migration_0019_add_new_attributes_in_ldap": "Hinzufügen neuer Attribute für die Berechtigungen in der LDAP-Datenbank", + "migration_0019_can_not_backup_before_migration": "Das Backup des Systems konnte nicht abgeschlossen werden bevor die Migration fehlschlug. Fehlermeldung: {error}", + "migration_0019_migration_failed_trying_to_rollback": "Konnte nicht migrieren... versuche ein Rollback des Systems.", + "migrations_not_pending_cant_skip": "Diese Migrationen sind nicht anstehend und können deshalb nicht übersprungen werden: {ids}", + "migration_0018_failed_to_reset_legacy_rules": "Zurücksetzen der veralteten iptables-Regeln fehlgeschlagen: {error}", + "migration_0019_rollback_success": "Rollback des Systems durchgeführt.", + "migration_0019_slapd_config_will_be_overwritten": "Es schaut aus, als ob Sie die slapd-Konfigurationsdatei manuell bearbeitet haben. Für diese kritische Migration muss das Update der slapd-Konfiguration erzwungen werden. Von der Originaldatei wird ein Backup gemacht in {conf_backup_folder}.", + "migrations_success_forward": "Migration {id} abgeschlossen", + "migrations_cant_reach_migration_file": "Die Migrationsdateien konnten nicht aufgerufen werden im Verzeichnis '%s'", + "migrations_dependencies_not_satisfied": "Führen Sie diese Migrationen aus: '{dependencies_id}', vor der Migration {id}.", + "migrations_failed_to_load_migration": "Konnte Migration nicht laden {id}: {error}", + "migrations_list_conflict_pending_done": "Sie können nicht '--previous' und '--done' gleichzeitig benützen.", + "migrations_already_ran": "Diese Migrationen wurden bereits durchgeführt: {ids}", + "migrations_loading_migration": "Lade Migrationen {id}...", + "migrations_migration_has_failed": "Migration {id} gescheitert mit der Ausnahme {exception}: Abbruch", + "migrations_must_provide_explicit_targets": "Sie müssen konkrete Ziele angeben, wenn Sie '--skip' oder '--force-rerun' verwenden", + "migrations_need_to_accept_disclaimer": "Um die Migration {id} durchzuführen, müssen Sie den Disclaimer akzeptieren.\n---\n{disclaimer}\n---\n Wenn Sie bestätigen, dass Sie die Migration durchführen wollen, wiederholen Sie bitte den Befehl mit der Option '--accept-disclaimer'.", + "migrations_no_migrations_to_run": "Keine Migrationen durchzuführen", + "migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 ist installiert aber nicht postgreSQL 11? Etwas komisches ist Ihrem System zugestossen :(...", + "migration_0017_not_enough_space": "Stellen Siea ausreichend Speicherplatz im Verzeichnis {path} zur Verfügung um die Migration durchzuführen.", + "migration_0018_failed_to_migrate_iptables_rules": "Migration der veralteten iptables-Regeln zu nftables fehlgeschlagen: {error}", + "migration_0019_backup_before_migration": "Ein Backup der LDAP-Datenbank und der Applikationseinstellungen erstellen vor der Migration.", + "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.", + "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'", + "migrations_running_forward": "Durchführen der Migrationen {id}...", + "migrations_skip_migration": "Überspringe Migrationen {id}..." } From a0cd4d3ca50b2fdcbeda5b5884d78d547434395f Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 24 Feb 2021 06:52:40 +0000 Subject: [PATCH 268/363] Translated using Weblate (German) Currently translated at 79.7% (505 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 77b4fad49..536bdd842 100644 --- a/locales/de.json +++ b/locales/de.json @@ -569,5 +569,10 @@ "migrations_exclusive_options": "'--auto', '--skip' und '--force-rerun' sind Optionen, die sich gegenseitig ausschliessen.", "migrations_no_such_migration": "Es existiert keine Migration genannt '{id}'", "migrations_running_forward": "Durchführen der Migrationen {id}...", - "migrations_skip_migration": "Überspringe Migrationen {id}..." + "migrations_skip_migration": "Überspringe Migrationen {id}...", + "password_too_simple_2": "Dieses Passwort gehört zu den meistverwendeten der Welt. Bitte nehmen Sie etwas einzigartigeres.", + "password_listed": "Dieses Passwort gehört zu den meistverwendeten der Welt. Bitte nehmen Sie etwas einzigartigeres.", + "operation_interrupted": "Wurde die Operation manuell unterbrochen?", + "invalid_number": "Muss eine Zahl sein", + "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus." } From a5038b5cc0a4cedffa0bc39ad001b75ced11bd6a Mon Sep 17 00:00:00 2001 From: Nils Van Zuijlen Date: Tue, 23 Feb 2021 14:20:24 +0000 Subject: [PATCH 269/363] Translated using Weblate (French) Currently translated at 99.5% (630 of 633 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/fr/ --- locales/fr.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 7d016a70e..47bae6407 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -692,5 +692,9 @@ "invalid_number": "Doit être un nombre", "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", "diagnosis_basesystem_hardware_model": "Le modèle du serveur est {model}", - "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système." + "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", + "postinstall_low_rootfsspace": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement les remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez le postinstall avec --force-diskspace", + "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]", + "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement les remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", + "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Ça peut suffire, mais faites attention car vous risquez de les remplire rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers." } From fdba94e2e5e0d18686f86efb1d8249accb995042 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 05:17:36 +0100 Subject: [PATCH 270/363] Fix fr translation --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 47bae6407..8982d7ccc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -693,7 +693,7 @@ "migration_description_0019_extend_permissions_features": "Étendre et retravailler le système de gestion des permissions applicatives", "diagnosis_basesystem_hardware_model": "Le modèle du serveur est {model}", "diagnosis_backports_in_sources_list": "Il semble qu'apt (le gestionnaire de paquets) soit configuré pour utiliser le dépôt des rétroportages (backports). A moins que vous ne sachiez vraiment ce que vous faites, nous vous déconseillons fortement d'installer des paquets provenant des rétroportages, car cela risque de créer des instabilités ou des conflits sur votre système.", - "postinstall_low_rootfsspace": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement les remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez le postinstall avec --force-diskspace", + "postinstall_low_rootfsspace": "Le système de fichiers racine a une taille totale inférieure à 10 GB, ce qui est inquiétant ! Vous allez certainement arriver à court d'espace disque rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers. Si vous voulez installer YunoHost malgré cet avertissement, relancez le postinstall avec --force-diskspace", "domain_remove_confirm_apps_removal": "Le retrait de ce domaine retirera aussi ces applications :\n{apps}\n\nÊtes vous sûr de vouloir cela ? [{answers}]", "diagnosis_rootfstotalspace_critical": "Le système de fichiers racine ne fait que {space} ! Vous allez certainement les remplir très rapidement ! Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers.", "diagnosis_rootfstotalspace_warning": "Le système de fichiers racine n'est que de {space}. Ça peut suffire, mais faites attention car vous risquez de les remplire rapidement... Il est recommandé d'avoir au moins 16 GB pour ce système de fichiers." From a8b315b873e745dc8a5d039b92456f499e9513d8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 05:18:21 +0100 Subject: [PATCH 271/363] Fix nl translation --- locales/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/nl.json b/locales/nl.json index 63ec7bd6d..59de95f58 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -103,7 +103,7 @@ "app_action_cannot_be_ran_because_required_services_down": "De volgende diensten moeten actief zijn om deze actie uit te voeren: {services}. Probeer om deze te herstarten om verder te gaan (en om eventueel te onderzoeken waarom ze niet werken).", "aborting": "Annulatie.", "app_upgrade_app_name": "Bezig {app} te upgraden...", - "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domein}' wordt al gebruikt door '{other_app}'", + "app_make_default_location_already_used": "Kan '{app}' niet de standaardapp maken op het domein, '{domain}' wordt al gebruikt door '{other_app}'", "app_install_failed": "Kan {app} niet installeren: {error}", "app_remove_after_failed_install": "Bezig de app te verwijderen na gefaalde installatie...", "app_manifest_install_ask_domain": "Kies het domein waar deze app op geïnstalleerd moet worden", From 8f7ced35a5ee7f1493dcbbbcb42256b8b36a78df Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 25 Feb 2021 11:16:34 +0100 Subject: [PATCH 272/363] fix ynh_systemd_action in case of service fails --- data/helpers.d/systemd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index b416e5745..f8e21bfbd 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -58,7 +58,7 @@ ynh_remove_systemd_config () { # usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ] # | arg: -n, --service_name= - Name of the service to start. Default : $app # | arg: -a, --action= - Action to perform with systemctl. Default: start -# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. WARNING: When using --line_match, you should always add `ynh_clean_check_starting` into your `ynh_clean_setup` at the beginning of the script. Otherwise, tail will not stop in case of failure of the script. The script will then hang forever. +# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. # | arg: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log # | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. # | arg: -e, --length= - Length of the error log : Default : 20 @@ -117,6 +117,7 @@ ynh_systemd_action() { then ynh_exec_err tail --lines=$length "$log_path" fi + ynh_clean_check_starting return 1 fi @@ -161,9 +162,8 @@ ynh_systemd_action() { } # Clean temporary process and file used by ynh_check_starting -# (usually used in ynh_clean_setup scripts) # -# usage: ynh_clean_check_starting +# [internal] # # Requires YunoHost version 3.5.0 or higher. ynh_clean_check_starting () { @@ -174,7 +174,7 @@ ynh_clean_check_starting () { fi if [ -n "$templog" ] then - ynh_secure_remove "$templog" 2>&1 + ynh_secure_remove --file="$templog" 2>&1 fi } From 1d3380415eca14ea9291d3b29fbf326111077e68 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 25 Feb 2021 11:17:46 +0100 Subject: [PATCH 273/363] add missing getops --- data/helpers.d/apt | 6 +++--- data/helpers.d/fail2ban | 8 ++++---- data/helpers.d/network | 2 +- data/helpers.d/php | 6 +++--- data/helpers.d/postgresql | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index 6abaf20a2..bfdeffe7b 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -459,11 +459,11 @@ ynh_remove_extra_repo () { ynh_handle_getopts_args "$@" name="${name:-$app}" - ynh_secure_remove "/etc/apt/sources.list.d/$name.list" + ynh_secure_remove --file="/etc/apt/sources.list.d/$name.list" # Sury pinning is managed by the regenconf in the core... [[ "$name" == "extra_php_version" ]] || ynh_secure_remove "/etc/apt/preferences.d/$name" - ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null - ynh_secure_remove "/etc/apt/trusted.gpg.d/$name.asc" > /dev/null + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.gpg" > /dev/null + ynh_secure_remove --file="/etc/apt/trusted.gpg.d/$name.asc" > /dev/null # Update the list of package to exclude the old repo ynh_package_update diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index c41226e14..c9322d067 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -77,8 +77,8 @@ ynh_add_fail2ban_config () { if [ $use_template -ne 1 ] then # Usage 1, no template. Build a config file from scratch. - test -n "$logpath" || ynh_die "ynh_add_fail2ban_config expects a logfile path as first argument and received nothing." - test -n "$failregex" || ynh_die "ynh_add_fail2ban_config expects a failure regex as second argument and received nothing." + test -n "$logpath" || ynh_die --message="ynh_add_fail2ban_config expects a logfile path as first argument and received nothing." + test -n "$failregex" || ynh_die --message="ynh_add_fail2ban_config expects a failure regex as second argument and received nothing." echo " [__APP__] @@ -117,7 +117,7 @@ ignoreregex = # # Requires YunoHost version 3.5.0 or higher. ynh_remove_fail2ban_config () { - ynh_secure_remove "/etc/fail2ban/jail.d/$app.conf" - ynh_secure_remove "/etc/fail2ban/filter.d/$app.conf" + ynh_secure_remove --file="/etc/fail2ban/jail.d/$app.conf" + ynh_secure_remove --file="/etc/fail2ban/filter.d/$app.conf" ynh_systemd_action --service_name=fail2ban --action=reload } diff --git a/data/helpers.d/network b/data/helpers.d/network index 4f108422b..0760909c6 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -27,7 +27,7 @@ ynh_find_port () { # Test if a port is available # -# example: ynh_port_available --port=1234 || ynh_die "Port 1234 is needs to be available for this app" +# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app" # # usage: ynh_find_port --port=XYZ # | arg: -p, --port= - port to check diff --git a/data/helpers.d/php b/data/helpers.d/php index 683f252be..5c050cc88 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -337,7 +337,7 @@ ynh_install_php () { if [ "$phpversion" == "$YNH_DEFAULT_PHP_VERSION" ] then - ynh_die "Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" + ynh_die --message="Do not use ynh_install_php to install php$YNH_DEFAULT_PHP_VERSION" fi # Create the file if doesn't exist already @@ -611,9 +611,9 @@ ynh_install_composer () { curl -sS https://getcomposer.org/installer \ | COMPOSER_HOME="$workdir/.composer" \ php${phpversion} -- --quiet --install-dir="$workdir" --version=$composerversion \ - || ynh_die "Unable to install Composer." + || ynh_die --message="Unable to install Composer." # install dependencies ynh_composer_exec --phpversion="${phpversion}" --workdir="$workdir" --commands="install --no-dev $install_args" \ - || ynh_die "Unable to install core dependencies with Composer." + || ynh_die --message="Unable to install core dependencies with Composer." } diff --git a/data/helpers.d/postgresql b/data/helpers.d/postgresql index 11b9c0fed..f2f427842 100644 --- a/data/helpers.d/postgresql +++ b/data/helpers.d/postgresql @@ -295,10 +295,10 @@ ynh_psql_remove_db() { ynh_psql_test_if_first_run() { # Make sure postgresql is indeed installed - dpkg --list | grep -q "ii postgresql-$PSQL_VERSION" || ynh_die "postgresql-$PSQL_VERSION is not installed !?" + dpkg --list | grep -q "ii postgresql-$PSQL_VERSION" || ynh_die --message="postgresql-$PSQL_VERSION is not installed !?" # Check for some weird issue where postgresql could be installed but etc folder would not exist ... - [ -e "/etc/postgresql/$PSQL_VERSION" ] || ynh_die "It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist" + [ -e "/etc/postgresql/$PSQL_VERSION" ] || ynh_die --message="It looks like postgresql was not properly configured ? /etc/postgresql/$PSQL_VERSION is missing ... Could be due to a locale issue, c.f.https://serverfault.com/questions/426989/postgresql-etc-postgresql-doesnt-exist" # Make sure postgresql is started and enabled # (N.B. : to check the active state, we check the cluster state because From 47420c623252c98f2efc65498abde32ba90a3dec Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 25 Feb 2021 12:40:49 +0100 Subject: [PATCH 274/363] fix multimedia helper --- data/helpers.d/multimedia | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/multimedia b/data/helpers.d/multimedia index 5517917b5..f7c9d5974 100644 --- a/data/helpers.d/multimedia +++ b/data/helpers.d/multimedia @@ -1,3 +1,5 @@ +#!/bin/bash + readonly MEDIA_GROUP=multimedia readonly MEDIA_DIRECTORY=/home/yunohost.multimedia @@ -53,10 +55,13 @@ ynh_multimedia_build_main_dir() { # | arg: -d, --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia" ynh_multimedia_addfolder() { - # Declare an array to define the options of this helper. - declare -Ar args_array=( [s]=source_dir= [d]=dest_dir= ) + # Declare an array to define the options of this helper. + local legacy_args=sd + local -A args_array=( [s]=source_dir= [d]=dest_dir= ) local source_dir local dest_dir + # Manage arguments with getopts + ynh_handle_getopts_args "$@" # Ajout d'un lien symbolique vers le dossier à partager ln -sfn "$source_dir" "$MEDIA_DIRECTORY/$dest_dir" @@ -77,6 +82,7 @@ ynh_multimedia_addfolder() { # | arg: -u, --user_name= - The name of the user which gain this access. ynh_multimedia_addaccess () { # Declare an array to define the options of this helper. + local legacy_args=u declare -Ar args_array=( [u]=user_name=) local user_name # Manage arguments with getopts From 33f291be962333b2a455ac6e4a7162cf5aee8a8b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:36:28 +0100 Subject: [PATCH 275/363] Fix ynh_replace_vars again, mystical bash is mystic... --- data/helpers.d/utils | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index bd9e6a87b..96609c7dc 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -389,8 +389,11 @@ ynh_replace_vars () { for one_var in "${uniques_vars[@]}" do # Validate that one_var is indeed defined - # Explanation for the weird '+x' syntax: https://stackoverflow.com/a/13864829 - test -n "${one_var+x}" || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" + # -v checks if the variable is defined, for example: + # -v FOO tests if $FOO is defined + # -v $FOO tests if ${!FOO} is defined + # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 + [[ -v "${one_var:-}" ]] || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" # Escape delimiter in match/replace string match_string="__${one_var^^}__" From 0dd033745089316ad6d03be1985942ec76bb81bf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:44:55 +0100 Subject: [PATCH 276/363] Don't redact empty string... --- src/yunohost/log.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 24ecc6713..763967b38 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -399,7 +399,11 @@ class RedactingFormatter(Formatter): msg = super(RedactingFormatter, self).format(record) self.identify_data_to_redact(msg) for data in self.data_to_redact: - msg = msg.replace(data, "**********") + # we check that data is not empty string, + # otherwise this may lead to super epic stuff + # (try to run "foo".replace("", "bar")) + if data: + msg = msg.replace(data, "**********") return msg def identify_data_to_redact(self, record): From 675c4d0eeaea772703b951c78e12bbefae855913 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:57:57 +0100 Subject: [PATCH 277/363] Fix permission helper doc format --- data/helpers.d/permission | 47 +++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/data/helpers.d/permission b/data/helpers.d/permission index f4e200019..ba2b22a53 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -25,19 +25,14 @@ # usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false] # [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false] # [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Not that if 'show_tile' is enabled, this URL will be the URL of the tile. -# | arg: -A, additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true -# | arg: -a, allowed= - (optional) A list of group/user to allow for the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | Default is "APP_LABEL (permission name)". -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. -# | Default is false (for the permission different than 'main'). -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. -# | By default it's 'false' +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if 'show_tile' is enabled, this URL will be the URL of the tile. +# | arg: -A, --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true +# | arg: -a, --allowed= - (optional) A list of group/user to allow for the permission +# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "APP_LABEL (permission name)". +# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. Defaults to false for the permission different than 'main'. +# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'. # # If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they # start with '/'. For example: @@ -193,13 +188,12 @@ ynh_permission_exists() { # # usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] # [--auth_header=true|false] [--clear_urls] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Note that if you want to remove url you can pass an empty sting as arguments (""). -# | arg: -a, add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. -# | arg: -r, remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application -# | arg: -c, clear_urls - (optional) Clean all urls (url and additional_urls) +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments (""). +# | arg: -a, --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. +# | arg: -r, --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application +# | arg: -c, --clear_urls - (optional) Clean all urls (url and additional_urls) # # Requires YunoHost version 3.7.0 or higher. ynh_permission_url() { @@ -269,13 +263,12 @@ ynh_permission_url() { # # usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] # [--label="label"] [--show_tile=true|false] [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -a, add= - the list of group or users to enable add to the permission -# | arg: -r, remove= - the list of group or users to remove from the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -a, --add= - the list of group or users to enable add to the permission +# | arg: -r, --remove= - the list of group or users to remove from the permission +# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. +# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO +# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. # # Requires YunoHost version 3.7.0 or higher. ynh_permission_update() { From 7efc6dcd07d08b8c3923165d1378d8d7490c1754 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 02:56:09 +0100 Subject: [PATCH 278/363] Handle the dyndns cron from the regenconf --- data/hooks/conf_regen/01-yunohost | 11 +++++++++++ src/yunohost/dyndns.py | 26 +++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index 9da2d91ca..8b7a7c6fc 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -86,6 +86,17 @@ SHELL=/bin/bash 0 7,19 * * * root : YunoHost Automatic Diagnosis; sleep \$((RANDOM\\%1200)); yunohost diagnosis run --email > /dev/null 2>/dev/null || echo "Running the automatic diagnosis failed miserably" EOF + # If we subscribed to a dyndns domain, add the corresponding cron + # - delay between 0 and 60 secs to spread the check over a 1 min window + # - do not run the command if some process already has the lock, to avoid queuing hundreds of commands... + if ls -l /etc/yunohost/dyndns/K*.private 2>/dev/null + then + cat > $pending_dir/etc/cron.d/yunohost-dyndns << EOF +SHELL=/bin/bash +*/10 * * * * root : YunoHost DynDNS update; sleep \$((RANDOM\\%60)); test -e /var/run/moulinette_yunohost.lock || yunohost dyndns update >> /dev/null +EOF + fi + # legacy stuff to avoid yunohost reporting etckeeper as manually modified # (this make sure that the hash is null / file is flagged as to-delete) mkdir -p $pending_dir/etc/etckeeper diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d94748881..ce5ebfc5e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -40,6 +40,7 @@ from yunohost.utils.error import YunohostError from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip, dig from yunohost.log import is_unit_operation +from yunohost.regenconf import regen_conf logger = getActionLogger("yunohost.dyndns") @@ -121,10 +122,9 @@ def dyndns_subscribe( subscribe_host -- Dynette HTTP API to subscribe to """ - if len(glob.glob("/etc/yunohost/dyndns/*.key")) != 0 or os.path.exists( - "/etc/cron.d/yunohost-dyndns" - ): - raise YunohostError("domain_dyndns_already_subscribed") + + if _guess_current_dyndns_domain(dyn_host) != (None, None): + raise YunohostError('domain_dyndns_already_subscribed') if domain is None: domain = _get_maindomain() @@ -186,9 +186,17 @@ def dyndns_subscribe( error = 'Server error, code: %s. (Message: "%s")' % (r.status_code, r.text) raise YunohostError("dyndns_registration_failed", error=error) - logger.success(m18n.n("dyndns_registered")) + # Yunohost regen conf will add the dyndns cron job if a private key exists + # in /etc/yunohost/dyndns + regen_conf("yunohost") - dyndns_installcron() + # Add some dyndns update in 2 and 4 minutes from now such that user should + # not have to wait 10ish minutes for the conf to propagate + cmd = "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" + subprocess.check_call(cmd.format(t="2 min"), shell=True) + subprocess.check_call(cmd.format(t="4 min"), shell=True) + + logger.success(m18n.n('dyndns_registered')) @is_unit_operation() @@ -220,6 +228,10 @@ def dyndns_update( # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) + + if domain is None: + raise YunohostError('dyndns_no_domain_registered') + # If key is not given, pick the first file we find with the domain given else: if key is None: @@ -414,4 +426,4 @@ def _guess_current_dyndns_domain(dyn_host): else: return (_domain, path) - raise YunohostError("dyndns_no_domain_registered") + return (None, None) From 2752a8e485dfcffed761f93309829ba31cd74188 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 02:56:36 +0100 Subject: [PATCH 279/363] Deprecate yunohost dyndns installcron/removecron --- data/actionsmap/yunohost.yml | 6 ++---- locales/en.json | 3 --- src/yunohost/dyndns.py | 22 ++-------------------- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 33b8b5cfe..290952aa3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1363,13 +1363,11 @@ dyndns: ### dyndns_installcron() installcron: - action_help: Install IP update cron - api: POST /dyndns/cron + deprecated: true ### dyndns_removecron() removecron: - action_help: Remove IP update cron - api: DELETE /dyndns/cron + deprecated: true ############################# diff --git a/locales/en.json b/locales/en.json index 7e3de2341..199c21b66 100644 --- a/locales/en.json +++ b/locales/en.json @@ -291,9 +291,6 @@ "dpkg_lock_not_available": "This command can't be run right now because another program seems to be using the lock of dpkg (the system package manager)", "dyndns_could_not_check_provide": "Could not check if {provider:s} can provide {domain:s}.", "dyndns_could_not_check_available": "Could not check if {domain:s} is available on {provider:s}.", - "dyndns_cron_installed": "DynDNS cron job created", - "dyndns_cron_remove_failed": "Could not remove the DynDNS cron job because: {error}", - "dyndns_cron_removed": "DynDNS cron job removed", "dyndns_ip_update_failed": "Could not update IP address to DynDNS", "dyndns_ip_updated": "Updated your IP on DynDNS", "dyndns_key_generating": "Generating DNS key... It may take a while.", diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index ce5ebfc5e..6545d33c7 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -373,29 +373,11 @@ def dyndns_update( def dyndns_installcron(): - """ - Install IP update cron - - - """ - with open("/etc/cron.d/yunohost-dyndns", "w+") as f: - f.write("*/2 * * * * root yunohost dyndns update >> /dev/null\n") - - logger.success(m18n.n("dyndns_cron_installed")) + logger.warning("This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'.") def dyndns_removecron(): - """ - Remove IP update cron - - - """ - try: - os.remove("/etc/cron.d/yunohost-dyndns") - except Exception as e: - raise YunohostError("dyndns_cron_remove_failed", error=e) - - logger.success(m18n.n("dyndns_cron_removed")) + logger.warning("This command is deprecated. The dyndns cron job should automatically be added/removed by the regenconf depending if there's a private key in /etc/yunohost/dyndns. You can run the regenconf yourself with 'yunohost tools regen-conf yunohost'.") def _guess_current_dyndns_domain(dyn_host): From a8e11c19db6d8b21f5d6d90512cc4d5471ce3bd6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:01:05 +0100 Subject: [PATCH 280/363] Delete dyndns key during domain removal if any --- src/yunohost/domain.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index cc9980549..95d9ee7b0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -245,6 +245,9 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): os.system("rm -rf /etc/yunohost/certs/%s" % domain) + # Delete dyndns keys for this domain (if any) + os.system('rm -rf /etc/yunohost/dyndns/K%s.+*' % domain) + # Sometime we have weird issues with the regenconf where some files # appears as manually modified even though they weren't touched ... # There are a few ideas why this happens (like backup/restore nginx From cc6cc1860add2c44d9af2a47329acef2c3a48dd7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:02:04 +0100 Subject: [PATCH 281/363] Do not backup damn cron jobs ... they will automatically be regenerated by the regenconf --- data/hooks/backup/42-conf_ynh_dyndns | 1 - data/hooks/restore/42-conf_ynh_dyndns | 1 - 2 files changed, 2 deletions(-) diff --git a/data/hooks/backup/42-conf_ynh_dyndns b/data/hooks/backup/42-conf_ynh_dyndns index 3dbcc2780..6343f9086 100644 --- a/data/hooks/backup/42-conf_ynh_dyndns +++ b/data/hooks/backup/42-conf_ynh_dyndns @@ -8,4 +8,3 @@ cd "$YNH_CWD" # Backup the configuration ynh_exec_warn_less ynh_backup --src_path="/etc/yunohost/dyndns" --not_mandatory -ynh_exec_warn_less ynh_backup --src_path="/etc/cron.d/yunohost-dyndns" --not_mandatory diff --git a/data/hooks/restore/42-conf_ynh_dyndns b/data/hooks/restore/42-conf_ynh_dyndns index d16d7a67c..8ed4941ef 100644 --- a/data/hooks/restore/42-conf_ynh_dyndns +++ b/data/hooks/restore/42-conf_ynh_dyndns @@ -7,4 +7,3 @@ cd "$YNH_CWD" # Restore file if exists ynh_restore_file --origin_path="/etc/yunohost/dyndns" --not_mandatory -ynh_restore_file --origin_path="/etc/cron.d/yunohost-dyndns" --not_mandatory From 071732dd7f78f96d4d4f8f2973082f308f841335 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 22 Jan 2021 03:04:56 +0100 Subject: [PATCH 282/363] Improve check that a dyndns domain already exists --- src/yunohost/domain.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 95d9ee7b0..1198ef473 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -119,11 +119,11 @@ def domain_add(operation_logger, domain, dyndns=False): # DynDNS domain if dyndns: - # Do not allow to subscribe to multiple dyndns domains... - if os.path.exists("/etc/cron.d/yunohost-dyndns"): - raise YunohostError("domain_dyndns_already_subscribed") + from yunohost.dyndns import dyndns_subscribe, _dyndns_provides, _guess_current_dyndns_domain - from yunohost.dyndns import dyndns_subscribe, _dyndns_provides + # Do not allow to subscribe to multiple dyndns domains... + if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None): + raise YunohostError('domain_dyndns_already_subscribed') # Check that this domain can effectively be provided by # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) From 64a1b4cad2ad8b871db88f820e30ddfe59e21edd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Feb 2021 00:20:39 +0100 Subject: [PATCH 283/363] Misc fixes after testing --- debian/control | 2 +- src/yunohost/dyndns.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/debian/control b/debian/control index 7275cb7b1..ef5061fe7 100644 --- a/debian/control +++ b/debian/control @@ -27,7 +27,7 @@ Depends: ${python3:Depends}, ${misc:Depends} , redis-server , metronome (>=3.14.0) , acl - , git, curl, wget, cron, unzip, jq, bc + , git, curl, wget, cron, unzip, jq, bc, at , lsb-release, haveged, fake-hwclock, equivs, lsof, whois Recommends: yunohost-admin , ntp, inetutils-ping | iputils-ping diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 6545d33c7..a921cfb5c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -123,7 +123,7 @@ def dyndns_subscribe( """ - if _guess_current_dyndns_domain(dyn_host) != (None, None): + if _guess_current_dyndns_domain(subscribe_host) != (None, None): raise YunohostError('domain_dyndns_already_subscribed') if domain is None: @@ -169,7 +169,7 @@ def dyndns_subscribe( try: r = requests.post( "https://%s/key/%s?key_algo=hmac-sha512" - % (subscribe_host, base64.b64encode(key)), + % (subscribe_host, base64.b64encode(key.encode()).decode()), data={"subdomain": domain}, timeout=30, ) @@ -188,13 +188,14 @@ def dyndns_subscribe( # Yunohost regen conf will add the dyndns cron job if a private key exists # in /etc/yunohost/dyndns - regen_conf("yunohost") + regen_conf(["yunohost"]) # Add some dyndns update in 2 and 4 minutes from now such that user should # not have to wait 10ish minutes for the conf to propagate cmd = "at -M now + {t} >/dev/null 2>&1 <<< \"/bin/bash -c 'yunohost dyndns update'\"" - subprocess.check_call(cmd.format(t="2 min"), shell=True) - subprocess.check_call(cmd.format(t="4 min"), shell=True) + # For some reason subprocess doesn't like the redirections so we have to use bash -c explicity... + subprocess.check_call(["bash", "-c", cmd.format(t="2 min")]) + subprocess.check_call(["bash", "-c", cmd.format(t="4 min")]) logger.success(m18n.n('dyndns_registered')) From c6d14219cc5b5cc47ca9e367bdec085d71404967 Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sat, 27 Feb 2021 20:39:55 +0100 Subject: [PATCH 284/363] Force destination to be replaced --- 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 96609c7dc..d4797c8a0 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -316,7 +316,7 @@ ynh_add_config () { ynh_backup_if_checksum_is_different --file="$destination" - cp "$template_path" "$destination" + cp -f "$template_path" "$destination" chown root: "$destination" ynh_replace_vars --file="$destination" From fcea5a6af0d0d29d8d57c030c7c0836b8fdbb462 Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sun, 28 Feb 2021 13:38:10 +0100 Subject: [PATCH 285/363] Make grep lazy --- 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 d4797c8a0..af8464279 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -382,7 +382,7 @@ ynh_replace_vars () { # Replace others variables # List other unique (__ __) variables in $file - local uniques_vars=( $(grep -o '__[A-Z0-9_]*__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) + local uniques_vars=( $(grep -oP '__[A-Z0-9_]+?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) # Do the replacement local delimit=@ From 4131ddb0706ccd6121e0d542cc2b6d84c665f0ab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 28 Feb 2021 17:00:48 +0100 Subject: [PATCH 286/363] Fix cases where we want to test if translation exists for a key --- src/yunohost/diagnosis.py | 3 +-- src/yunohost/service.py | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py index 93ece21fc..5666afb07 100644 --- a/src/yunohost/diagnosis.py +++ b/src/yunohost/diagnosis.py @@ -449,9 +449,8 @@ class Diagnoser(): @staticmethod def get_description(id_): key = "diagnosis_description_" + id_ - descr = m18n.n(key) # If no description available, fallback to id - return descr if descr != key else id_ + return m18n.n(key) if m18n.key_exists(key) else id_ @staticmethod def i18n(report, force_remove_html_tags=False): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 347932add..211d7bf56 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -352,14 +352,11 @@ def _get_and_format_service_status(service, infos): # If no description was there, try to get it from the .json locales if not description: - translation_key = "service_description_%s" % service - description = m18n.n(translation_key) - # If descrption is still equal to the 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: + translation_key = "service_description_%s" % service + if m18n.key_exists(translation_key): + description = m18n.key_exists(translation_key) + else: description = str(raw_status.get("Description", "")) output = { From c86d4327832dc85a559a71d28e50405875bbed0a Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sun, 28 Feb 2021 18:56:30 +0100 Subject: [PATCH 287/363] Fixing ___APP__ --- 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 af8464279..487ec41db 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -382,7 +382,7 @@ ynh_replace_vars () { # Replace others variables # List other unique (__ __) variables in $file - local uniques_vars=( $(grep -oP '__[A-Z0-9_]+?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) + local uniques_vars=( $(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) # Do the replacement local delimit=@ From 0884a0c162af28ac71f5d1cdf6878349b0f6ffc1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 1 Mar 2021 18:06:43 +0100 Subject: [PATCH 288/363] Further simplify logging configuration by using the 'cli' handler when running yunohost-api in debug mode instead of creating a 'console' handler which is pretty much the same --- src/yunohost/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py index 04b2115b2..04caefc7a 100644 --- a/src/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -154,14 +154,8 @@ def init_logging(interface="cli", debug=False, quiet=False, logdir="/var/log/yun # This is for when launching yunohost-api in debug mode, we want to display stuff in the console if debug: - logging_configuration["handlers"]["console"] = { - 'class': 'logging.StreamHandler', - 'formatter': 'console', - 'stream': 'ext://sys.stdout', - 'filters': ['action'], - }, - logging_configuration["loggers"]["yunohost"]["handlers"].append("console") - logging_configuration["loggers"]["moulinette"]["handlers"].append("console") - logging_configuration["root"]["handlers"].append("console") + logging_configuration["loggers"]["yunohost"]["handlers"].append("cli") + logging_configuration["loggers"]["moulinette"]["handlers"].append("cli") + logging_configuration["root"]["handlers"].append("cli") configure_logging(logging_configuration) From d763247df445a0e485746f49453a024e5285e660 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 1 Mar 2021 19:11:41 +0100 Subject: [PATCH 289/363] No need for mysql root password (#912) * Get rid of /etc/yunohost/mysql * Get rid of restore hook for mysql password * Tab -> spaces * declare->local lost while merging conflicts etc * Gotta keep that var --- data/helpers.d/mysql | 26 +++++++++------------- data/hooks/conf_regen/34-mysql | 33 +++++++++++++++++++--------- data/hooks/restore/11-conf_ynh_mysql | 5 ----- src/yunohost/tests/test_apps.py | 15 ++++--------- 4 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 data/hooks/restore/11-conf_ynh_mysql diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 05f75e0a2..6808441b7 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -1,7 +1,5 @@ #!/bin/bash -MYSQL_ROOT_PWD_FILE=/etc/yunohost/mysql - # Open a connection as a user # # example: ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;" @@ -49,8 +47,7 @@ ynh_mysql_execute_as_root() { database="--database=$database" fi - ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \ - $database <<< "$sql" + mysql -B "$database" <<< "$sql" } # Execute a command from a file as root user @@ -75,9 +72,7 @@ ynh_mysql_execute_file_as_root() { database="--database=$database" fi - - ynh_mysql_connect_as --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" \ - $database < "$file" + mysql -B "$database" < "$file" } # Create a database and grant optionnaly privilegies to a user @@ -140,7 +135,7 @@ ynh_mysql_dump_db() { # Manage arguments with getopts ynh_handle_getopts_args "$@" - mysqldump --user="root" --password="$(cat $MYSQL_ROOT_PWD_FILE)" --single-transaction --skip-dump-date "$database" + mysqldump --single-transaction --skip-dump-date "$database" } # Create a user @@ -214,12 +209,13 @@ ynh_mysql_setup_db () { # Manage arguments with getopts ynh_handle_getopts_args "$@" - local new_db_pwd=$(ynh_string_random) # Generate a random password + # Generate a random password + local new_db_pwd=$(ynh_string_random) # If $db_pwd is not provided, use new_db_pwd instead for db_pwd db_pwd="${db_pwd:-$new_db_pwd}" - ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" # Create the database - ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd # Store the password in the app's config + ynh_mysql_create_db "$db_name" "$db_user" "$db_pwd" + ynh_app_setting_set --app=$app --key=mysqlpwd --value=$db_pwd } # Remove a database if it exists, and the associated user @@ -232,16 +228,14 @@ ynh_mysql_setup_db () { ynh_mysql_remove_db () { # Declare an array to define the options of this helper. local legacy_args=un - local -A args_array=( [u]=db_user= [n]=db_name= ) + local -Ar args_array=( [u]=db_user= [n]=db_name= ) local db_user local db_name # Manage arguments with getopts ynh_handle_getopts_args "$@" - local mysql_root_password=$(cat $MYSQL_ROOT_PWD_FILE) - if mysqlshow --user=root --password=$mysql_root_password | grep --quiet "^| $db_name" - then # Check if the database exists - ynh_mysql_drop_db $db_name # Remove the database + if mysqlshow | grep -q "^| $db_name "; then + ynh_mysql_drop_db $db_name else ynh_print_warn --message="Database $db_name not found" fi diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index d9374bbf5..6c9694796 100755 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -1,7 +1,6 @@ #!/bin/bash set -e -MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')" . /usr/share/yunohost/helpers do_pre_regen() { @@ -20,6 +19,7 @@ do_post_regen() { # dpkg-reconfigure will initialize mysql (if it ain't already) # It enabled auth_socket for root, so no need to define any root password... # c.f. : cat /var/lib/dpkg/info/mariadb-server-10.3.postinst | grep install_db -C3 + MYSQL_PKG="$(dpkg --list | sed -ne 's/^ii \(mariadb-server-[[:digit:].]\+\) .*$/\1/p')" dpkg-reconfigure -freadline -u "$MYSQL_PKG" 2>&1 systemctl -q is-active mariadb.service \ @@ -27,17 +27,30 @@ do_post_regen() { sleep 5 - echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" + echo "" | mysql && echo "Can't connect to mysql using unix_socket auth ... something went wrong during initial configuration of mysql !?" >&2 fi - if [ ! -e /etc/yunohost/mysql ] - then - # Dummy password that's not actually used nor meaningful ... - # (because mysql is supposed to be configured to use unix_socket on new setups) - # but keeping it for legacy - # until we merge https://github.com/YunoHost/yunohost/pull/912 ... - ynh_string_random 10 > /etc/yunohost/mysql - chmod 400 /etc/yunohost/mysql + # Legacy code to get rid of /etc/yunohost/mysql ... + # Nowadays, we can simply run mysql while being run as root of unix_socket/auth_socket is enabled... + if [ -f /etc/yunohost/mysql ]; then + + # This is a trick to check if we're able to use mysql without password + # Expect instances installed in stretch to already have unix_socket + #configured, but not old instances from the jessie/wheezy era + if ! echo "" | mysql + then + password="$(cat /etc/yunohost/mysql)" + # Enable plugin unix_socket for root on localhost + mysql -u root -p"$password" <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED WITH unix_socket WITH GRANT OPTION;" + fi + + # If now we're able to login without password, drop the mysql password + if echo "" | mysql + then + rm /etc/yunohost/mysql + else + echo "Can't connect to mysql using unix_socket auth ... something went wrong while trying to get rid of mysql password !?" >&2 + fi fi # mysql is supposed to be an alias to mariadb... but in some weird case is not diff --git a/data/hooks/restore/11-conf_ynh_mysql b/data/hooks/restore/11-conf_ynh_mysql deleted file mode 100644 index 11353425a..000000000 --- a/data/hooks/restore/11-conf_ynh_mysql +++ /dev/null @@ -1,5 +0,0 @@ -# We don't backup/restore mysql password anymore -# c.f. https://github.com/YunoHost/yunohost/pull/912 - -# This is a dummy empty file as a workaround for -# https://github.com/YunoHost/issues/issues/1553 until it is fixed diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index ae8a4829b..b9e9e7530 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -55,18 +55,11 @@ def clean(): for folderpath in glob.glob("/var/www/*%s*" % test_app): shutil.rmtree(folderpath, ignore_errors=True) - os.system( - "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP DATABASE %s' \"" - % test_app - ) - os.system( - "bash -c \"mysql -u root --password=$(cat /etc/yunohost/mysql) 2>/dev/null <<< 'DROP USER %s@localhost'\"" - % test_app - ) + os.system("bash -c \"mysql -B 2>/dev/null <<< 'DROP DATABASE %s' \"" % test_app) + os.system("bash -c \"mysql -B 2>/dev/null <<< 'DROP USER %s@localhost'\"" % test_app) - os.system( - "systemctl reset-failed nginx" - ) # Reset failed quota for service to avoid running into start-limit rate ? + # Reset failed quota for service to avoid running into start-limit rate ? + os.system("systemctl reset-failed nginx") os.system("systemctl start nginx") # Clean permissions From 01058aca281a2bcde3092d9fb35a548505370867 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Mar 2021 01:49:23 +0100 Subject: [PATCH 290/363] Catch more secrets not redacted --- 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 763967b38..1b7d66c44 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -415,7 +415,7 @@ class RedactingFormatter(Formatter): # (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=") # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest match = re.search( - r"(pwd|pass|password|secret|\w+key|token)=(\S{3,})$", record.strip() + r"(pwd|pass|password|secret\w*|\w+key|token)=(\S{3,})$", record.strip() ) if ( match From 0c172cd3f95799f20d3ab55faa9d3d02437a35d4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Mar 2021 01:49:23 +0100 Subject: [PATCH 291/363] Catch more secrets not redacted --- 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 7e9ae18e6..c51628c50 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -387,7 +387,7 @@ class RedactingFormatter(Formatter): # This matches stuff like db_pwd=the_secret or admin_password=other_secret # (the secret part being at least 3 chars to avoid catching some lines like just "db_pwd=") # Some names like "key" or "manifest_key" are ignored, used in helpers like ynh_app_setting_set or ynh_read_manifest - match = re.search(r'(pwd|pass|password|secret|\w+key|token)=(\S{3,})$', record.strip()) + match = re.search(r'(pwd|pass|password|secret\w*|\w+key|token)=(\S{3,})$', record.strip()) if match and match.group(2) not in self.data_to_redact and match.group(1) not in ["key", "manifest_key"]: self.data_to_redact.append(match.group(2)) except Exception as e: From a43cd72c72524b2f9eb22fa86a2edac0465ab432 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:36:28 +0100 Subject: [PATCH 292/363] Fix ynh_replace_vars again, mystical bash is mystic... --- data/helpers.d/utils | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 5e02ca762..c67e1fae1 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -393,8 +393,11 @@ ynh_replace_vars () { for one_var in "${uniques_vars[@]}" do # Validate that one_var is indeed defined - # Explanation for the weird '+x' syntax: https://stackoverflow.com/a/13864829 - test -n "${one_var+x}" || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" + # -v checks if the variable is defined, for example: + # -v FOO tests if $FOO is defined + # -v $FOO tests if ${!FOO} is defined + # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 + [[ -v "${one_var:-}" ]] || ynh_die --message="Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" # Escape delimiter in match/replace string match_string="__${one_var^^}__" From 2728801d1798be9eff25294dc85dbce2c4bb02ad Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sat, 27 Feb 2021 20:39:55 +0100 Subject: [PATCH 293/363] Force destination to be replaced --- 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 c67e1fae1..fee7e009b 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -321,7 +321,7 @@ ynh_add_config () { ynh_backup_if_checksum_is_different --file="$destination" - cp "$template_path" "$destination" + cp -f "$template_path" "$destination" ynh_replace_vars --file="$destination" From 9bbc3b72ae30092b717af7095359b16975700ca6 Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sun, 28 Feb 2021 13:38:10 +0100 Subject: [PATCH 294/363] Make grep lazy --- 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 fee7e009b..b1b265b8b 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -386,7 +386,7 @@ ynh_replace_vars () { # Replace others variables # List other unique (__ __) variables in $file - local uniques_vars=( $(grep -o '__[A-Z0-9_]*__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) + local uniques_vars=( $(grep -oP '__[A-Z0-9_]+?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) # Do the replacement local delimit=@ From 2402a1db39bf3a294459e1c0123b254e36c9f19c Mon Sep 17 00:00:00 2001 From: yalh76 Date: Sun, 28 Feb 2021 18:56:30 +0100 Subject: [PATCH 295/363] Fixing ___APP__ --- 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 b1b265b8b..53a7ea3b7 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -386,7 +386,7 @@ ynh_replace_vars () { # Replace others variables # List other unique (__ __) variables in $file - local uniques_vars=( $(grep -oP '__[A-Z0-9_]+?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) + local uniques_vars=( $(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g" )) # Do the replacement local delimit=@ From 88b414c8f3e0d3192a057f18260b65b9e3b1b23e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:44:55 +0100 Subject: [PATCH 296/363] Don't redact empty string... --- src/yunohost/log.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c51628c50..a5501557d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -376,7 +376,11 @@ class RedactingFormatter(Formatter): msg = super(RedactingFormatter, self).format(record) self.identify_data_to_redact(msg) for data in self.data_to_redact: - msg = msg.replace(data, "**********") + # we check that data is not empty string, + # otherwise this may lead to super epic stuff + # (try to run "foo".replace("", "bar")) + if data: + msg = msg.replace(data, "**********") return msg def identify_data_to_redact(self, record): From d12f403fe3235364f80f33c0522fb56e1998cb26 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Feb 2021 16:57:57 +0100 Subject: [PATCH 297/363] Fix permission helper doc format --- data/helpers.d/permission | 47 +++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/data/helpers.d/permission b/data/helpers.d/permission index 1791425b5..4b51902eb 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -25,19 +25,14 @@ # usage: ynh_permission_create --permission="permission" [--url="url"] [--additional_urls="second-url" [ "third-url" ]] [--auth_header=true|false] # [--allowed=group1 [ group2 ]] [--label="label"] [--show_tile=true|false] # [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Not that if 'show_tile' is enabled, this URL will be the URL of the tile. -# | arg: -A, additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true -# | arg: -a, allowed= - (optional) A list of group/user to allow for the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | Default is "APP_LABEL (permission name)". -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. -# | Default is false (for the permission different than 'main'). -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. -# | By default it's 'false' +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if 'show_tile' is enabled, this URL will be the URL of the tile. +# | arg: -A, --additional_urls= - (optional) List of additional URL for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application. Default is true +# | arg: -a, --allowed= - (optional) A list of group/user to allow for the permission +# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. Default is "APP_LABEL (permission name)". +# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO. If yes the name of the tile will be the 'label' parameter. Defaults to false for the permission different than 'main'. +# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. Defaults to 'false'. # # If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they # start with '/'. For example: @@ -192,13 +187,12 @@ ynh_permission_exists() { # # usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] # [--auth_header=true|false] [--clear_urls] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) -# | arg: -u, url= - (optional) URL for which access will be allowed/forbidden. -# | Note that if you want to remove url you can pass an empty sting as arguments (""). -# | arg: -a, add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. -# | arg: -r, remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden -# | arg: -h, auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application -# | arg: -c, clear_urls - (optional) Clean all urls (url and additional_urls) +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) +# | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments (""). +# | arg: -a, --add_url= - (optional) List of additional url to add for which access will be allowed/forbidden. +# | arg: -r, --remove_url= - (optional) List of additional url to remove for which access will be allowed/forbidden +# | arg: -h, --auth_header= - (optional) Define for the URL of this permission, if SSOwat pass the authentication header to the application +# | arg: -c, --clear_urls - (optional) Clean all urls (url and additional_urls) # # Requires YunoHost version 3.7.0 or higher. ynh_permission_url() { @@ -268,13 +262,12 @@ ynh_permission_url() { # # usage: ynh_permission_update --permission "permission" [--add="group" ["group" ...]] [--remove="group" ["group" ...]] # [--label="label"] [--show_tile=true|false] [--protected=true|false] -# | arg: -p, permission= - the name for the permission (by default a permission named "main" already exist) -# | arg: -a, add= - the list of group or users to enable add to the permission -# | arg: -r, remove= - the list of group or users to remove from the permission -# | arg: -l, label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. -# | arg: -t, show_tile= - (optional) Define if a tile will be shown in the SSO -# | arg: -P, protected= - (optional) Define if this permission is protected. If it is protected the administrator -# | won't be able to add or remove the visitors group of this permission. +# | arg: -p, --permission= - the name for the permission (by default a permission named "main" already exist) +# | arg: -a, --add= - the list of group or users to enable add to the permission +# | arg: -r, --remove= - the list of group or users to remove from the permission +# | arg: -l, --label= - (optional) Define a name for the permission. This label will be shown on the SSO and in the admin. +# | arg: -t, --show_tile= - (optional) Define if a tile will be shown in the SSO +# | arg: -P, --protected= - (optional) Define if this permission is protected. If it is protected the administrator won't be able to add or remove the visitors group of this permission. # # Requires YunoHost version 3.7.0 or higher. ynh_permission_update() { From 05969184feda1cb6c554ff0be2857229d140fde9 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Thu, 25 Feb 2021 11:16:34 +0100 Subject: [PATCH 298/363] fix ynh_systemd_action in case of service fails --- data/helpers.d/systemd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index b0e175d4d..7f316cf2b 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -92,7 +92,7 @@ ynh_remove_systemd_config () { # usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ] # | arg: -n, --service_name= - Name of the service to start. Default : $app # | arg: -a, --action= - Action to perform with systemctl. Default: start -# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. WARNING: When using --line_match, you should always add `ynh_clean_check_starting` into your `ynh_clean_setup` at the beginning of the script. Otherwise, tail will not stop in case of failure of the script. The script will then hang forever. +# | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. # | arg: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log # | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. # | arg: -e, --length= - Length of the error log : Default : 20 @@ -151,6 +151,7 @@ ynh_systemd_action() { then ynh_exec_err tail --lines=$length "$log_path" fi + ynh_clean_check_starting return 1 fi @@ -195,9 +196,8 @@ ynh_systemd_action() { } # Clean temporary process and file used by ynh_check_starting -# (usually used in ynh_clean_setup scripts) # -# usage: ynh_clean_check_starting +# [internal] # # Requires YunoHost version 3.5.0 or higher. ynh_clean_check_starting () { @@ -208,7 +208,7 @@ ynh_clean_check_starting () { fi if [ -n "$templog" ] then - ynh_secure_remove "$templog" 2>&1 + ynh_secure_remove --file="$templog" 2>&1 fi } From 6ce02270ae9b6f5ad0c14b790769795b80aa0afa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Feb 2021 23:46:12 +0100 Subject: [PATCH 299/363] Gotta escape \ during ynh_replace_vars --- data/helpers.d/utils | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 53a7ea3b7..e26cac5ba 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -403,6 +403,7 @@ ynh_replace_vars () { match_string="__${one_var^^}__" match_string=${match_string//${delimit}/"\\${delimit}"} replace_string="${!one_var}" + replace_string=${replace_string//\\/\\\\} replace_string=${replace_string//${delimit}/"\\${delimit}"} # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) From bd8644a651a77b16713aec2d83ca04468c31b5b5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 20 Feb 2021 17:07:38 +0100 Subject: [PATCH 300/363] Translation typo.. --- locales/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 9b5d18f1a..0a5095b9e 100644 --- a/locales/it.json +++ b/locales/it.json @@ -591,7 +591,7 @@ "log_user_permission_update": "Aggiorna gli accessi del permesso '{}'", "log_user_group_update": "Aggiorna il gruppo '{}'", "log_user_group_delete": "Cancella il gruppo '{}'", - "log_user_group_create": "Crea il gruppo '[}'", + "log_user_group_create": "Crea il gruppo '{}'", "log_permission_url": "Aggiorna l'URL collegato al permesso '{}'", "log_permission_delete": "Cancella permesso '{}'", "log_permission_create": "Crea permesso '{}'", From 234ae15d771b620b1239236c789cd5be66f7062f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Mar 2021 02:07:58 +0100 Subject: [PATCH 301/363] Update changelog for 4.1.7.3 --- debian/changelog | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 95cca2eb8..5fb0e563d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,17 @@ -yunohost (4.1.7.2) testing; urgency=low +yunohost (4.1.7.3) stable; urgency=low + + - [fix] log: Some secrets were not redacted (0c172cd3) + - [fix] log: For some reason sometimes we were redacting 'empty string' which made everything explode (88b414c8) + - [fix] helpers: Various fixes for ynh_add_config / ynh_replace_vars (a43cd72c, 2728801d, 9bbc3b72, 2402a1db, 6ce02270) + - [fix] helpers: Fix permission helpers doc format (d12f403f) + - [fix] helpers: ynh_systemd_action did not properly clean the 'tail' process when service action failed (05969184) + - [fix] i18n: Translation typo in italian translation ... (bd8644a6) + + Thanks to all contributors <3 ! (Kay0u, yalh76) + + -- Alexandre Aubin Tue, 02 Mar 2021 02:03:35 +0100 + +yunohost (4.1.7.2) stable; urgency=low - [fix] When migration legacy protected permissions, all users were allowed on the new perm (29bd3c4a) - [fix] Mysql is a fucking joke (... trying to fix the mysql issue on RPi ...) (cd4fdb2b) From 9b13c95f77f1d2929841ec889d5c46173bc9033b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 2 Mar 2021 19:54:24 +0100 Subject: [PATCH 302/363] Re-add --others_var in fail2ban and systemd helpers for backward compatibility... --- data/helpers.d/fail2ban | 7 +++++-- data/helpers.d/systemd | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index c9322d067..f8b2b1761 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -61,12 +61,13 @@ # Requires YunoHost version 3.5.0 or higher. ynh_add_fail2ban_config () { # Declare an array to define the options of this helper. - local legacy_args=lrmpt - local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template) + local legacy_args=lrmptv + local -A args_array=( [l]=logpath= [r]=failregex= [m]=max_retry= [p]=ports= [t]=use_template [v]=others_var=) local logpath local failregex local max_retry local ports + local others_var local use_template # Manage arguments with getopts ynh_handle_getopts_args "$@" @@ -74,6 +75,8 @@ ynh_add_fail2ban_config () { ports=${ports:-http,https} use_template="${use_template:-0}" + [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2" + if [ $use_template -ne 1 ] then # Usage 1, no template. Build a config file from scratch. diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index f8e21bfbd..b43b593fa 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -13,15 +13,18 @@ # Requires YunoHost version 2.7.11 or higher. ynh_add_systemd_config () { # Declare an array to define the options of this helper. - local legacy_args=st - local -A args_array=( [s]=service= [t]=template=) + local legacy_args=stv + local -A args_array=( [s]=service= [t]=template= [v]=others_var=) local service local template + local others_var # Manage arguments with getopts ynh_handle_getopts_args "$@" local service="${service:-$app}" local template="${template:-systemd.service}" + [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2" + ynh_add_config --template="$YNH_APP_BASEDIR/conf/$template" --destination="/etc/systemd/system/$service.service" systemctl enable $service --quiet From 4603697ac5e37efc6cd0c734dec260019301d96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Tue, 16 Feb 2021 14:39:24 +0100 Subject: [PATCH 303/363] helper doc fixes : permission --- data/helpers.d/permission | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/data/helpers.d/permission b/data/helpers.d/permission index ba2b22a53..995bff0eb 100644 --- a/data/helpers.d/permission +++ b/data/helpers.d/permission @@ -2,15 +2,17 @@ # Create a new permission for the app # -# example 1: ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \ -# --label="My app admin" --show_tile=true +# Example 1: `ynh_permission_create --permission=admin --url=/admin --additional_urls=domain.tld/admin /superadmin --allowed=alice bob \ +# --label="My app admin" --show_tile=true` # # This example will create a new permission permission with this following effect: # - A tile named "My app admin" in the SSO will be available for the users alice and bob. This tile will point to the relative url '/admin'. # - Only the user alice and bob will have the access to theses following url: /admin, domain.tld/admin, /superadmin # # -# example 2: ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \ +# Example 2: +# +# ynh_permission_create --permission=api --url=domain.tld/api --auth_header=false --allowed=visitors \ # --label="MyApp API" --protected=true # # This example will create a new protected permission. So the admin won't be able to add/remove the visitors group of this permission. @@ -18,7 +20,7 @@ # With this permission all client will be allowed to access to the url 'domain.tld/api'. # Note that in this case no tile will be show on the SSO. # Note that the auth_header parameter is to 'false'. So no authentication header will be passed to the application. -# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case. +# Generally the API is requested by an application and enabling the auth_header has no advantage and could bring some issues in some case. # So in this case it's better to disable this option for all API. # # @@ -36,14 +38,14 @@ # # If provided, 'url' or 'additional_urls' is assumed to be relative to the app domain/path if they # start with '/'. For example: -# / -> domain.tld/app -# /admin -> domain.tld/app/admin -# domain.tld/app/api -> domain.tld/app/api +# / -> domain.tld/app +# /admin -> domain.tld/app/admin +# domain.tld/app/api -> domain.tld/app/api # # 'url' or 'additional_urls' can be treated as a PCRE (not lua) regex if it starts with "re:". # For example: -# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ -# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ +# re:domain.tld/app/api/[A-Z]*$ -> domain.tld/app/api/[A-Z]*$ # # Note that globally the parameter 'url' and 'additional_urls' are same. The only difference is: # - 'url' is only one url, 'additional_urls' can be a list of urls. There are no limitation of 'additional_urls' @@ -56,7 +58,7 @@ # - "Remote-User": username # - "Email": user email # -# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly. +# Generally this feature is usefull to authenticate automatically the user in the application but in some case the application don't work with theses header and theses header need to be disabled to have the application to work correctly. # See https://github.com/YunoHost/issues/issues/1420 for more informations # # @@ -186,7 +188,7 @@ ynh_permission_exists() { # Redefine the url associated to a permission # -# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] +# usage: ynh_permission_url --permission "permission" [--url="url"] [--add_url="new-url" [ "other-new-url" ]] [--remove_url="old-url" [ "other-old-url" ]] # [--auth_header=true|false] [--clear_urls] # | arg: -p, --permission= - the name for the permission (by default a permission named "main" is removed automatically when the app is removed) # | arg: -u, --url= - (optional) URL for which access will be allowed/forbidden. Note that if you want to remove url you can pass an empty sting as arguments (""). From e6f3adfa08a08373e5ab1b4e75a8fc88e7f00dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:58:06 +0100 Subject: [PATCH 304/363] helper doc fixes : apt --- data/helpers.d/apt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index bfdeffe7b..1b5ab982f 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -47,10 +47,11 @@ ynh_wait_dpkg_free() { # Check either a package is installed or not # -# example: ynh_package_is_installed --package=yunohost && echo "ok" +# example: ynh_package_is_installed --package=yunohost && echo "installed" # # usage: ynh_package_is_installed --package=name # | arg: -p, --package= - the package name to check +# | ret: 0 if the package is installed, 1 else. # # Requires YunoHost version 2.2.4 or higher. ynh_package_is_installed() { @@ -180,7 +181,7 @@ ynh_package_install_from_equivs () { # Build and install the package local TMPDIR=$(mktemp --directory) - # Force the compatibility level at 10, levels below are deprecated + # 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 @@ -194,7 +195,7 @@ ynh_package_install_from_equivs () { LC_ALL=C dpkg --force-depends --install "./${pkgname}_${pkgversion}_all.deb" 2>&1 | tee ./dpkg_log) ynh_package_install --fix-broken || \ - { # If the installation failed + { # If the installation failed # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) # Parse the list of problematic dependencies from dpkg's log ... # (relevant lines look like: "foo-ynh-deps depends on bar; however:") @@ -216,7 +217,8 @@ ynh_package_install_from_equivs () { # example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5" # # usage: ynh_install_app_dependencies dep [dep [...]] -# | arg: dep - the package name to install in dependence. Writing "dep3|dep4|dep5" can be used to specify alternatives. For example : dep1 dep2 "dep3|dep4|dep5" will require to install dep1 and dep 2 and (dep3 or dep4 or dep5). +# | arg: dep - the package name to install in dependence. +# | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc). # # Requires YunoHost version 2.6.4 or higher. ynh_install_app_dependencies () { @@ -253,7 +255,7 @@ ynh_install_app_dependencies () { # Epic ugly hack to fix the goddamn dependency nightmare of sury # Sponsored by the "Djeezusse Fokin Kraiste Why Do Adminsys Has To Be So Fucking Complicated I Should Go Grow Potatoes Instead Of This Shit" collective # https://github.com/YunoHost/issues/issues/1407 - # + # # If we require to install php dependency if echo $dependencies | grep --quiet 'php' then From d69f031d8fffedbfe916f91f493c32ea5d627ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:58:34 +0100 Subject: [PATCH 305/363] helper doc fixes : backup --- data/helpers.d/backup | 79 ++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/data/helpers.d/backup b/data/helpers.d/backup index 33a6db4e2..17da0fb2e 100644 --- a/data/helpers.d/backup +++ b/data/helpers.d/backup @@ -13,13 +13,13 @@ CAN_BIND=${CAN_BIND:-1} # # This helper can be used both in a system backup hook, and in an app backup script # -# Details: ynh_backup writes SRC and the relative DEST into a CSV file. And it +# `ynh_backup` writes `src_path` and the relative `dest_path` 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. -# -# Example in the context of a wordpress app +# If `dest_path` is ended by a slash it complete this path with the basename of `src_path`. # +# Example in the context of a wordpress app : +# ``` # 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" @@ -40,26 +40,28 @@ CAN_BIND=${CAN_BIND:-1} # ynh_backup "/etc/nginx/conf.d/$domain.d/$app.conf" "/conf/" # # => "/etc/nginx/conf.d/$domain.d/$app.conf","apps/wordpress/conf/$app.conf" # +# ``` # -# How to use --is_big: -# --is_big is used to specify that this part of the backup can be quite huge. +# How to use `--is_big`: +# +# `--is_big` is used to specify that this part of the backup can be quite huge. # So, you don't want that your package does backup that part during ynh_backup_before_upgrade. # In the same way, an user may doesn't want to backup this big part of the app for -# each of his backup. And so handle that part differently. -# -# As this part of your backup may not be done, your restore script has to handle it. -# In your restore script, use --not_mandatory with ynh_restore_file -# As well in your remove script, you should not remove those data ! Or an user may end up with -# a failed upgrade restoring an app without data anymore ! +# each of his backup. And so handle that part differently. # -# To have the benefit of --is_big while doing a backup, you can whether set the environement -# variable BACKUP_CORE_ONLY to 1 (BACKUP_CORE_ONLY=1) before the backup command. It will affect -# only that backup command. -# Or set the config do_not_backup_data to 1 into the settings.yml of the app. This will affect -# all backups for this app until the setting is removed. +# As this part of your backup may not be done, your restore script has to handle it. +# In your restore script, use `--not_mandatory` with `ynh_restore_file` +# As well in your remove script, you should not remove those data ! Or an user may end up with +# a failed upgrade restoring an app without data anymore ! +# +# To have the benefit of `--is_big` while doing a backup, you can whether set the environement +# variable `BACKUP_CORE_ONLY` to 1 (`BACKUP_CORE_ONLY=1`) before the backup command. It will affect +# only that backup command. +# Or set the config `do_not_backup_data` to 1 into the `settings.yml` of the app. This will affect +# all backups for this app until the setting is removed. # # Requires YunoHost version 2.4.0 or higher. -# Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory +# Requires YunoHost version 3.5.0 or higher for the argument `--not_mandatory` ynh_backup() { # TODO find a way to avoid injection by file strange naming ! @@ -81,7 +83,7 @@ ynh_backup() { # If backing up core only (used by ynh_backup_before_upgrade), # don't backup big data items - if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] ) + if [ $is_big -eq 1 ] && ( [ ${do_not_backup_data:-0} -eq 1 ] || [ $BACKUP_CORE_ONLY -eq 1 ] ) then if [ $BACKUP_CORE_ONLY -eq 1 ] then @@ -221,26 +223,25 @@ with open(sys.argv[1], 'r') as backup_file: # Restore a file or a directory # -# Use the registered path in backup_list by ynh_backup to restore the file at -# the right place. -# # usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory] # | arg: -o, --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: -d, --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 +# | arg: -d, --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` # | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it. # +# Use the registered path in backup_list by ynh_backup to restore the file at the right place. +# # examples: -# ynh_restore_file "/etc/nginx/conf.d/$domain.d/$app.conf" +# ynh_restore_file -o "/etc/nginx/conf.d/$domain.d/$app.conf" # # You can also use relative paths: -# ynh_restore_file "conf/nginx.conf" +# ynh_restore_file -o "conf/nginx.conf" # -# 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. +# 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. # -# if apps/wordpress/etc/nginx/conf.d/$domain.d/$app.conf exists, restore it into -# /etc/nginx/conf.d/$domain.d/$app.conf +# if `apps/$app/etc/nginx/conf.d/$domain.d/$app.conf` exists, restore it into +# `/etc/nginx/conf.d/$domain.d/$app.conf` # if no, search for a match in the csv (eg: conf/nginx.conf) and restore it into -# /etc/nginx/conf.d/$domain.d/$app.conf +# `/etc/nginx/conf.d/$domain.d/$app.conf` # # Requires YunoHost version 2.6.4 or higher. # Requires YunoHost version 3.5.0 or higher for the argument --not_mandatory @@ -345,14 +346,14 @@ ynh_store_file_checksum () { } # 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. # # usage: ynh_backup_if_checksum_is_different --file=file # | arg: -f, --file= - The file on which the checksum test will be perfomed. # | ret: the name of a backup file, or nothing # +# This helper is primarily meant to allow to easily backup personalised/manually +# modified config files. +# # Requires YunoHost version 2.6.4 or higher. ynh_backup_if_checksum_is_different () { # Declare an array to define the options of this helper. @@ -410,12 +411,16 @@ ynh_backup_archive_exists () { # Make a backup in case of failed upgrade # -# usage: +# usage: ynh_backup_before_upgrade +# +# Usage in a package script: +# ``` # ynh_backup_before_upgrade # ynh_clean_setup () { # ynh_restore_upgradebackup # } # ynh_abort_if_errors +# ``` # # Requires YunoHost version 2.7.2 or higher. ynh_backup_before_upgrade () { @@ -459,12 +464,16 @@ ynh_backup_before_upgrade () { # Restore a previous backup if the upgrade process failed # -# usage: +# usage: ynh_restore_upgradebackup +# +# Usage in a package script: +# ``` # ynh_backup_before_upgrade # ynh_clean_setup () { # ynh_restore_upgradebackup # } # ynh_abort_if_errors +# ``` # # Requires YunoHost version 2.7.2 or higher. ynh_restore_upgradebackup () { From 8f294916d9ea0af8318635c0588cb5ac4155cf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:58:44 +0100 Subject: [PATCH 306/363] helper doc fixes : fail2ban --- data/helpers.d/fail2ban | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index f8b2b1761..756f4b84a 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -12,15 +12,14 @@ # # usage 2: ynh_add_fail2ban_config --use_template [--others_var="list of others variables to replace"] # | arg: -t, --use_template - Use this helper in template mode -# | arg: -v, --others_var= - List of others variables to replace separeted by a space -# | for example : 'var_1 var_2 ...' +# | arg: -v, --others_var= - List of others variables to replace separeted by a space for example : 'var_1 var_2 ...' # -# This will use a template in ../conf/f2b_jail.conf and ../conf/f2b_filter.conf -# See the documentation of ynh_add_config for a description of the template +# This will use a template in `../conf/f2b_jail.conf` and `../conf/f2b_filter.conf` +# See the documentation of `ynh_add_config` for a description of the template # format and how placeholders are replaced with actual variables. # # Generally your template will look like that by example (for synapse): -# +# ``` # f2b_jail.conf: # [__APP__] # enabled = true @@ -28,7 +27,8 @@ # filter = __APP__ # logpath = /var/log/__APP__/logfile.log # maxretry = 3 -# +# ``` +# ``` # f2b_filter.conf: # [INCLUDES] # before = common.conf @@ -41,22 +41,25 @@ # failregex = ^%(__synapse_start_line)s INFO \- POST\-(\d+)\- \- \d+ \- Received request\: POST /_matrix/client/r0/login\??%(__synapse_start_line)s INFO \- POST\-\1\- Got login request with identifier: \{u'type': u'm.id.user', u'user'\: u'(.+?)'\}, medium\: None, address: None, user\: u'\5'%(__synapse_start_line)s WARNING \- \- (Attempted to login as @\5\:.+ but they do not exist|Failed password login for user @\5\:.+)$ # # ignoreregex = +# ``` # # ----------------------------------------------------------------------------- # # Note about the "failregex" option: -# regex to match the password failure messages in the logfile. The -# host must be matched by a group named "host". The tag "" can -# be used for standard IP/hostname matching and is only an alias for -# (?:::f{4,6}:)?(?P[\w\-.^_]+) # -# You can find some more explainations about how to make a regex here : -# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters +# regex to match the password failure messages in the logfile. The host must be +# matched by a group named "`host`". The tag "``" can be used for standard +# IP/hostname matching and is only an alias for `(?:::f{4,6}:)?(?P[\w\-.^_]+)` +# +# You can find some more explainations about how to make a regex here : +# https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Filters # # Note that the logfile need to exist before to call this helper !! # # To validate your regex you can test with this command: +# ``` # fail2ban-regex /var/log/YOUR_LOG_FILE_PATH /etc/fail2ban/filter.d/YOUR_APP.conf +# ``` # # Requires YunoHost version 3.5.0 or higher. ynh_add_fail2ban_config () { From 966cd2ff165c1d2de5d999eb5b5a4e42657c4983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:58:53 +0100 Subject: [PATCH 307/363] helper doc fixes : hardware --- data/helpers.d/hardware | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/helpers.d/hardware b/data/helpers.d/hardware index 0771c81d8..6d1c314fa 100644 --- a/data/helpers.d/hardware +++ b/data/helpers.d/hardware @@ -7,7 +7,7 @@ # | arg: -t, --total - Count total RAM+swap # | arg: -s, --ignore_swap - Ignore swap, consider only real RAM # | arg: -o, --only_swap - Ignore real RAM, consider only swap -# | ret: the amount of free ram +# | ret: the amount of free ram, in MB (MegaBytes) # # Requires YunoHost version 3.8.1 or higher. ynh_get_ram () { @@ -35,7 +35,7 @@ ynh_get_ram () { local free_ram=$(vmstat --stats --unit M | grep "free memory" | awk '{print $1}') local free_swap=$(vmstat --stats --unit M | grep "free swap" | awk '{print $1}') local free_ram_swap=$(( free_ram + free_swap )) - + # Use the total amount of free ram local ram=$free_ram_swap if [ $ignore_swap -eq 1 ] @@ -52,7 +52,7 @@ ynh_get_ram () { local total_ram=$(vmstat --stats --unit M | grep "total memory" | awk '{print $1}') local total_swap=$(vmstat --stats --unit M | grep "total swap" | awk '{print $1}') local total_ram_swap=$(( total_ram + total_swap )) - + local ram=$total_ram_swap if [ $ignore_swap -eq 1 ] then @@ -70,13 +70,13 @@ ynh_get_ram () { # Return 0 or 1 depending if the system has a given amount of RAM+swap free or total # -# usage: ynh_require_ram --required=RAM required in Mb [--free|--total] [--ignore_swap|--only_swap] -# | arg: -r, --required= - The amount to require, in Mb +# usage: ynh_require_ram --required=RAM [--free|--total] [--ignore_swap|--only_swap] +# | arg: -r, --required= - The amount to require, in MB # | arg: -f, --free - Count free RAM+swap # | arg: -t, --total - Count total RAM+swap # | arg: -s, --ignore_swap - Ignore swap, consider only real RAM # | arg: -o, --only_swap - Ignore real RAM, consider only swap -# | exit: Return 1 if the ram is under the requirement, 0 otherwise. +# | ret: 1 if the ram is under the requirement, 0 otherwise. # # Requires YunoHost version 3.8.1 or higher. ynh_require_ram () { From 33f8337f11af0a3d9809483cdae5e7e3d78ad1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:28 +0100 Subject: [PATCH 308/363] helper doc fixes : logging --- data/helpers.d/logging | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/data/helpers.d/logging b/data/helpers.d/logging index dc32ecba9..0505117b7 100644 --- a/data/helpers.d/logging +++ b/data/helpers.d/logging @@ -102,8 +102,7 @@ ynh_print_err () { # Execute a command and print the result as an error # -# usage: ynh_exec_err your_command -# usage: ynh_exec_err "your_command | other_command" +# usage: ynh_exec_err "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. @@ -117,8 +116,7 @@ ynh_exec_err () { # Execute a command and print the result as a warning # -# usage: ynh_exec_warn your_command -# usage: ynh_exec_warn "your_command | other_command" +# usage: ynh_exec_warn "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. @@ -132,8 +130,7 @@ ynh_exec_warn () { # Execute a command and force the result to be printed on stdout # -# usage: ynh_exec_warn_less your_command -# usage: ynh_exec_warn_less "your_command | other_command" +# usage: ynh_exec_warn_less "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. @@ -147,8 +144,7 @@ ynh_exec_warn_less () { # Execute a command and redirect stdout in /dev/null # -# usage: ynh_exec_quiet your_command -# usage: ynh_exec_quiet "your_command | other_command" +# usage: ynh_exec_quiet "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. @@ -162,8 +158,7 @@ ynh_exec_quiet () { # Execute a command and redirect stdout and stderr in /dev/null # -# usage: ynh_exec_fully_quiet your_command -# usage: ynh_exec_fully_quiet "your_command | other_command" +# usage: ynh_exec_fully_quiet "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. @@ -363,8 +358,7 @@ ynh_debug () { # Execute a command and print the result as debug # -# usage: ynh_debug_exec your_command -# usage: ynh_debug_exec "your_command | other_command" +# usage: ynh_debug_exec "your_command [ | other_command ]" # | arg: command - command to execute # # When using pipes, double quotes are required - otherwise, this helper will run the first command, and the whole output will be sent through the next pipe. From 0dde41fe4aaac5bd56c1d091df9a0a00c4532856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:28 +0100 Subject: [PATCH 309/363] helper doc fixes : logrotate --- data/helpers.d/logrotate | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/data/helpers.d/logrotate b/data/helpers.d/logrotate index d5384264c..2d9ab6b72 100644 --- a/data/helpers.d/logrotate +++ b/data/helpers.d/logrotate @@ -7,16 +7,14 @@ # | arg: -n, --nonappend - (optional) Replace the config file instead of appending this new config. # | arg: -u, --specific_user= - run logrotate as the specified user and group. If not specified logrotate is runned as root. # -# If no --logfile is provided, /var/log/${app} will be used as default. -# logfile can be just a directory, or a full path to a logfile : -# /parentdir/logdir -# /parentdir/logdir/logfile.log +# If no `--logfile` is provided, `/var/log/$app` will be used as default. +# `logfile` can point to a directory or a file. # # It's possible to use this helper multiple times, each config will be added to -# the same logrotate config file. Unless you use the option --non-append +# the same logrotate config file. Unless you use the option `--non-append` # # Requires YunoHost version 2.6.4 or higher. -# Requires YunoHost version 3.2.0 or higher for the argument --specific_user +# Requires YunoHost version 3.2.0 or higher for the argument `--specific_user` ynh_use_logrotate () { # Declare an array to define the options of this helper. local legacy_args=lnuya From 27caab72345bd9f27fa304ca23b4b48ff3b5380c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 310/363] helper doc fixes : multimedia --- data/helpers.d/multimedia | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/multimedia b/data/helpers.d/multimedia index f7c9d5974..7b379ad0f 100644 --- a/data/helpers.d/multimedia +++ b/data/helpers.d/multimedia @@ -47,12 +47,14 @@ ynh_multimedia_build_main_dir() { } # Add a directory in yunohost.multimedia -# This "directory" will be a symbolic link to a existing directory. # -# usage: ynh_multimedia_addfolder "Source directory" "Destination directory" +# usage: ynh_multimedia_addfolder --source_dir="source_dir" --dest_dir="dest_dir" # # | arg: -s, --source_dir= - Source directory - The real directory which contains your medias. # | arg: -d, --dest_dir= - Destination directory - The name and the place of the symbolic link, relative to "/home/yunohost.multimedia" +# +# This "directory" will be a symbolic link to a existing directory. +# ynh_multimedia_addfolder() { # Declare an array to define the options of this helper. @@ -80,6 +82,7 @@ ynh_multimedia_addfolder() { # usage: ynh_multimedia_addaccess user_name # # | arg: -u, --user_name= - The name of the user which gain this access. +# ynh_multimedia_addaccess () { # Declare an array to define the options of this helper. local legacy_args=u From 75539472ea9d28ce76d1a298fb85aa25a946b1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 311/363] helper doc fixes : mysql --- data/helpers.d/mysql | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/data/helpers.d/mysql b/data/helpers.d/mysql index 6808441b7..091dfaf40 100644 --- a/data/helpers.d/mysql +++ b/data/helpers.d/mysql @@ -2,14 +2,15 @@ # Open a connection as a user # -# example: ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;" -# example: ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql -# # usage: ynh_mysql_connect_as --user=user --password=password [--database=database] # | arg: -u, --user= - the user name to connect as # | arg: -p, --password= - the user password # | arg: -d, --database= - the database to connect to # +# examples: +# ynh_mysql_connect_as --user="user" --password="pass" <<< "UPDATE ...;" +# ynh_mysql_connect_as --user="user" --password="pass" < /path/to/file.sql +# # Requires YunoHost version 2.2.4 or higher. ynh_mysql_connect_as() { # Declare an array to define the options of this helper. @@ -120,11 +121,11 @@ ynh_mysql_drop_db() { # Dump a database # -# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql -# # usage: ynh_mysql_dump_db --database=database # | arg: -d, --database= - the database name to dump -# | ret: the mysqldump output +# | ret: The mysqldump output +# +# example: ynh_mysql_dump_db --database=roundcube > ./dump.sql # # Requires YunoHost version 2.2.4 or higher. ynh_mysql_dump_db() { @@ -156,7 +157,7 @@ ynh_mysql_create_user() { # # usage: ynh_mysql_user_exists --user=user # | arg: -u, --user= - the user for which to check existence -# | exit: Return 1 if the user doesn't exist, 0 otherwise. +# | ret: 0 if the user exists, 1 otherwise. # # Requires YunoHost version 2.2.4 or higher. ynh_mysql_user_exists() @@ -195,8 +196,8 @@ ynh_mysql_drop_user() { # | arg: -n, --db_name= - Name of the database # | arg: -p, --db_pwd= - Password of the database. If not provided, a password will be generated # -# 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. +# 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. # # Requires YunoHost version 2.6.4 or higher. ynh_mysql_setup_db () { From 94097d1153a40261a6d7b9df654e8f9a9cd66716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 312/363] helper doc fixes : network --- data/helpers.d/network | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/helpers.d/network b/data/helpers.d/network index 0760909c6..702757534 100644 --- a/data/helpers.d/network +++ b/data/helpers.d/network @@ -2,12 +2,12 @@ # Find a free port and return it # -# example: port=$(ynh_find_port --port=8080) -# # usage: ynh_find_port --port=begin_port # | arg: -p, --port= - port to start to search # | ret: the port number # +# example: port=$(ynh_find_port --port=8080) +# # Requires YunoHost version 2.6.4 or higher. ynh_find_port () { # Declare an array to define the options of this helper. @@ -27,11 +27,11 @@ ynh_find_port () { # Test if a port is available # -# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app" -# # usage: ynh_find_port --port=XYZ # | arg: -p, --port= - port to check -# | exit: Return 1 if the port is already used by another process. +# | ret: 0 if the port is available, 1 if it is already used by another process. +# +# example: ynh_port_available --port=1234 || ynh_die --message="Port 1234 is needs to be available for this app" # # Requires YunoHost version 3.8.0 or higher. ynh_port_available () { @@ -89,12 +89,12 @@ EOF # Validate an IPv4 address # -# example: ynh_validate_ip4 111.222.333.444 -# # usage: ynh_validate_ip4 --ip_address=ip_address # | arg: -i, --ip_address= - the ipv4 address to check # | ret: 0 for valid ipv4 addresses, 1 otherwise # +# example: ynh_validate_ip4 111.222.333.444 +# # Requires YunoHost version 2.2.4 or higher. ynh_validate_ip4() { @@ -111,12 +111,12 @@ ynh_validate_ip4() # Validate an IPv6 address # -# example: ynh_validate_ip6 2000:dead:beef::1 -# # usage: ynh_validate_ip6 --ip_address=ip_address -# | arg: -i, --ip_address= - the ipv6 address to check +# | arg: -i, --ip_address= - the ipv6 address to check # | ret: 0 for valid ipv6 addresses, 1 otherwise # +# example: ynh_validate_ip6 2000:dead:beef::1 +# # Requires YunoHost version 2.2.4 or higher. ynh_validate_ip6() { From 52cc5949da252c9c3ec807c303aac6365c86a41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 313/363] helper doc fixes : nginx --- data/helpers.d/nginx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/nginx b/data/helpers.d/nginx index 3c6254953..7214b1e26 100644 --- a/data/helpers.d/nginx +++ b/data/helpers.d/nginx @@ -4,13 +4,13 @@ # # usage: ynh_add_nginx_config # -# This will use a template in ../conf/nginx.conf -# See the documentation of ynh_add_config for a description of the template +# This will use a template in `../conf/nginx.conf` +# See the documentation of `ynh_add_config` for a description of the template # format and how placeholders are replaced with actual variables. # # Additionally, ynh_add_nginx_config will replace: -# - #sub_path_only by empty string if path_url is not '/' -# - #root_path_only by empty string if path_url *is* '/' +# - `#sub_path_only` by empty string if `path_url` is not `'/'` +# - `#root_path_only` by empty string if `path_url` *is* `'/'` # # This allows to enable/disable specific behaviors dependenging on the install # location From 3844b48962d58116d89ccb8d58c343ab3b391c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 314/363] helper doc fixes : nodejs --- data/helpers.d/nodejs | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index 2e1c787cf..f78a53fd5 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -28,11 +28,14 @@ SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > " # 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. +# usage: ynh_use_nodejs +# +# `ynh_use_nodejs` has to be used in any app scripts before using node for the first time. # This helper will provide alias and variables to use in your scripts. # -# To use npm or node, use the alias `ynh_npm` and `ynh_node` -# Those alias will use the correct version installed for the app +# To use npm or node, use the alias `ynh_npm` and `ynh_node`. +# +# Those alias will use the correct version installed for the app. # For example: use `ynh_npm install` instead of `npm install` # # With `sudo` or `ynh_exec_as`, use instead the fallback variables `$ynh_npm` and `$ynh_node` @@ -40,30 +43,32 @@ SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > " # Exemple: `ynh_exec_as $app $ynh_node_load_PATH $ynh_npm install` # # $PATH contains the path of the requested version of node. -# However, $PATH is duplicated into $node_PATH to outlast any manipulation of $PATH +# However, $PATH is duplicated into $node_PATH to outlast any manipulation of `$PATH` # You can use the variable `$ynh_node_load_PATH` to quickly load your node version -# in $PATH for an usage into a separate script. +# in $PATH for an usage into a separate script. # Exemple: $ynh_node_load_PATH $final_path/script_that_use_npm.sh` # # # Finally, to start a nodejs service with the correct version, 2 solutions # Either the app is dependent of node or npm, but does not called it directly. -# In such situation, you need to load PATH -# `Environment="__NODE_ENV_PATH__"` -# `ExecStart=__FINALPATH__/my_app` -# You will replace __NODE_ENV_PATH__ with $ynh_node_load_PATH +# In such situation, you need to load PATH : +# ``` +# Environment="__NODE_ENV_PATH__" +# ExecStart=__FINALPATH__/my_app +# ``` +# You will replace __NODE_ENV_PATH__ with $ynh_node_load_PATH. # # Or node start the app directly, then you don't need to load the PATH variable -# `ExecStart=__YNH_NODE__ my_app run` -# You will replace __YNH_NODE__ with $ynh_node +# ``` +# ExecStart=__YNH_NODE__ my_app run +# ``` +# You will replace __YNH_NODE__ with $ynh_node # # # 2 other variables are also available # - $nodejs_path: The absolute path to node binaries for the chosen version. # - $nodejs_version: Just the version number of node for this app. Stored as 'nodejs_version' in settings.yml. # -# usage: ynh_use_nodejs -# # Requires YunoHost version 2.7.12 or higher. ynh_use_nodejs () { nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) @@ -97,10 +102,10 @@ ynh_use_nodejs () { # usage: ynh_install_nodejs --nodejs_version=nodejs_version # | arg: -n, --nodejs_version= - Version of node to install. When possible, your should prefer to use major version number (e.g. 8 instead of 8.10.0). The crontab will then handle the update of minor versions when needed. # -# n (Node version management) uses the PATH variable to store the path of the version of node it is going to use. +# `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 # -# Refer to ynh_use_nodejs for more information about available commands and variables +# Refer to `ynh_use_nodejs` for more information about available commands and variables # # Requires YunoHost version 2.7.12 or higher. ynh_install_nodejs () { @@ -177,12 +182,12 @@ ynh_install_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 # +# 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. +# # Requires YunoHost version 2.7.12 or higher. ynh_remove_nodejs () { nodejs_version=$(ynh_app_setting_get --app=$app --key=nodejs_version) From 706dbe723e9852943596de38c26202ac4367d36f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 315/363] helper doc fixes : php --- data/helpers.d/php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/helpers.d/php b/data/helpers.d/php index 5c050cc88..a590da3dd 100644 --- a/data/helpers.d/php +++ b/data/helpers.d/php @@ -569,6 +569,7 @@ YNH_COMPOSER_VERSION=${YNH_COMPOSER_VERSION:-$YNH_DEFAULT_COMPOSER_VERSION} # | arg: -v, --phpversion - PHP version to use with composer # | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. # | arg: -c, --commands - Commands to execute. +# ynh_composer_exec () { # Declare an array to define the options of this helper. local legacy_args=vwc @@ -593,6 +594,7 @@ ynh_composer_exec () { # | arg: -w, --workdir - The directory from where the command will be executed. Default $final_path. # | arg: -a, --install_args - Additional arguments provided to the composer install. Argument --no-dev already include # | arg: -c, --composerversion - Composer version to install +# ynh_install_composer () { # Declare an array to define the options of this helper. local legacy_args=vwac From dc27fd3ec3ab2a1c67102f023c4d1413723b4ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 316/363] helper doc fixes : postgresql --- data/helpers.d/postgresql | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/data/helpers.d/postgresql b/data/helpers.d/postgresql index f2f427842..12738a922 100644 --- a/data/helpers.d/postgresql +++ b/data/helpers.d/postgresql @@ -5,15 +5,15 @@ PSQL_VERSION=11 # Open a connection as a user # -# examples: -# ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;" -# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql -# # usage: ynh_psql_connect_as --user=user --password=password [--database=database] # | arg: -u, --user= - the user name to connect as # | arg: -p, --password= - the user password # | arg: -d, --database= - the database to connect to # +# examples: +# ynh_psql_connect_as 'user' 'pass' <<< "UPDATE ...;" +# ynh_psql_connect_as 'user' 'pass' < /path/to/file.sql +# # Requires YunoHost version 3.5.0 or higher. ynh_psql_connect_as() { # Declare an array to define the options of this helper. @@ -127,12 +127,12 @@ ynh_psql_drop_db() { # Dump a database # -# example: ynh_psql_dump_db 'roundcube' > ./dump.sql -# # usage: ynh_psql_dump_db --database=database # | arg: -d, --database= - the database name to dump # | ret: the psqldump output # +# example: ynh_psql_dump_db 'roundcube' > ./dump.sql +# # Requires YunoHost version 3.5.0 or higher. ynh_psql_dump_db() { # Declare an array to define the options of this helper. @@ -243,7 +243,7 @@ ynh_psql_setup_db() { local new_db_pwd=$(ynh_string_random) # Generate a random password # If $db_pwd is not provided, use new_db_pwd instead for db_pwd db_pwd="${db_pwd:-$new_db_pwd}" - + ynh_psql_create_user "$db_user" "$db_pwd" elif [ -z $db_pwd ]; then ynh_die --message="The user $db_user exists, please provide his password" @@ -286,11 +286,12 @@ ynh_psql_remove_db() { } # Create a master password and set up global settings -# It also make sure that postgresql is installed and running -# Please always call this script in install and restore scripts # # usage: ynh_psql_test_if_first_run # +# It also make sure that postgresql is installed and running +# Please always call this script in install and restore scripts +# # Requires YunoHost version 2.7.13 or higher. ynh_psql_test_if_first_run() { From 305a70a8d5a60fb10aad807a0af71c82e059afc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 317/363] helper doc fixes : setting --- data/helpers.d/setting | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/setting b/data/helpers.d/setting index a66e0d1ea..2950b3829 100644 --- a/data/helpers.d/setting +++ b/data/helpers.d/setting @@ -77,7 +77,7 @@ ynh_app_setting_delete() { # [internal] # ynh_app_setting() -{ +{ set +o xtrace # set +x ACTION="$1" APP="$2" KEY="$3" VALUE="${4:-}" python3 - < Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 318/363] helper doc fixes : string --- data/helpers.d/string | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index a0bcdbfaf..7036b3b3c 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -2,12 +2,12 @@ # Generate a random string # -# example: pwd=$(ynh_string_random --length=8) -# # usage: ynh_string_random [--length=string_length] # | arg: -l, --length= - the string length to generate (default: 24) # | ret: the generated string # +# example: pwd=$(ynh_string_random --length=8) +# # Requires YunoHost version 2.2.4 or higher. ynh_string_random() { # Declare an array to define the options of this helper. @@ -30,9 +30,8 @@ ynh_string_random() { # | arg: -r, --replace_string= - String that will replace matches # | arg: -f, --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) +# 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) # # Requires YunoHost version 2.6.4 or higher. ynh_replace_string () { @@ -86,14 +85,15 @@ ynh_replace_special_string () { } # Sanitize a string intended to be the name of a database -# (More specifically : replace - and . by _) -# -# example: dbname=$(ynh_sanitize_dbid $app) # # usage: ynh_sanitize_dbid --db_name=name # | arg: -n, --db_name= - name to correct/sanitize # | ret: the corrected name # +# example: dbname=$(ynh_sanitize_dbid $app) +# +# Underscorify the string (replace - and . by _) +# # Requires YunoHost version 2.2.4 or higher. ynh_sanitize_dbid () { # Declare an array to define the options of this helper. From f0d7eba64a9d087d5d44c3472014301937c408ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 319/363] helper doc fixes : systemd --- data/helpers.d/systemd | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index b43b593fa..16dee928a 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -3,11 +3,12 @@ # Create a dedicated systemd config # # usage: ynh_add_systemd_config [--service=service] [--template=template] -# | arg: -s, --service= - Service name (optionnal, $app by default) -# | arg: -t, --template= - Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template) +# | arg: -s, --service= - Service name (optionnal, `$app` by default) +# | arg: -t, --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 -# See the documentation of ynh_add_config for a description of the template +# This will use the template `../conf/.service`. +# +# See the documentation of `ynh_add_config` for a description of the template # format and how placeholders are replaced with actual variables. # # Requires YunoHost version 2.7.11 or higher. @@ -59,10 +60,10 @@ ynh_remove_systemd_config () { # Start (or other actions) a service, print a log in case of failure and optionnaly wait until the service is completely started # # usage: ynh_systemd_action [--service_name=service_name] [--action=action] [ [--line_match="line to match"] [--log_path=log_path] [--timeout=300] [--length=20] ] -# | arg: -n, --service_name= - Name of the service to start. Default : $app +# | arg: -n, --service_name= - Name of the service to start. Default : `$app` # | arg: -a, --action= - Action to perform with systemctl. Default: start # | arg: -l, --line_match= - Line to match - The line to find in the log to attest the service have finished to boot. If not defined it don't wait until the service is completely started. -# | arg: -p, --log_path= - Log file - Path to the log file. Default : /var/log/$app/$app.log +# | arg: -p, --log_path= - Log file - Path to the log file. Default : `/var/log/$app/$app.log` # | arg: -t, --timeout= - Timeout - The maximum time to wait before ending the watching. Default : 300 seconds. # | arg: -e, --length= - Length of the error log : Default : 20 # @@ -180,4 +181,3 @@ ynh_clean_check_starting () { ynh_secure_remove --file="$templog" 2>&1 fi } - From 36996482659bfd711a715a5403a0871ed7d7266e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 320/363] helper doc fixes : user --- data/helpers.d/user | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/data/helpers.d/user b/data/helpers.d/user index 4d2d9ad65..c12b4656e 100644 --- a/data/helpers.d/user +++ b/data/helpers.d/user @@ -2,11 +2,11 @@ # Check if a YunoHost user exists # -# example: ynh_user_exists 'toto' || exit 1 -# # usage: ynh_user_exists --username=username # | arg: -u, --username= - the username to check -# | exit: Return 1 if the user doesn't exist, 0 otherwise +# | ret: 0 if the user exists, 1 otherwise. +# +# example: ynh_user_exists 'toto' || echo "User does not exist" # # Requires YunoHost version 2.2.4 or higher. ynh_user_exists() { @@ -22,12 +22,12 @@ ynh_user_exists() { # Retrieve a YunoHost user information # -# example: mail=$(ynh_user_get_info 'toto' 'mail') -# # usage: ynh_user_get_info --username=username --key=key # | arg: -u, --username= - the username to retrieve info from # | arg: -k, --key= - the key to retrieve -# | ret: string - the key's value +# | ret: the value associate to that key +# +# example: mail=$(ynh_user_get_info 'toto' 'mail') # # Requires YunoHost version 2.2.4 or higher. ynh_user_get_info() { @@ -44,10 +44,10 @@ ynh_user_get_info() { # Get the list of YunoHost users # -# example: for u in $(ynh_user_list); do ... -# # usage: ynh_user_list -# | ret: string - one username per line +# | ret: one username per line as strings +# +# example: for u in $(ynh_user_list); do ... ; done # # Requires YunoHost version 2.4.0 or higher. ynh_user_list() { @@ -58,7 +58,7 @@ ynh_user_list() { # # usage: ynh_system_user_exists --username=username # | arg: -u, --username= - the username to check -# | exit: Return 1 if the user doesn't exist, 0 otherwise +# | ret: 0 if the user exists, 1 otherwise. # # Requires YunoHost version 2.2.4 or higher. ynh_system_user_exists() { @@ -76,7 +76,7 @@ ynh_system_user_exists() { # # usage: ynh_system_group_exists --group=group # | arg: -g, --group= - the group to check -# | exit: Return 1 if the group doesn't exist, 0 otherwise +# | ret: 0 if the group exists, 1 otherwise. # # Requires YunoHost version 3.5.0.2 or higher. ynh_system_group_exists() { @@ -92,17 +92,20 @@ ynh_system_group_exists() { # Create a system user # -# examples: -# # Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) -# ynh_system_user_create --username=nextcloud -# # Create a discourse user using /var/www/discourse as home directory and the default login shell -# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell -# # usage: ynh_system_user_create --username=user_name [--home_dir=home_dir] [--use_shell] # | arg: -u, --username= - Name of the system user that will be create # | arg: -h, --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 # | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell # +# Create a nextcloud user with no home directory and /usr/sbin/nologin login shell (hence no login capability) : +# ``` +# ynh_system_user_create --username=nextcloud +# ``` +# Create a discourse user using /var/www/discourse as home directory and the default login shell : +# ``` +# ynh_system_user_create --username=discourse --home_dir=/var/www/discourse --use_shell +# ``` +# # Requires YunoHost version 2.6.4 or higher. ynh_system_user_create () { # Declare an array to define the options of this helper. From 72da2ad8e3a40f2237f37af911eb069b14b356aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Pi=C3=A9dallu?= Date: Wed, 3 Mar 2021 10:59:29 +0100 Subject: [PATCH 321/363] helper doc fixes : utils --- data/helpers.d/utils | 135 ++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 487ec41db..28b1e5a1d 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -48,9 +48,8 @@ ynh_exit_properly () { # 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_clean_setup` is triggered if it has been -# defined by your script. +# or if an empty variable is used, the execution of the script stops immediately +# and a call to `ynh_clean_setup` is triggered if it has been defined by your script. # # Requires YunoHost version 2.6.4 or higher. ynh_abort_if_errors () { @@ -63,45 +62,37 @@ ynh_abort_if_errors () { # # usage: ynh_setup_source --dest_dir=dest_dir [--source_id=source_id] # | arg: -d, --dest_dir= - Directory where to setup sources -# | arg: -s, --source_id= - Name of the app, if the package contains more than one app +# | arg: -s, --source_id= - Name of the source, defaults to `app` # -# The file conf/app.src need to contains: +# This helper will read `conf/${source_id}.src`, download and install the sources. # +# The src file need to contains: +# ``` # SOURCE_URL=Address to download the app archive # SOURCE_SUM=Control sum -# # (Optional) Program to check the integrity (sha256sum, md5sum...) -# # default: sha256 +# # (Optional) Program to check the integrity (sha256sum, md5sum...). Default: sha256 # SOURCE_SUM_PRG=sha256 -# # (Optional) Archive format -# # default: tar.gz +# # (Optional) Archive format. Default: tar.gz # SOURCE_FORMAT=tar.gz -# # (Optional) Put false if sources are directly in the archive root -# # default: true -# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories -# # to remove. +# # (Optional) Put false if sources are directly in the archive root. Default: true +# # Instead of true, SOURCE_IN_SUBDIR could be the number of sub directories to remove. # SOURCE_IN_SUBDIR=false -# # (Optionnal) Name of the local archive (offline setup support) -# # default: ${src_id}.${src_format} +# # (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. +# # (Optional) If it set as false don't extract the source. Default: true # # (Useful to get a debian package or a python wheel.) -# # default: true # SOURCE_EXTRACT=(true|false) +# ``` # -# 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. -# If SOURCE_IN_SUBDIR is a numeric value, 2 for example, the 2 first level -# directories will be removed -# -# Finally, patches named sources/patches/${src_id}-*.patch and extra files in -# sources/extra_files/$src_id will be applied to dest_dir +# The helper will: +# - Check if there is a local source archive in `/opt/yunohost-apps-src/$APP_ID/$SOURCE_FILENAME` +# - Download `$SOURCE_URL` if there is no local archive +# - Check the integrity with `$SOURCE_SUM_PRG -c --status` +# - Uncompress the archive to `$dest_dir`. +# - If `$SOURCE_IN_SUBDIR` is true, the first level directory of the archive will be removed. +# - If `$SOURCE_IN_SUBDIR` is a numeric value, the N first level directories will be removed. +# - Patches named `sources/patches/${src_id}-*.patch` will be applied to `$dest_dir` +# - Extra files in `sources/extra_files/$src_id` will be copied to dest_dir # # Requires YunoHost version 2.6.4 or higher. ynh_setup_source () { @@ -211,17 +202,17 @@ ynh_setup_source () { # Curl abstraction to help with POST requests to local pages (such as installation forms) # -# 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: 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 # +# example: ynh_local_curl "/install.php?installButton" "foo=$var1" "bar=$var2" +# # For multiple calls, cookies are persisted between each call for the same app # -# $domain and $path_url should be defined externally (and correspond to the domain.tld and the /path (of the app?)) +# `$domain` and `$path_url` should be defined externally (and correspond to the domain.tld and the /path (of the app?)) # # Requires YunoHost version 2.6.4 or higher. ynh_local_curl () { @@ -250,7 +241,7 @@ ynh_local_curl () { # Wait untils nginx has fully reloaded (avoid curl fail with http2) sleep 2 - + local cookiefile=/tmp/ynh-$app-cookie.txt touch $cookiefile chown root $cookiefile @@ -262,20 +253,22 @@ ynh_local_curl () { # Create a dedicated config file from a template # +# usage: ynh_add_config --template="template" --destination="destination" +# | arg: -t, --template= - Template config file to use +# | arg: -d, --destination= - Destination of the config file +# # examples: # ynh_add_config --template=".env" --destination="$final_path/.env" # ynh_add_config --template="../conf/.env" --destination="$final_path/.env" # ynh_add_config --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf" # -# usage: ynh_add_config --template="template" --destination="destination" -# | arg: -t, --template= - Template config file to use -# | arg: -d, --destination= - Destination of the config file -# # The template can be by default the name of a file in the conf directory -# of a YunoHost Package, a relative path or an absolute path -# The helper will use the template $template to generate a config file -# $destination by replacing the following keywords with global variables +# of a YunoHost Package, a relative path or an absolute path. +# +# The helper will use the template `template` to generate a config file +# `destination` by replacing the following keywords with global variables # that should be defined before calling this helper : +# ``` # __PATH__ by $path_url # __NAME__ by $app # __NAMETOCHANGE__ by $app @@ -283,15 +276,18 @@ ynh_local_curl () { # __FINALPATH__ by $final_path # __PHPVERSION__ by $YNH_PHP_VERSION # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH -# +# ``` # And any dynamic variables that should be defined before calling this helper like: +# ``` # __DOMAIN__ by $domain # __APP__ by $app # __VAR_1__ by $var_1 # __VAR_2__ by $var_2 +# ``` # # The helper will verify the checksum and backup the destination file # if it's different before applying the new template. +# # And it will calculate and store the destination file checksum # into the app settings when configuration is done. # @@ -556,16 +552,17 @@ ynh_read_manifest () { jq ".$manifest_key" "$manifest" --raw-output } -# Read the upstream version from the manifest, or from the env variable $YNH_APP_MANIFEST_VERSION if not given +# Read the upstream version from the manifest or `$YNH_APP_MANIFEST_VERSION` # # usage: ynh_app_upstream_version [--manifest="manifest.json"] # | arg: -m, --manifest= - Path of the manifest to read # | ret: the version number of the upstream app # -# The version number in the manifest is defined by ~ynh -# For example : 4.3-2~ynh3 -# This include the number before ~ynh -# In the last example it return 4.3-2 +# If the `manifest` is not specified, the envvar `$YNH_APP_MANIFEST_VERSION` will be used. +# +# The version number in the manifest is defined by `~ynh`. +# +# For example, if the manifest contains `4.3-2~ynh3` the function will return `4.3-2` # # Requires YunoHost version 3.5.0 or higher. ynh_app_upstream_version () { @@ -593,10 +590,9 @@ ynh_app_upstream_version () { # | arg: -m, --manifest= - Path of the manifest to read # | ret: the version number of the package # -# The version number in the manifest is defined by ~ynh -# For example : 4.3-2~ynh3 -# This include the number after ~ynh -# In the last example it return 3 +# The version number in the manifest is defined by `~ynh`. +# +# For example, if the manifest contains `4.3-2~ynh3` the function will return `3` # # Requires YunoHost version 3.5.0 or higher. ynh_app_package_version () { @@ -613,18 +609,16 @@ ynh_app_package_version () { # Checks the app version to upgrade with the existing app version and returns: # -# - UPGRADE_PACKAGE if only the YunoHost package has changed -# - UPGRADE_APP otherwise +# usage: ynh_check_app_version_changed +# | ret: `UPGRADE_APP` if the upstream version changed, `UPGRADE_PACKAGE` otherwise. # # This helper should be used to avoid an upgrade of an app, or the upstream part # of it, when it's not needed # -# To force an upgrade, even if the package is up to date, -# you have to use the parameter --force (or -F). -# example: sudo yunohost app upgrade MyApp --force -# -# usage: ynh_check_app_version_changed -# +# You can force an upgrade, even if the package is up to date, with the `--force` (or `-F`) argument : +# ``` +# sudo yunohost app upgrade --force +# ``` # Requires YunoHost version 3.5.0 or higher. ynh_check_app_version_changed () { local return_value=${YNH_APP_UPGRADE_TYPE} @@ -638,24 +632,23 @@ ynh_check_app_version_changed () { } # Compare the current package version against another version given as an argument. -# This is really useful when we need to do some actions only for some old package versions. +# +# usage: ynh_compare_current_package_version --comparison (lt|le|eq|ne|ge|gt) --version +# | arg: --comparison - Comparison type. Could be : `lt` (lower than), `le` (lower or equal), `eq` (equal), `ne` (not equal), `ge` (greater or equal), `gt` (greater than) +# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like `2.3.1~ynh4`) +# | ret: 0 if the evaluation is true, 1 if false. # # example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 -# This example will check if the installed version is lower than (lt) the version 2.3.2~ynh1 # -# Generally you might probably use it as follow in the upgrade script +# This helper is usually used when we need to do some actions only for some old package versions. # +# Generally you might probably use it as follow in the upgrade script : +# ``` # if ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1 # then # # Do something that is needed for the package version older than 2.3.2~ynh1 # fi -# -# usage: ynh_compare_current_package_version --comparison lt|le|eq|ne|ge|gt -# | arg: --comparison - Comparison type. Could be : lt (lower than), le (lower or equal), -# | eq (equal), ne (not equal), ge (greater or equal), gt (greater than) -# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like 2.3.1~ynh4) -# -# Return 0 if the evaluation is true. 1 if false. +# ``` # # Requires YunoHost version 3.8.0 or higher. ynh_compare_current_package_version() { From 5539751ca4bfadb2dfc3a1864a81736d28017c54 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 8 Mar 2021 17:38:10 +0100 Subject: [PATCH 322/363] fix others_var --- data/helpers.d/fail2ban | 1 + data/helpers.d/systemd | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/fail2ban b/data/helpers.d/fail2ban index f8b2b1761..aa2c8d3c0 100644 --- a/data/helpers.d/fail2ban +++ b/data/helpers.d/fail2ban @@ -73,6 +73,7 @@ ynh_add_fail2ban_config () { ynh_handle_getopts_args "$@" max_retry=${max_retry:-3} ports=${ports:-http,https} + others_var="${others_var:-}" use_template="${use_template:-0}" [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2" diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index b43b593fa..4c7bd31d1 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -20,8 +20,9 @@ ynh_add_systemd_config () { local others_var # Manage arguments with getopts ynh_handle_getopts_args "$@" - local service="${service:-$app}" - local template="${template:-systemd.service}" + service="${service:-$app}" + template="${template:-systemd.service}" + others_var="${others_var:-}" [[ -z "$others_var" ]] || ynh_print_warn --message="Packagers: using --others_var is unecessary since Yunohost 4.2" From 9e7eda681bcf82fac70a0b6cb481c5fbda3b689c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 8 Mar 2021 18:37:53 +0100 Subject: [PATCH 323/363] ynh_install_n: put the source descriptor file in the appropriate dir during backup/restore --- data/helpers.d/nodejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index 2e1c787cf..200e5ecbd 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -18,7 +18,7 @@ ynh_install_n () { # Build an app.src for n mkdir --parents "../conf" echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > "../conf/n.src" +SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > "$YNH_APP_BASEDIR/conf/n.src" # Download and extract n ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n # Install n From db93b82b23d0470a8dcde3393e67002e2d975fd8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 8 Mar 2021 18:41:54 +0100 Subject: [PATCH 324/363] ynh_setup_source: add a check that we could actually parse SOURCE_URL --- 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 487ec41db..8246b9986 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -135,12 +135,15 @@ ynh_setup_source () { if [ "$src_filename" = "" ]; then src_filename="${source_id}.${src_format}" fi - local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" + # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... + 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 + then cp $local_src $src_filename - else # If not, download the source + else + [ -n "$src_url" ] || ynh_die "Couldn't parse SOURCE_URL from $src_file_path ?" + # NB. we have to declare the var as local first, # otherwise 'local foo=$(false) || echo 'pwet'" does'nt work # because local always return 0 ... From ad602ee0a13d59bb3c79c289e34ab294e4bb26f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Gaspar?= <46165813+ericgaspar@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:55:45 +0100 Subject: [PATCH 325/363] Upgrade n (#1178) Co-authored-by: Alexandre Aubin --- data/helpers.d/nodejs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index 200e5ecbd..d4122c1c6 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -1,6 +1,6 @@ #!/bin/bash -n_version=7.0.0 +n_version=7.0.2 n_install_dir="/opt/node_n" 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. @@ -18,7 +18,7 @@ ynh_install_n () { # Build an app.src for n mkdir --parents "../conf" echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz -SOURCE_SUM=2933855140f980fc6d1d6103ea07cd4d915b17dea5e17e43921330ea89978b5b" > "$YNH_APP_BASEDIR/conf/n.src" +SOURCE_SUM=fa80a8685f0fb1b4187fc0a1228b44f0ea2f244e063fe8f443b8913ea595af89" > "$YNH_APP_BASEDIR/conf/n.src" # Download and extract n ynh_setup_source --dest_dir="$n_install_dir/git" --source_id=n # Install n From b85d959d7e0dd6f287d9121dba5adc9bd2caa490 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 8 Mar 2021 20:56:32 +0100 Subject: [PATCH 326/363] ynh_install_n: No need to mkdir ../conf --- data/helpers.d/nodejs | 1 - 1 file changed, 1 deletion(-) diff --git a/data/helpers.d/nodejs b/data/helpers.d/nodejs index d4122c1c6..c2f374a56 100644 --- a/data/helpers.d/nodejs +++ b/data/helpers.d/nodejs @@ -16,7 +16,6 @@ export N_PREFIX="$n_install_dir" ynh_install_n () { ynh_print_info --message="Installation of N - Node.js version management" # Build an app.src for n - mkdir --parents "../conf" echo "SOURCE_URL=https://github.com/tj/n/archive/v${n_version}.tar.gz SOURCE_SUM=fa80a8685f0fb1b4187fc0a1228b44f0ea2f244e063fe8f443b8913ea595af89" > "$YNH_APP_BASEDIR/conf/n.src" # Download and extract n From 01ccab52529a55e8d40fe3676f2d7023c112aef1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 11 Mar 2021 01:39:52 +0100 Subject: [PATCH 327/363] Add semantic of YunohostValidationError for all exceptions which are related to validating stuff --- src/yunohost/app.py | 74 +++++++++---------- src/yunohost/backup.py | 28 +++---- src/yunohost/certificate.py | 24 +++--- .../0017_postgresql_9p6_to_11.py | 4 +- src/yunohost/diagnosis.py | 18 ++--- src/yunohost/domain.py | 29 ++++---- src/yunohost/dyndns.py | 12 +-- src/yunohost/firewall.py | 4 +- src/yunohost/hook.py | 8 +- src/yunohost/log.py | 2 +- src/yunohost/permission.py | 24 +++--- src/yunohost/service.py | 12 +-- src/yunohost/settings.py | 22 +++--- src/yunohost/ssh.py | 6 +- src/yunohost/tools.py | 36 ++++----- src/yunohost/user.py | 52 ++++++------- src/yunohost/utils/error.py | 4 + src/yunohost/utils/password.py | 4 +- tests/test_i18n_keys.py | 2 + 19 files changed, 187 insertions(+), 178 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3d1d16f3c..613ca21df 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -54,7 +54,7 @@ from moulinette.utils.filesystem import ( from yunohost.service import service_status, _run_service_command from yunohost.utils import packages -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger logger = getActionLogger("yunohost.app") @@ -192,7 +192,7 @@ def app_info(app, full=False): from yunohost.permission import user_permission_list if not _is_installed(app): - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) @@ -321,7 +321,7 @@ def app_map(app=None, raw=False, user=None): if app is not None: if not _is_installed(app): - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) apps = [ @@ -421,14 +421,14 @@ def app_change_url(operation_logger, app, domain, path): installed = _is_installed(app) if not installed: - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) if not os.path.exists( os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url") ): - raise YunohostError("app_change_url_no_script", app_name=app) + raise YunohostValidationError("app_change_url_no_script", app_name=app) old_domain = app_setting(app, "domain") old_path = app_setting(app, "path") @@ -438,7 +438,7 @@ def app_change_url(operation_logger, app, domain, path): domain, path = _normalize_domain_path(domain, path) if (domain, path) == (old_domain, old_path): - raise YunohostError( + raise YunohostValidationError( "app_change_url_identical_domains", domain=domain, path=path ) @@ -551,12 +551,12 @@ def app_upgrade(app=[], url=None, file=None, force=False): # Abort if any of those app is in fact not installed.. for app in [app_ for app_ in apps if not _is_installed(app_)]: - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) if len(apps) == 0: - raise YunohostError("apps_already_up_to_date") + raise YunohostValidationError("apps_already_up_to_date") if len(apps) > 1: logger.info(m18n.n("app_upgrade_several_apps", apps=", ".join(apps))) @@ -880,11 +880,11 @@ def app_install( confirm_install("thirdparty") manifest, extracted_app_folder = _extract_app_from_file(app) else: - raise YunohostError("app_unknown") + raise YunohostValidationError("app_unknown") # Check ID if "id" not in manifest or "__" in manifest["id"]: - raise YunohostError("app_id_invalid") + raise YunohostValidationError("app_id_invalid") app_id = manifest["id"] label = label if label else manifest["name"] @@ -897,7 +897,7 @@ def app_install( instance_number = _installed_instance_number(app_id, last=True) + 1 if instance_number > 1: if "multi_instance" not in manifest or not is_true(manifest["multi_instance"]): - raise YunohostError("app_already_installed", app=app_id) + raise YunohostValidationError("app_already_installed", app=app_id) # Change app_id to the forked app id app_instance_name = app_id + "__" + str(instance_number) @@ -1209,7 +1209,7 @@ def app_remove(operation_logger, app): ) if not _is_installed(app): - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) @@ -1372,10 +1372,10 @@ def app_makedefault(operation_logger, app, domain=None): domain = app_domain operation_logger.related_to.append(("domain", domain)) elif domain not in domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) if "/" in app_map(raw=True)[domain]: - raise YunohostError( + raise YunohostValidationError( "app_make_default_location_already_used", app=app, domain=app_domain, @@ -1578,7 +1578,7 @@ def app_register_url(app, domain, path): if _is_installed(app): settings = _get_app_settings(app) if "path" in settings.keys() and "domain" in settings.keys(): - raise YunohostError("app_already_installed_cant_change_url") + raise YunohostValidationError("app_already_installed_cant_change_url") # Check the url is available _assert_no_conflicting_apps(domain, path) @@ -1694,7 +1694,7 @@ def app_change_label(app, new_label): installed = _is_installed(app) if not installed: - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) logger.warning(m18n.n("app_label_deprecated")) @@ -1730,7 +1730,7 @@ def app_action_run(operation_logger, app, action, args=None): actions = {x["id"]: x for x in actions} if action not in actions: - raise YunohostError( + raise YunohostValidationError( "action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys())), raw_msg=True, @@ -1884,7 +1884,7 @@ def app_config_apply(operation_logger, app, args): installed = _is_installed(app) if not installed: - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app, all_apps=_get_all_installed_apps_id() ) @@ -2199,7 +2199,7 @@ def _get_app_settings(app_id): """ if not _is_installed(app_id): - raise YunohostError( + raise YunohostValidationError( "app_not_installed", app=app_id, all_apps=_get_all_installed_apps_id() ) try: @@ -2546,9 +2546,9 @@ def _fetch_app_from_git(app): app_id, _ = _parse_app_instance_name(app) if app_id not in app_dict: - raise YunohostError("app_unknown") + raise YunohostValidationError("app_unknown") elif "git" not in app_dict[app_id]: - raise YunohostError("app_unsupported_remote_type") + raise YunohostValidationError("app_unsupported_remote_type") app_info = app_dict[app_id] url = app_info["git"]["url"] @@ -2684,7 +2684,7 @@ def _check_manifest_requirements(manifest, app_instance_name): packaging_format = int(manifest.get("packaging_format", 0)) if packaging_format not in [0, 1]: - raise YunohostError("app_packaging_format_not_supported") + raise YunohostValidationError("app_packaging_format_not_supported") requirements = manifest.get("requirements", dict()) @@ -2697,7 +2697,7 @@ def _check_manifest_requirements(manifest, app_instance_name): for pkgname, spec in requirements.items(): if not packages.meets_version_specifier(pkgname, spec): version = packages.ynh_packages_version()[pkgname]["version"] - raise YunohostError( + raise YunohostValidationError( "app_requirements_unmeet", pkgname=pkgname, version=version, @@ -2796,7 +2796,7 @@ class YunoHostArgumentFormatParser(object): # we don't have an answer, check optional and default_value if question.value is None or question.value == "": if not question.optional and question.default is None: - raise YunohostError("app_argument_required", name=question.name) + raise YunohostValidationError("app_argument_required", name=question.name) else: question.value = ( getattr(self, "default_value", None) @@ -2816,7 +2816,7 @@ class YunoHostArgumentFormatParser(object): return (question.value, self.argument_type) def _raise_invalid_answer(self, question): - raise YunohostError( + raise YunohostValidationError( "app_argument_choice_invalid", name=question.name, choices=", ".join(question.choices), @@ -2854,13 +2854,13 @@ class PasswordArgumentParser(YunoHostArgumentFormatParser): ) if question.default is not None: - raise YunohostError("app_argument_password_no_default", name=question.name) + raise YunohostValidationError("app_argument_password_no_default", name=question.name) return question def _post_parse_value(self, question): if any(char in question.value for char in self.forbidden_chars): - raise YunohostError( + raise YunohostValidationError( "pattern_password_app", forbidden_chars=self.forbidden_chars ) @@ -2913,7 +2913,7 @@ class BooleanArgumentParser(YunoHostArgumentFormatParser): if str(question.value).lower() in ["0", "no", "n", "false"]: return 0 - raise YunohostError( + raise YunohostValidationError( "app_argument_choice_invalid", name=question.name, choices="yes, no, y, n, 1, 0", @@ -2938,7 +2938,7 @@ class DomainArgumentParser(YunoHostArgumentFormatParser): return question def _raise_invalid_answer(self, question): - raise YunohostError( + raise YunohostValidationError( "app_argument_invalid", name=question.name, error=m18n.n("domain_unknown") ) @@ -2964,7 +2964,7 @@ class UserArgumentParser(YunoHostArgumentFormatParser): return question def _raise_invalid_answer(self, question): - raise YunohostError( + raise YunohostValidationError( "app_argument_invalid", name=question.name, error=m18n.n("user_unknown", user=question.value), @@ -2992,7 +2992,7 @@ class NumberArgumentParser(YunoHostArgumentFormatParser): if isinstance(question.value, str) and question.value.isdigit(): return int(question.value) - raise YunohostError( + raise YunohostValidationError( "app_argument_invalid", name=question.name, error=m18n.n("invalid_number") ) @@ -3123,7 +3123,7 @@ def _get_conflicting_apps(domain, path, ignore_app=None): # Abort if domain is unknown if domain not in domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) # Fetch apps map apps_map = app_map(raw=True) @@ -3162,9 +3162,9 @@ def _assert_no_conflicting_apps(domain, path, ignore_app=None, full_domain=False ) if full_domain: - raise YunohostError("app_full_domain_unavailable", domain=domain) + raise YunohostValidationError("app_full_domain_unavailable", domain=domain) else: - raise YunohostError("app_location_unavailable", apps="\n".join(apps)) + raise YunohostValidationError("app_location_unavailable", apps="\n".join(apps)) def _make_environment_for_app_script(app, args={}, args_prefix="APP_ARG_"): @@ -3469,7 +3469,7 @@ def _assert_system_is_sane_for_app(manifest, when): faulty_services = [s for s in services if service_status(s)["status"] != "running"] if faulty_services: if when == "pre": - raise YunohostError( + raise YunohostValidationError( "app_action_cannot_be_ran_because_required_services_down", services=", ".join(faulty_services), ) @@ -3480,7 +3480,7 @@ def _assert_system_is_sane_for_app(manifest, when): if packages.dpkg_is_broken(): if when == "pre": - raise YunohostError("dpkg_is_broken") + raise YunohostValidationError("dpkg_is_broken") elif when == "post": raise YunohostError("this_action_broke_dpkg") @@ -3659,7 +3659,7 @@ def _patch_legacy_helpers(app_folder): # couldn't patch the deprecated helper in the previous lines. In # that case, abort the install or whichever step is performed if helper in content and infos["important"]: - raise YunohostError( + raise YunohostValidationError( "This app is likely pretty old and uses deprecated / outdated helpers that can't be migrated easily. It can't be installed anymore.", raw_msg=True, ) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 408cd6f15..b020c0f34 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -348,7 +348,7 @@ class BackupManager: # Try to recursively unmount stuff (from a previously failed backup ?) if not _recursive_umount(self.work_dir): - raise YunohostError("backup_output_directory_not_empty") + raise YunohostValidationError("backup_output_directory_not_empty") else: # If umount succeeded, remove the directory (we checked that # we're in /home/yunohost.backup/tmp so that should be okay... @@ -1027,7 +1027,7 @@ class RestoreManager: already_installed = [app for app in to_be_restored if _is_installed(app)] if already_installed != []: if already_installed == to_be_restored: - raise YunohostError( + raise YunohostValidationError( "restore_already_installed_apps", apps=", ".join(already_installed) ) else: @@ -1133,14 +1133,14 @@ class RestoreManager: return True elif free_space > needed_space: # TODO Add --force options to avoid the error raising - raise YunohostError( + raise YunohostValidationError( "restore_may_be_not_enough_disk_space", free_space=free_space, needed_space=needed_space, margin=margin, ) else: - raise YunohostError( + raise YunohostValidationError( "restore_not_enough_disk_space", free_space=free_space, needed_space=needed_space, @@ -1729,7 +1729,7 @@ class BackupMethod(object): free_space, backup_size, ) - raise YunohostError("not_enough_disk_space", path=self.repo) + raise YunohostValidationError("not_enough_disk_space", path=self.repo) def _organize_files(self): """ @@ -2186,7 +2186,7 @@ def backup_create( # Validate there is no archive with the same name if name and name in backup_list()["archives"]: - raise YunohostError("backup_archive_name_exists") + raise YunohostValidationError("backup_archive_name_exists") # By default we backup using the tar method if not methods: @@ -2201,14 +2201,14 @@ def backup_create( r"^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$", output_directory, ): - raise YunohostError("backup_output_directory_forbidden") + raise YunohostValidationError("backup_output_directory_forbidden") if "copy" in methods: if not output_directory: - raise YunohostError("backup_output_directory_required") + raise YunohostValidationError("backup_output_directory_required") # Check that output directory is empty elif os.path.isdir(output_directory) and os.listdir(output_directory): - raise YunohostError("backup_output_directory_not_empty") + raise YunohostValidationError("backup_output_directory_not_empty") # If no --system or --apps given, backup everything if system is None and apps is None: @@ -2381,7 +2381,7 @@ def backup_download(name): if not os.path.lexists(archive_file): archive_file += ".gz" if not os.path.lexists(archive_file): - raise YunohostError("backup_archive_name_unknown", name=name) + raise YunohostValidationError("backup_archive_name_unknown", name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2389,7 +2389,7 @@ def backup_download(name): # Raise exception if link is broken (e.g. on unmounted external storage) if not os.path.exists(archive_file): - raise YunohostError("backup_archive_broken_link", path=archive_file) + raise YunohostValidationError("backup_archive_broken_link", path=archive_file) # We return a raw bottle HTTPresponse (instead of serializable data like # list/dict, ...), which is gonna be picked and used directly by moulinette @@ -2415,7 +2415,7 @@ def backup_info(name, with_details=False, human_readable=False): if not os.path.lexists(archive_file): archive_file += ".gz" if not os.path.lexists(archive_file): - raise YunohostError("backup_archive_name_unknown", name=name) + raise YunohostValidationError("backup_archive_name_unknown", name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2423,7 +2423,7 @@ 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 YunohostError("backup_archive_broken_link", path=archive_file) + raise YunohostValidationError("backup_archive_broken_link", path=archive_file) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) @@ -2531,7 +2531,7 @@ def backup_delete(name): """ if name not in backup_list()["archives"]: - raise YunohostError("backup_archive_name_unknown", name=name) + raise YunohostValidationError("backup_archive_name_unknown", name=name) hook_callback("pre_backup_delete", args=[name]) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c48af2c07..56ea70a04 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -37,7 +37,7 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.network import get_public_ip from yunohost.diagnosis import Diagnoser @@ -90,7 +90,7 @@ def certificate_status(domain_list, full=False): for domain in domain_list: # Is it in Yunohost domain list? if domain not in yunohost_domains_list: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) certificates = {} @@ -166,7 +166,7 @@ def _certificate_install_selfsigned(domain_list, force=False): status = _get_status(domain) if status["summary"]["code"] in ("good", "great"): - raise YunohostError( + raise YunohostValidationError( "certmanager_attempt_to_replace_valid_cert", domain=domain ) @@ -267,12 +267,12 @@ def _certificate_install_letsencrypt( for domain in domain_list: yunohost_domains_list = yunohost.domain.domain_list()["domains"] if domain not in yunohost_domains_list: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise YunohostError( + raise YunohostValidationError( "certmanager_domain_cert_not_selfsigned", domain=domain ) @@ -370,25 +370,25 @@ def certificate_renew( # Is it in Yunohost dmomain list? if domain not in yunohost.domain.domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) status = _get_status(domain) # Does it expire soon? if status["validity"] > VALIDITY_LIMIT and not force: - raise YunohostError( + raise YunohostValidationError( "certmanager_attempt_to_renew_valid_cert", domain=domain ) # Does it have a Let's Encrypt cert? if status["CA_type"]["code"] != "lets-encrypt": - raise YunohostError( + raise YunohostValidationError( "certmanager_attempt_to_renew_nonLE_cert", domain=domain ) # Check ACME challenge configured for given domain if not _check_acme_challenge_configuration(domain): - raise YunohostError( + raise YunohostValidationError( "certmanager_acme_not_configured_for_domain", domain=domain ) @@ -898,20 +898,20 @@ def _check_domain_is_ready_for_ACME(domain): ) if not dnsrecords or not httpreachable: - raise YunohostError("certmanager_domain_not_diagnosed_yet", domain=domain) + raise YunohostValidationError("certmanager_domain_not_diagnosed_yet", domain=domain) # Check if IP from DNS matches public IP if not dnsrecords.get("status") in [ "SUCCESS", "WARNING", ]: # Warning is for missing IPv6 record which ain't critical for ACME - raise YunohostError( + raise YunohostValidationError( "certmanager_domain_dns_ip_differs_from_public_ip", domain=domain ) # Check if domain seems to be accessible through HTTP? if not httpreachable.get("status") == "SUCCESS": - raise YunohostError("certmanager_domain_http_not_working", domain=domain) + raise YunohostValidationError("certmanager_domain_http_not_working", domain=domain) # FIXME / TODO : ideally this should not be needed. There should be a proper diff --git a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py index 728ae443f..0526c025d 100644 --- a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py +++ b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py @@ -23,7 +23,7 @@ class MyMigration(Migration): return if not self.package_is_installed("postgresql-11"): - raise YunohostError("migration_0017_postgresql_11_not_installed") + raise YunohostValidationError("migration_0017_postgresql_11_not_installed") # Make sure there's a 9.6 cluster try: @@ -37,7 +37,7 @@ class MyMigration(Migration): if not space_used_by_directory( "/var/lib/postgresql/9.6" ) > free_space_in_directory("/var/lib/postgresql"): - raise YunohostError( + raise YunohostValidationError( "migration_0017_not_enough_space", path="/var/lib/postgresql/" ) diff --git a/src/yunohost/diagnosis.py b/src/yunohost/diagnosis.py index d01d56613..cc0035755 100644 --- a/src/yunohost/diagnosis.py +++ b/src/yunohost/diagnosis.py @@ -37,7 +37,7 @@ from moulinette.utils.filesystem import ( write_to_yaml, ) -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.hook import hook_list, hook_exec logger = log.getActionLogger("yunohost.diagnosis") @@ -59,11 +59,11 @@ def diagnosis_get(category, item): all_categories_names = [c for c, _ in all_categories] if category not in all_categories_names: - raise YunohostError("diagnosis_unknown_categories", categories=category) + raise YunohostValidationError("diagnosis_unknown_categories", categories=category) if isinstance(item, list): if any("=" not in criteria for criteria in item): - raise YunohostError( + raise YunohostValidationError( "Criterias should be of the form key=value (e.g. domain=yolo.test)" ) @@ -91,7 +91,7 @@ def diagnosis_show( else: unknown_categories = [c for c in categories if c not in all_categories_names] if unknown_categories: - raise YunohostError( + raise YunohostValidationError( "diagnosis_unknown_categories", categories=", ".join(unknown_categories) ) @@ -181,7 +181,7 @@ def diagnosis_run( else: unknown_categories = [c for c in categories if c not in all_categories_names] if unknown_categories: - raise YunohostError( + raise YunohostValidationError( "diagnosis_unknown_categories", categories=", ".join(unknown_categories) ) @@ -270,14 +270,14 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False): # Sanity checks for the provided arguments if len(filter_) == 0: - raise YunohostError( + raise YunohostValidationError( "You should provide at least one criteria being the diagnosis category to ignore" ) category = filter_[0] if category not in all_categories_names: - raise YunohostError("%s is not a diagnosis category" % category) + raise YunohostValidationError("%s is not a diagnosis category" % category) if any("=" not in criteria for criteria in filter_[1:]): - raise YunohostError( + raise YunohostValidationError( "Criterias should be of the form key=value (e.g. domain=yolo.test)" ) @@ -331,7 +331,7 @@ def diagnosis_ignore(add_filter=None, remove_filter=None, list=False): configuration["ignore_filters"][category] = [] if criterias not in configuration["ignore_filters"][category]: - raise YunohostError("This filter does not exists.") + raise YunohostValidationError("This filter does not exists.") configuration["ignore_filters"][category].remove(criterias) _diagnosis_write_configuration(configuration) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 1198ef473..fdf247f89 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -101,16 +101,14 @@ def domain_add(operation_logger, domain, dyndns=False): from yunohost.utils.ldap import _get_ldap_interface if domain.startswith("xmpp-upload."): - raise YunohostError("domain_cannot_add_xmpp_upload") + raise YunohostValidationError("domain_cannot_add_xmpp_upload") ldap = _get_ldap_interface() try: ldap.validate_uniqueness({"virtualdomain": domain}) except MoulinetteError: - raise YunohostError("domain_exists") - - operation_logger.start() + raise YunohostValidationError("domain_exists") # Lower domain to avoid some edge cases issues # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 @@ -119,17 +117,21 @@ def domain_add(operation_logger, domain, dyndns=False): # DynDNS domain if dyndns: - from yunohost.dyndns import dyndns_subscribe, _dyndns_provides, _guess_current_dyndns_domain + from yunohost.dyndns import _dyndns_provides, _guess_current_dyndns_domain # Do not allow to subscribe to multiple dyndns domains... if _guess_current_dyndns_domain("dyndns.yunohost.org") != (None, None): - raise YunohostError('domain_dyndns_already_subscribed') + raise YunohostValidationError('domain_dyndns_already_subscribed') # 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 YunohostError("domain_dyndns_root_unknown") + raise YunohostValidationError("domain_dyndns_root_unknown") + operation_logger.start() + + if dyndns: + from yunohost.dyndns import dndns_subscribe # Actually subscribe dyndns_subscribe(domain=domain) @@ -197,7 +199,7 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): # we don't want to check the domain exists because the ldap add may have # failed if not force and domain not in domain_list()['domains']: - raise YunohostError('domain_name_unknown', domain=domain) + raise YunohostValidationError('domain_name_unknown', domain=domain) # Check domain is not the main domain if domain == _get_maindomain(): @@ -205,13 +207,13 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): other_domains.remove(domain) if other_domains: - raise YunohostError( + raise YunohostValidationError( "domain_cannot_remove_main", domain=domain, other_domains="\n * " + ("\n * ".join(other_domains)), ) else: - raise YunohostError("domain_cannot_remove_main_add_new_one", domain=domain) + raise YunohostValidationError("domain_cannot_remove_main_add_new_one", domain=domain) # Check if apps are installed on the domain apps_on_that_domain = [] @@ -234,9 +236,10 @@ def domain_remove(operation_logger, domain, remove_apps=False, force=False): for app, _ in apps_on_that_domain: app_remove(app) else: - raise YunohostError('domain_uninstall_app_first', apps="\n".join([x[1] for x in apps_on_that_domain])) + raise YunohostValidationError('domain_uninstall_app_first', apps="\n".join([x[1] for x in apps_on_that_domain])) operation_logger.start() + ldap = _get_ldap_interface() try: ldap.remove("virtualdomain=" + domain + ",ou=domains") @@ -288,7 +291,7 @@ def domain_dns_conf(domain, ttl=None): """ if domain not in domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) ttl = 3600 if ttl is None else ttl @@ -345,7 +348,7 @@ def domain_main_domain(operation_logger, new_main_domain=None): # Check domain exists if new_main_domain not in domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=new_main_domain) + raise YunohostValidationError("domain_name_unknown", domain=new_main_domain) operation_logger.related_to.append(("domain", new_main_domain)) operation_logger.start() diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index a921cfb5c..b2ac3de6d 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -36,7 +36,7 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file, read_file from moulinette.utils.network import download_json -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip, dig from yunohost.log import is_unit_operation @@ -124,7 +124,7 @@ def dyndns_subscribe( """ if _guess_current_dyndns_domain(subscribe_host) != (None, None): - raise YunohostError('domain_dyndns_already_subscribed') + raise YunohostValidationError('domain_dyndns_already_subscribed') if domain is None: domain = _get_maindomain() @@ -132,13 +132,13 @@ def dyndns_subscribe( # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): - raise YunohostError( + raise YunohostValidationError( "dyndns_domain_not_provided", domain=domain, provider=subscribe_host ) # Verify if domain is available if not _dyndns_available(subscribe_host, domain): - raise YunohostError("dyndns_unavailable", domain=domain) + raise YunohostValidationError("dyndns_unavailable", domain=domain) operation_logger.start() @@ -231,7 +231,7 @@ def dyndns_update( (domain, key) = _guess_current_dyndns_domain(dyn_host) if domain is None: - raise YunohostError('dyndns_no_domain_registered') + raise YunohostValidationError('dyndns_no_domain_registered') # If key is not given, pick the first file we find with the domain given else: @@ -239,7 +239,7 @@ def dyndns_update( keys = glob.glob("/etc/yunohost/dyndns/K{0}.+*.private".format(domain)) if not keys: - raise YunohostError("dyndns_key_not_found") + raise YunohostValidationError("dyndns_key_not_found") key = keys[0] diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 1b708a626..bc21f1948 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -28,7 +28,7 @@ import yaml import miniupnpc from moulinette import m18n -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import process from moulinette.utils.log import getActionLogger from moulinette.utils.text import prependlines @@ -366,7 +366,7 @@ def firewall_upnp(action="status", no_refresh=False): if action == "status": no_refresh = True else: - raise YunohostError("action_invalid", action=action) + raise YunohostValidationError("action_invalid", action=action) # Refresh port mapping using UPnP if not no_refresh: diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index e9857e4f9..493ad2c35 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -32,7 +32,7 @@ from glob import iglob from importlib import import_module from moulinette import m18n, msettings -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils import log from moulinette.utils.filesystem import read_json @@ -117,7 +117,7 @@ def hook_info(action, name): ) if not hooks: - raise YunohostError("hook_name_unknown", name=name) + raise YunohostValidationError("hook_name_unknown", name=name) return { "action": action, "name": name, @@ -186,7 +186,7 @@ def hook_list(action, list_by="name", show_info=False): d.add(name) else: - raise YunohostError("hook_list_by_invalid") + raise YunohostValidationError("hook_list_by_invalid") def _append_folder(d, folder): # Iterate over and add hook from a folder @@ -273,7 +273,7 @@ def hook_callback( try: hl = hooks_names[n] except KeyError: - raise YunohostError("hook_name_unknown", n) + raise YunohostValidationError("hook_name_unknown", n) # Iterate over hooks with this name for h in hl: # Update hooks dict diff --git a/src/yunohost/log.py b/src/yunohost/log.py index e5a53d466..1260cd98d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -191,7 +191,7 @@ def log_show( log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): - raise YunohostError("log_does_exists", log=path) + raise YunohostValidationError("log_does_exists", log=path) infos = {} diff --git a/src/yunohost/permission.py b/src/yunohost/permission.py index 3cd67b148..e0a3c6be8 100644 --- a/src/yunohost/permission.py +++ b/src/yunohost/permission.py @@ -31,7 +31,7 @@ import random from moulinette import m18n from moulinette.utils.log import getActionLogger -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation logger = getActionLogger("yunohost.user") @@ -175,14 +175,14 @@ def user_permission_update( # Refuse to add "visitors" to mail, xmpp ... they require an account to make sense. if add and "visitors" in add and permission.split(".")[0] in SYSTEM_PERMS: - raise YunohostError("permission_require_account", permission=permission) + raise YunohostValidationError("permission_require_account", permission=permission) # Refuse to add "visitors" to protected permission if ( (add and "visitors" in add and existing_permission["protected"]) or (remove and "visitors" in remove and existing_permission["protected"]) ) and not force: - raise YunohostError("permission_protected", permission=permission) + raise YunohostValidationError("permission_protected", permission=permission) # Fetch currently allowed groups for this permission @@ -198,7 +198,7 @@ def user_permission_update( groups_to_add = [add] if not isinstance(add, list) else add for group in groups_to_add: if group not in all_existing_groups: - raise YunohostError("group_unknown", group=group) + raise YunohostValidationError("group_unknown", group=group) if group in current_allowed_groups: logger.warning( m18n.n( @@ -326,7 +326,7 @@ def user_permission_info(permission): permission, None ) if existing_permission is None: - raise YunohostError("permission_not_found", permission=permission) + raise YunohostValidationError("permission_not_found", permission=permission) return existing_permission @@ -391,7 +391,7 @@ def permission_create( if ldap.get_conflict( {"cn": permission}, base_dn="ou=permission,dc=yunohost,dc=org" ): - raise YunohostError("permission_already_exist", permission=permission) + raise YunohostValidationError("permission_already_exist", permission=permission) # Get random GID all_gid = {x.gr_gid for x in grp.getgrall()} @@ -427,7 +427,7 @@ def permission_create( all_existing_groups = user_group_list()["groups"].keys() for group in allowed or []: if group not in all_existing_groups: - raise YunohostError("group_unknown", group=group) + raise YunohostValidationError("group_unknown", group=group) operation_logger.related_to.append(("app", permission.split(".")[0])) operation_logger.start() @@ -594,7 +594,7 @@ def permission_delete(operation_logger, permission, force=False, sync_perm=True) permission = permission + ".main" if permission.endswith(".main") and not force: - raise YunohostError("permission_cannot_remove_main") + raise YunohostValidationError("permission_cannot_remove_main") from yunohost.utils.ldap import _get_ldap_interface @@ -861,7 +861,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): try: re.compile(regex) except Exception: - raise YunohostError("invalid_regex", regex=regex) + raise YunohostValidationError("invalid_regex", regex=regex) if url.startswith("re:"): @@ -874,12 +874,12 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): # regex with domain if "/" not in url: - raise YunohostError("regex_with_only_domain") + raise YunohostValidationError("regex_with_only_domain") domain, path = url[3:].split("/", 1) path = "/" + path if domain.replace("%", "").replace("\\", "") not in domains: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) validate_regex(path) @@ -914,7 +914,7 @@ def _validate_and_sanitize_permission_url(url, app_base_path, app): sanitized_url = domain + path if domain not in domains: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) _assert_no_conflicting_apps(domain, path, ignore_app=app) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 2de395131..3a0450bce 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -34,7 +34,7 @@ from glob import glob from datetime import datetime from moulinette import m18n -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.process import check_output from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, append_to_file, write_to_file @@ -145,7 +145,7 @@ def service_remove(name): services = _get_services() if name not in services: - raise YunohostError("service_unknown", service=name) + raise YunohostValidationError("service_unknown", service=name) del services[name] try: @@ -325,7 +325,7 @@ def service_status(names=[]): # Validate service names requested for name in names: if name not in services.keys(): - raise YunohostError("service_unknown", service=name) + raise YunohostValidationError("service_unknown", service=name) # Filter only requested servivces services = {k: v for k, v in services.items() if k in names} @@ -484,7 +484,7 @@ def service_log(name, number=50): number = int(number) if name not in services.keys(): - raise YunohostError("service_unknown", service=name) + raise YunohostValidationError("service_unknown", service=name) log_list = services[name].get("log", []) @@ -545,7 +545,7 @@ def service_regen_conf( for name in names: if name not in services.keys(): - raise YunohostError("service_unknown", service=name) + raise YunohostValidationError("service_unknown", service=name) if names is []: names = list(services.keys()) @@ -568,7 +568,7 @@ def _run_service_command(action, service): """ services = _get_services() if service not in services.keys(): - raise YunohostError("service_unknown", service=service) + raise YunohostValidationError("service_unknown", service=service) possible_actions = [ "start", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 9bf75ff1d..9d1a6d11f 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -6,7 +6,7 @@ from datetime import datetime from collections import OrderedDict from moulinette import m18n -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.log import getActionLogger from yunohost.regenconf import regen_conf @@ -109,7 +109,7 @@ def settings_get(key, full=False): settings = _get_settings() if key not in settings: - raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) + raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key) if full: return settings[key] @@ -137,7 +137,7 @@ def settings_set(key, value): settings = _get_settings() if key not in settings: - raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) + raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key) key_type = settings[key]["type"] @@ -146,7 +146,7 @@ def settings_set(key, value): if boolean_value[0]: value = boolean_value[1] else: - raise YunohostError( + raise YunohostValidationError( "global_settings_bad_type_for_setting", setting=key, received_type=type(value).__name__, @@ -158,14 +158,14 @@ def settings_set(key, value): try: value = int(value) except Exception: - raise YunohostError( + raise YunohostValidationError( "global_settings_bad_type_for_setting", setting=key, received_type=type(value).__name__, expected_type=key_type, ) else: - raise YunohostError( + raise YunohostValidationError( "global_settings_bad_type_for_setting", setting=key, received_type=type(value).__name__, @@ -173,7 +173,7 @@ def settings_set(key, value): ) elif key_type == "string": if not isinstance(value, str): - raise YunohostError( + raise YunohostValidationError( "global_settings_bad_type_for_setting", setting=key, received_type=type(value).__name__, @@ -181,14 +181,14 @@ def settings_set(key, value): ) elif key_type == "enum": if value not in settings[key]["choices"]: - raise YunohostError( + raise YunohostValidationError( "global_settings_bad_choice_for_enum", setting=key, choice=str(value), available_choices=", ".join(settings[key]["choices"]), ) else: - raise YunohostError( + raise YunohostValidationError( "global_settings_unknown_type", setting=key, unknown_type=key_type ) @@ -214,7 +214,7 @@ def settings_reset(key): settings = _get_settings() if key not in settings: - raise YunohostError("global_settings_key_doesnt_exists", settings_key=key) + raise YunohostValidationError("global_settings_key_doesnt_exists", settings_key=key) settings[key]["value"] = settings[key]["default"] _save_settings(settings) @@ -304,7 +304,7 @@ def _get_settings(): ) unknown_settings[key] = value except Exception as e: - raise YunohostError("global_settings_cant_open_settings", reason=e) + raise YunohostValidationError("global_settings_cant_open_settings", reason=e) if unknown_settings: try: diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index f7c6fcbb1..e9e7e1831 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -5,7 +5,7 @@ import os import pwd import subprocess -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" @@ -21,7 +21,7 @@ def user_ssh_allow(username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): - raise YunohostError("user_unknown", user=username) + raise YunohostValidationError("user_unknown", user=username) from yunohost.utils.ldap import _get_ldap_interface @@ -43,7 +43,7 @@ def user_ssh_disallow(username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(username): - raise YunohostError("user_unknown", user=username) + raise YunohostValidationError("user_unknown", user=username) from yunohost.utils.ldap import _get_ldap_interface diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index e1ebe1307..e5699dede 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -51,7 +51,7 @@ from yunohost.utils.packages import ( _list_upgradable_apt_packages, ynh_packages_version, ) -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.log import is_unit_operation, OperationLogger # FIXME this is a duplicate from apps.py @@ -156,7 +156,7 @@ def tools_adminpw(new_password, check_strength=True): # UNIX seems to not like password longer than 127 chars ... # e.g. SSH login gets broken (or even 'su admin' when entering the password) if len(new_password) >= 127: - raise YunohostError("admin_password_too_long") + raise YunohostValidationError("admin_password_too_long") new_hash = _hash_user_password(new_password) @@ -285,10 +285,10 @@ def tools_postinstall( # Do some checks at first if os.path.isfile("/etc/yunohost/installed"): - raise YunohostError("yunohost_already_installed") + raise YunohostValidationError("yunohost_already_installed") if os.path.isdir("/etc/yunohost/apps") and os.listdir("/etc/yunohost/apps") != []: - raise YunohostError( + raise YunohostValidationError( "It looks like you're trying to re-postinstall a system that was already working previously ... If you recently had some bug or issues with your installation, please first discuss with the team on how to fix the situation instead of savagely re-running the postinstall ...", raw_msg=True, ) @@ -301,7 +301,7 @@ def tools_postinstall( ) GB = 1024 ** 3 if not force_diskspace and main_space < 10 * GB: - raise YunohostError("postinstall_low_rootfsspace") + raise YunohostValidationError("postinstall_low_rootfsspace") # Check password if not force_password: @@ -331,14 +331,14 @@ def tools_postinstall( dyndns = True # If not, abort the postinstall else: - raise YunohostError("dyndns_unavailable", domain=domain) + raise YunohostValidationError("dyndns_unavailable", domain=domain) else: dyndns = False else: dyndns = False if os.system("iptables -V >/dev/null 2>/dev/null") != 0: - raise YunohostError( + raise YunohostValidationError( "iptables/nftables does not seems to be working on your setup. You may be in a container or your kernel does have the proper modules loaded. Sometimes, rebooting the machine may solve the issue.", raw_msg=True, ) @@ -530,17 +530,17 @@ def tools_upgrade( from yunohost.utils import packages if packages.dpkg_is_broken(): - raise YunohostError("dpkg_is_broken") + raise YunohostValidationError("dpkg_is_broken") # Check for obvious conflict with other dpkg/apt commands already running in parallel if not packages.dpkg_lock_available(): - raise YunohostError("dpkg_lock_not_available") + raise YunohostValidationError("dpkg_lock_not_available") if system is not False and apps is not None: - raise YunohostError("tools_upgrade_cant_both") + raise YunohostValidationError("tools_upgrade_cant_both") if system is False and apps is None: - raise YunohostError("tools_upgrade_at_least_one") + raise YunohostValidationError("tools_upgrade_at_least_one") # # Apps @@ -825,7 +825,7 @@ def tools_migrations_list(pending=False, done=False): # Check for option conflict if pending and done: - raise YunohostError("migrations_list_conflict_pending_done") + raise YunohostValidationError("migrations_list_conflict_pending_done") # Get all migrations migrations = _get_migrations_list() @@ -875,17 +875,17 @@ def tools_migrations_run( if m.id == target or m.name == target or m.id.split("_")[0] == target: return m - raise YunohostError("migrations_no_such_migration", id=target) + raise YunohostValidationError("migrations_no_such_migration", id=target) # auto, skip and force are exclusive options if auto + skip + force_rerun > 1: - raise YunohostError("migrations_exclusive_options") + raise YunohostValidationError("migrations_exclusive_options") # If no target specified if not targets: # skip, revert or force require explicit targets if skip or force_rerun: - raise YunohostError("migrations_must_provide_explicit_targets") + raise YunohostValidationError("migrations_must_provide_explicit_targets") # Otherwise, targets are all pending migrations targets = [m for m in all_migrations if m.state == "pending"] @@ -897,11 +897,11 @@ def tools_migrations_run( pending = [t.id for t in targets if t.state == "pending"] if skip and done: - raise YunohostError("migrations_not_pending_cant_skip", ids=", ".join(done)) + raise YunohostValidationError("migrations_not_pending_cant_skip", ids=", ".join(done)) if force_rerun and pending: - raise YunohostError("migrations_pending_cant_rerun", ids=", ".join(pending)) + raise YunohostValidationError("migrations_pending_cant_rerun", ids=", ".join(pending)) if not (skip or force_rerun) and done: - raise YunohostError("migrations_already_ran", ids=", ".join(done)) + raise YunohostValidationError("migrations_already_ran", ids=", ".join(done)) # So, is there actually something to do ? if not targets: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index f1fab786a..089f2ba0e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -37,7 +37,7 @@ from moulinette import msignals, msettings, m18n from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.service import service_status from yunohost.log import is_unit_operation @@ -125,7 +125,7 @@ def user_create( # Validate domain used for email address/xmpp account if domain is None: if msettings.get("interface") == "api": - raise YunohostError("Invalide usage, specify domain argument") + raise YunohostValidationError("Invalid usage, you should specify a domain argument") else: # On affiche les differents domaines possibles msignals.display(m18n.n("domains_available")) @@ -141,24 +141,24 @@ def user_create( # Check that the domain exists if domain not in domain_list()["domains"]: - raise YunohostError("domain_name_unknown", domain=domain) + raise YunohostValidationError("domain_name_unknown", domain=domain) mail = username + "@" + domain ldap = _get_ldap_interface() if username in user_list()["users"]: - raise YunohostError("user_already_exists", user=username) + raise YunohostValidationError("user_already_exists", user=username) # Validate uniqueness of username and mail in LDAP try: ldap.validate_uniqueness({"uid": username, "mail": mail, "cn": username}) except Exception as e: - raise YunohostError("user_creation_failed", user=username, error=e) + raise YunohostValidationError("user_creation_failed", user=username, error=e) # Validate uniqueness of username in system users all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: - raise YunohostError("system_username_exists") + raise YunohostValidationError("system_username_exists") main_domain = _get_maindomain() aliases = [ @@ -170,7 +170,7 @@ def user_create( ] if mail in aliases: - raise YunohostError("mail_unavailable") + raise YunohostValidationError("mail_unavailable") operation_logger.start() @@ -264,7 +264,7 @@ def user_delete(operation_logger, username, purge=False): from yunohost.utils.ldap import _get_ldap_interface if username not in user_list()["users"]: - raise YunohostError("user_unknown", user=username) + raise YunohostValidationError("user_unknown", user=username) operation_logger.start() @@ -347,7 +347,7 @@ def user_update( attrs=attrs_to_fetch, ) if not result: - raise YunohostError("user_unknown", user=username) + raise YunohostValidationError("user_unknown", user=username) user = result[0] env_dict = {"YNH_USER_USERNAME": username} @@ -396,13 +396,13 @@ def user_update( try: ldap.validate_uniqueness({"mail": mail}) except Exception as e: - raise YunohostError("user_update_failed", user=username, error=e) + raise YunohostValidationError("user_update_failed", user=username, error=e) if mail[mail.find("@") + 1 :] not in domains: - raise YunohostError( + raise YunohostValidationError( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) if mail in aliases: - raise YunohostError("mail_unavailable") + raise YunohostValidationError("mail_unavailable") del user["mail"][0] new_attr_dict["mail"] = [mail] + user["mail"] @@ -414,9 +414,9 @@ def user_update( try: ldap.validate_uniqueness({"mail": mail}) except Exception as e: - raise YunohostError("user_update_failed", user=username, error=e) + raise YunohostValidationError("user_update_failed", user=username, error=e) if mail[mail.find("@") + 1 :] not in domains: - raise YunohostError( + raise YunohostValidationError( "mail_domain_unknown", domain=mail[mail.find("@") + 1 :] ) user["mail"].append(mail) @@ -429,7 +429,7 @@ def user_update( if len(user["mail"]) > 1 and mail in user["mail"][1:]: user["mail"].remove(mail) else: - raise YunohostError("mail_alias_remove_failed", mail=mail) + raise YunohostValidationError("mail_alias_remove_failed", mail=mail) new_attr_dict["mail"] = user["mail"] if "mail" in new_attr_dict: @@ -451,7 +451,7 @@ def user_update( if len(user["maildrop"]) > 1 and mail in user["maildrop"][1:]: user["maildrop"].remove(mail) else: - raise YunohostError("mail_forward_remove_failed", mail=mail) + raise YunohostValidationError("mail_forward_remove_failed", mail=mail) new_attr_dict["maildrop"] = user["maildrop"] if "maildrop" in new_attr_dict: @@ -500,7 +500,7 @@ def user_info(username): if result: user = result[0] else: - raise YunohostError("user_unknown", user=username) + raise YunohostValidationError("user_unknown", user=username) result_dict = { "username": user["uid"][0], @@ -638,7 +638,7 @@ def user_group_create( {"cn": groupname}, base_dn="ou=groups,dc=yunohost,dc=org" ) if conflict: - raise YunohostError("group_already_exist", group=groupname) + raise YunohostValidationError("group_already_exist", group=groupname) # Validate uniqueness of groupname in system group all_existing_groupnames = {x.gr_name for x in grp.getgrall()} @@ -651,7 +651,7 @@ def user_group_create( "sed --in-place '/^%s:/d' /etc/group" % groupname, shell=True ) else: - raise YunohostError("group_already_exist_on_system", group=groupname) + raise YunohostValidationError("group_already_exist_on_system", group=groupname) if not gid: # Get random GID @@ -705,7 +705,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): existing_groups = list(user_group_list()["groups"].keys()) if groupname not in existing_groups: - raise YunohostError("group_unknown", group=groupname) + raise YunohostValidationError("group_unknown", group=groupname) # Refuse to delete primary groups of a user (e.g. group 'sam' related to user 'sam') # without the force option... @@ -714,7 +714,7 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True): existing_users = list(user_list()["users"].keys()) undeletable_groups = existing_users + ["all_users", "visitors"] if groupname in undeletable_groups and not force: - raise YunohostError("group_cannot_be_deleted", group=groupname) + raise YunohostValidationError("group_cannot_be_deleted", group=groupname) operation_logger.start() ldap = _get_ldap_interface() @@ -756,11 +756,11 @@ def user_group_update( # We also can't edit "all_users" without the force option because that's a special group... if not force: if groupname == "all_users": - raise YunohostError("group_cannot_edit_all_users") + raise YunohostValidationError("group_cannot_edit_all_users") elif groupname == "visitors": - raise YunohostError("group_cannot_edit_visitors") + raise YunohostValidationError("group_cannot_edit_visitors") elif groupname in existing_users: - raise YunohostError("group_cannot_edit_primary_group", group=groupname) + raise YunohostValidationError("group_cannot_edit_primary_group", group=groupname) # We extract the uid for each member of the group to keep a simple flat list of members current_group = user_group_info(groupname)["members"] @@ -771,7 +771,7 @@ def user_group_update( for user in users_to_add: if user not in existing_users: - raise YunohostError("user_unknown", user=user) + raise YunohostValidationError("user_unknown", user=user) if user in current_group: logger.warning( @@ -843,7 +843,7 @@ def user_group_info(groupname): ) if not result: - raise YunohostError("group_unknown", group=groupname) + raise YunohostValidationError("group_unknown", group=groupname) infos = result[0] diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index 3000a52f8..e78beb2c9 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -49,3 +49,7 @@ class YunohostError(MoulinetteError): return super(YunohostError, self).content() else: return {"error": self.strerror, "log_ref": self.log_ref} + + +class YunohostValidationError(YunohostError): + pass diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index dce337f84..9e693d8cd 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -90,11 +90,11 @@ class PasswordValidator(object): # on top (at least not the moulinette ones) # because the moulinette needs to be correctly initialized # as well as modules available in python's path. - from yunohost.utils.error import YunohostError + from yunohost.utils.error import YunohostValidationError status, msg = self.validation_summary(password) if status == "error": - raise YunohostError(msg) + raise YunohostValidationError(msg) def validation_summary(self, password): """ diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py index 6876cbcd8..799dc0d0c 100644 --- a/tests/test_i18n_keys.py +++ b/tests/test_i18n_keys.py @@ -25,9 +25,11 @@ def find_expected_string_keys(): # Try to find : # m18n.n( "foo" # YunohostError("foo" + # YunohostValidationError("foo" # # i18n: foo p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']") p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]") + p2 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]") p3 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") python_files = glob.glob("src/yunohost/*.py") From 41b5a1239336ff51868a3ec7cff7385e1d8ab88b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 11 Mar 2021 03:07:16 +0100 Subject: [PATCH 328/363] Enforce permissions for /home/yunohost.backup and .conf --- data/hooks/conf_regen/01-yunohost | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index c4120d487..1dd2705e1 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -94,6 +94,22 @@ do_post_regen() { # Enfore permissions # ###################### + if [ -d /home/yunohost.backup ] + then + chmod 750 /home/yunohost.backup + chown admin:root /home/yunohost.backup + fi + if [ -d /home/yunohost.backup/archives ] + then + chmod 750 /home/yunohost.backup/archives + chown admin:root /home/yunohost.backup/archives + fi + if [ -d /home/yunohost.conf ] + then + chmod 750 /home/yunohost.conf + chown root:root /home/yunohost.conf + fi + # Certs # We do this with find because there could be a lot of them... chown -R root:ssl-cert /etc/yunohost/certs From 4a7129e69b4e4a0d2096f3f8029cb425488a8bbe Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 11 Mar 2021 03:08:41 +0100 Subject: [PATCH 329/363] Update changelog for 4.1.7.4 --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 5fb0e563d..51eed275b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +yunohost (4.1.7.4) stable; urgency=low + + - [fix] sec: Enforce permissions for /home/yunohost.backup and .conf (41b5a123) + + -- Alexandre Aubin Thu, 11 Mar 2021 03:08:10 +0100 + yunohost (4.1.7.3) stable; urgency=low - [fix] log: Some secrets were not redacted (0c172cd3) From 37f0c30ddc8cf50a0fc7839026d6e604bff32810 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 11 Mar 2021 03:57:15 +0100 Subject: [PATCH 330/363] Inject log_ref into all is_unit_operation failures --- src/yunohost/app.py | 3 +-- src/yunohost/log.py | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 613ca21df..f214824aa 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1124,8 +1124,7 @@ def app_install( raise YunohostError( failure_message_with_debug_instructions, - raw_msg=True, - log_ref=operation_logger.name, + raw_msg=True ) # Clean hooks and add new ones diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 1260cd98d..3f5fa8e71 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -35,7 +35,7 @@ from logging import FileHandler, getLogger, Formatter from moulinette import m18n, msettings from moulinette.core import MoulinetteError -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.packages import get_ynh_package_version from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, read_yaml @@ -635,6 +635,14 @@ class OperationLogger(object): return if error is not None and not isinstance(error, str): error = str(error) + + # When the error happen's in the is_unit_operation try/except, + # we want to inject the log ref in the exception, such that it may be + # transmitted to the webadmin which can then redirect to the appropriate + # log page + if isinstance(error, Exception) and not isinstance(error, YunohostValidationError): + error.log_ref = operation_logger.name + self.ended_at = datetime.utcnow() self._error = error self._success = error is None From 1a0ef941099ad6d9d2befa24b41fd5dfdc1bc2c8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 12 Mar 2021 04:24:27 +0100 Subject: [PATCH 331/363] Define HTTP codes for Yunohost Errors --- src/yunohost/utils/error.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index e78beb2c9..c0ff2690c 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -25,6 +25,8 @@ from moulinette import m18n class YunohostError(MoulinetteError): + http_code = 500 + """ Yunohost base exception @@ -52,4 +54,5 @@ class YunohostError(MoulinetteError): class YunohostValidationError(YunohostError): - pass + + http_code = 400 From ce04570bfda75291a06727d1607641c13fcdfbfa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 12 Mar 2021 23:18:16 +0100 Subject: [PATCH 332/363] helpers: Simplify manifest path / parsing --- data/helpers.d/apt | 9 +++------ data/helpers.d/utils | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/data/helpers.d/apt b/data/helpers.d/apt index bfdeffe7b..f0c650ace 100644 --- a/data/helpers.d/apt +++ b/data/helpers.d/apt @@ -224,13 +224,10 @@ ynh_install_app_dependencies () { # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) dependencies="$(echo "$dependencies" | sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g')" 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 - fi + local manifest_path="$YNH_APP_BASEDIR/manifest.json" - local version=$(grep '\"version\": ' "$manifest_path" | cut --delimiter='"' --fields=4) # Retrieve the version number in the manifest file. - if [ ${#version} -eq 0 ]; then + local version=$(jq -r '.version' "$manifest_path") + if [ -z "${version}" ] || [ "$version" == "null" ]; then version="1.0" fi local dep_app=${app//_/-} # Replace all '_' by '-' diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 8246b9986..c5ebdcb96 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -553,7 +553,7 @@ ynh_read_manifest () { if [ ! -e "$manifest" ]; then # If the manifest isn't found, try the common place for backup and restore script. - manifest="../settings/manifest.json" + manifest="$YNH_APP_BASEDIR/manifest.json" fi jq ".$manifest_key" "$manifest" --raw-output From 721f6f265e1c69d3fcadf0eb27f61918c65cb8f8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Mar 2021 18:45:36 +0100 Subject: [PATCH 333/363] Typo ... --- 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 3d1d16f3c..fadc16b6e 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1069,7 +1069,7 @@ def app_install( 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) - env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") + env_dict_remove["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?") # Execute remove script operation_logger_remove = OperationLogger( From 07f8d6d7af437af38a23899aa1abb731dfff11db Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 14 Mar 2021 18:47:33 +0100 Subject: [PATCH 334/363] ynh_clean_check_starting: Let's not trigger an error when vars aren't set --- data/helpers.d/systemd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/systemd b/data/helpers.d/systemd index 4c7bd31d1..1b620e991 100644 --- a/data/helpers.d/systemd +++ b/data/helpers.d/systemd @@ -171,12 +171,12 @@ ynh_systemd_action() { # # Requires YunoHost version 3.5.0 or higher. ynh_clean_check_starting () { - if [ -n "$pid_tail" ] + if [ -n "${pid_tail:-}" ] then # Stop the execution of tail. kill -SIGTERM $pid_tail 2>&1 fi - if [ -n "$templog" ] + if [ -n "${templog:-}" ] then ynh_secure_remove --file="$templog" 2>&1 fi From 7a947dbce1684bbade4aeb22c33500543a6aa8d8 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 15 Mar 2021 00:03:31 +0100 Subject: [PATCH 335/363] [fix] True instead of description (#1189) --- 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 2de395131..6ba271ea6 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -400,7 +400,7 @@ def _get_and_format_service_status(service, infos): translation_key = "service_description_%s" % service if m18n.key_exists(translation_key): - description = m18n.key_exists(translation_key) + description = m18n.n(translation_key) else: description = str(raw_status.get("Description", "")) From dc6033c3993023162869d770029b5ee8baab1c03 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Mon, 15 Mar 2021 14:10:13 +0100 Subject: [PATCH 336/363] python3 way to get a list of dict keys --- data/hooks/diagnosis/14-ports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/diagnosis/14-ports.py b/data/hooks/diagnosis/14-ports.py index 6faf29053..7581a1ac6 100644 --- a/data/hooks/diagnosis/14-ports.py +++ b/data/hooks/diagnosis/14-ports.py @@ -43,7 +43,7 @@ class PortsDiagnoser(Diagnoser): for ipversion in ipversions: try: r = Diagnoser.remote_diagnosis( - "check-ports", data={"ports": ports.keys()}, ipversion=ipversion + "check-ports", data={"ports": list(ports)}, ipversion=ipversion ) results[ipversion] = r["ports"] except Exception as e: From 4a19a60b44a81ab5f80e4547a05b64694b169154 Mon Sep 17 00:00:00 2001 From: Kay0u Date: Wed, 10 Mar 2021 11:24:11 +0100 Subject: [PATCH 337/363] dirty patch to wait for services to finish reloading --- src/yunohost/app.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index fadc16b6e..9d53df815 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -3465,6 +3465,14 @@ def _assert_system_is_sane_for_app(manifest, when): if "fail2ban" not in services: services.append("fail2ban") + # Wait if a service is reloading + test_nb = 0 + while test_nb < 10: + if not any(s for s in services if service_status(s)["status"] == "reloading"): + break + time.sleep(0.5) + test_nb+=1 + # List services currently down and raise an exception if any are found faulty_services = [s for s in services if service_status(s)["status"] != "running"] if faulty_services: From c2506f362c476558e1dc4410c365e2bf79e88726 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 26 Feb 2021 18:49:26 +0000 Subject: [PATCH 338/363] Translated using Weblate (German) Currently translated at 80.9% (510 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 536bdd842..019781e91 100644 --- a/locales/de.json +++ b/locales/de.json @@ -574,5 +574,13 @@ "password_listed": "Dieses Passwort gehört zu den meistverwendeten der Welt. Bitte nehmen Sie etwas einzigartigeres.", "operation_interrupted": "Wurde die Operation manuell unterbrochen?", "invalid_number": "Muss eine Zahl sein", - "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus." + "migrations_to_be_ran_manually": "Die Migration {id} muss manuell durchgeführt werden. Bitte gehen Sie zu Werkzeuge → Migrationen auf der Webadmin-Seite oder führen Sie 'yunohost tools migrations run' aus.", + "permission_already_up_to_date": "Die Berechtigung wurde nicht aktualisiert, weil die Anfragen für Hinzufügen/Entfernen stimmen mit dem aktuellen Status bereits überein", + "permission_already_exist": "Berechtigung '{permission}' existiert bereits", + "permission_already_disallowed": "Für die Gruppe '{group}' wurde die Berechtigung '{permission}' deaktiviert", + "permission_already_allowed": "Die Gruppe '{group}' hat die Berechtigung '{permission}' bereits erhalten", + "pattern_password_app": "Entschuldigen Sie bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}", + "pattern_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)", + "password_too_simple_4": "Dass Passwort muss mindestens 12 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten", + "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten" } From 394fd90383f826bc918b611e466ca1b30be66839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Sat, 27 Feb 2021 20:20:48 +0000 Subject: [PATCH 339/363] Translated using Weblate (Occitan) Currently translated at 53.9% (340 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/oc/ --- locales/oc.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 07d841579..1849b3f3b 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -340,8 +340,8 @@ "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.", "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx", "users_available": "Lista dels utilizaires disponibles :", - "good_practices_about_admin_password": "Sètz per definir un nòu senhal per l’administracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far d’utilizar un senhal mai long qu’aquò (ex. una passafrasa) e/o d’utilizar mantun tipes de caractèrs (majuscula, minuscula, nombre e caractèrs especials).", - "good_practices_about_user_password": "Sètz a mand de definir un nòu senhal d’utilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far d’utilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantuns tipes de caractèrs (majusculas, minusculas, nombres e caractèrs especials).", + "good_practices_about_admin_password": "Sètz per definir un nòu senhal per l’administracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far d’utilizar un senhal mai long qu’aquò (ex. una passafrasa) e/o d’utilizar mantun tipe de caractèrs (majuscula, minuscula, nombre e caractèrs especials).", + "good_practices_about_user_password": "Sètz a mand de definir un nòu senhal d’utilizaire. Lo nòu senhal deu conténer almens 8 caractèrs, es de bon far d’utilizar un senhal mai long (es a dire una frasa de senhal) e/o utilizar mantun tipe de caractèrs (majusculas, minusculas, nombres e caractèrs especials).", "migration_description_0006_sync_admin_and_root_passwords": "Sincronizar los senhals admin e root", "migration_0006_disclaimer": "Ara YunoHost s’espèra que los senhals admin e root sián sincronizats. En lançant aquesta migracion, vòstre senhal root serà remplaçat pel senhal admin.", "password_listed": "Aqueste senhal es un dels mai utilizats al monde. Se vos plai utilizatz-ne un mai unic.", From 56321d41c4c666ff1fde30970d4507bfcb493597 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Sun, 28 Feb 2021 22:31:09 +0000 Subject: [PATCH 340/363] Translated using Weblate (German) Currently translated at 81.1% (511 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 019781e91..58b791d06 100644 --- a/locales/de.json +++ b/locales/de.json @@ -79,7 +79,7 @@ "ldap_initialized": "LDAP wurde initialisiert", "mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden", "mail_domain_unknown": "Die Domäne '{domain:s}' dieser E-Mail-Adresse ist ungültig. Wähle bitte eine Domäne, welche durch diesen Server verwaltet wird.", - "mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden", + "mail_forward_remove_failed": "Mailweiterleitung für '{mail:s}' konnte nicht deaktiviert werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_changed": "Die Hauptdomain wurde geändert", "no_internet_connection": "Der Server ist nicht mit dem Internet verbunden", From 74a9512d8ea8e6de9f70271e2b33549686e5ea4f Mon Sep 17 00:00:00 2001 From: Scapharnaum Date: Sun, 28 Feb 2021 21:48:41 +0000 Subject: [PATCH 341/363] Translated using Weblate (German) Currently translated at 81.1% (511 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/de.json b/locales/de.json index 58b791d06..a86ba681a 100644 --- a/locales/de.json +++ b/locales/de.json @@ -176,7 +176,7 @@ "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 Zertifikat installiert.", - "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert.", + "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert", "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domain {domain:s} ausgestellt. Bitte versuchen Sie es später nochmal. Besuchen Sie https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", "certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden", "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden", @@ -337,7 +337,7 @@ "diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", - "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie eine eigene /etc/resolv.conf verwendest.", + "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie Ihren eigenen /etc/resolv.conf verwenden.", "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues' in der Kommandozeile ausführen.", "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", @@ -359,7 +359,7 @@ "diagnosis_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!", "diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.", "diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!", - "diagnosis_domain_expiration_not_found": "Konnte die Ablaufdaten für einige Domänen nicht überprüfen.", + "diagnosis_domain_expiration_not_found": "Das Ablaufdatum einiger Domains kann nicht überprüft werden", "diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von Yunohost verwaltet werden. Andernfalls können Sie mittels yunohost dyndns update --force ein Update erzwingen.", "diagnosis_dns_point_to_doc": "Bitte schauen Sie in die Dokumentation unter https://yunohost.org/dns_config wenn Sie Hilfe bei der Konfiguration der DNS-Einträge brauchen.", "diagnosis_dns_discrepancy": "Der folgende DNS-Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen:
Typ: {type}
Name: {name}
Aktueller Wert: {current}
Erwarteter Wert: {value}", From acd4d223cbcfd54b5db8ca02420898bc2c71277b Mon Sep 17 00:00:00 2001 From: Scapharnaum Date: Mon, 1 Mar 2021 00:00:42 +0000 Subject: [PATCH 342/363] Translated using Weblate (German) Currently translated at 83.6% (527 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index a86ba681a..18bb9068a 100644 --- a/locales/de.json +++ b/locales/de.json @@ -79,7 +79,7 @@ "ldap_initialized": "LDAP wurde initialisiert", "mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden", "mail_domain_unknown": "Die Domäne '{domain:s}' dieser E-Mail-Adresse ist ungültig. Wähle bitte eine Domäne, welche durch diesen Server verwaltet wird.", - "mail_forward_remove_failed": "Mailweiterleitung für '{mail:s}' konnte nicht deaktiviert werden", + "mail_forward_remove_failed": "Die Weiterleitungs-E-Mail '{mail:s}' konnte nicht gelöscht werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_changed": "Die Hauptdomain wurde geändert", "no_internet_connection": "Der Server ist nicht mit dem Internet verbunden", @@ -582,5 +582,21 @@ "pattern_password_app": "Entschuldigen Sie bitte! Passwörter dürfen folgende Zeichen nicht enthalten: {forbidden_chars}", "pattern_email_forward": "Es muss sich um eine gültige E-Mail-Adresse handeln. Das Symbol '+' wird akzeptiert (zum Beispiel : maxmuster@beispiel.com oder maxmuster+yunohost@beispiel.com)", "password_too_simple_4": "Dass Passwort muss mindestens 12 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten", - "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten" + "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten", + "regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt", + "regenconf_file_manually_modified": "Die Konfigurationsdatei '{conf}' wurde manuell bearbeitet und wird nicht aktualisiert", + "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {Kategorie}) gelöscht werden, wurde aber beibehalten.", + "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden", + "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert", + "permission_require_account": "Berechtigung {permission} ist nur für Benutzer mit einem Konto sinnvoll und kann daher nicht für Besucher aktiviert werden.", + "permission_protected": "Die Berechtigung ist geschützt. Sie können die Besuchergruppe nicht zu dieser Berechtigung hinzufügen oder daraus entfernen.", + "permission_updated": "Berechtigung '{permission:s}' aktualisiert", + "permission_update_failed": "Die Berechtigung '{permission}' kann nicht aktualisiert werden : {error}", + "permission_not_found": "Berechtigung nicht gefunden", + "permission_deletion_failed": "Entfernung der Berechtigung nicht möglich '{permission}': {error}", + "permission_deleted": "Berechtigung gelöscht", + "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Benutzern zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchten Sie entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.", + "permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}", + "permission_created": "Berechtigung '{permission: s}' erstellt", + "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt" } From 4fadd4b68f4b9fe32847d152133de47e9229d30a Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 3 Mar 2021 21:56:17 +0000 Subject: [PATCH 343/363] Translated using Weblate (German) Currently translated at 84.2% (531 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 18bb9068a..2dc287db5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -598,5 +598,9 @@ "permission_currently_allowed_for_all_users": "Diese Berechtigung wird derzeit allen Benutzern zusätzlich zu anderen Gruppen erteilt. Möglicherweise möchten Sie entweder die Berechtigung 'all_users' entfernen oder die anderen Gruppen entfernen, für die sie derzeit zulässig sind.", "permission_creation_failed": "Berechtigungserstellung nicht möglich '{permission}' : {error}", "permission_created": "Berechtigung '{permission: s}' erstellt", - "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt" + "permission_cannot_remove_main": "Entfernung einer Hauptberechtigung nicht genehmigt", + "regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert", + "regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt", + "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen", + "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace" } From 0b7e040ee2b378cbd411729b088795c32c9ee821 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 5 Mar 2021 06:50:41 +0000 Subject: [PATCH 344/363] Translated using Weblate (German) Currently translated at 84.6% (533 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 2dc287db5..4fcc7a7e3 100644 --- a/locales/de.json +++ b/locales/de.json @@ -602,5 +602,7 @@ "regenconf_file_updated": "Konfigurationsdatei '{conf}' aktualisiert", "regenconf_file_removed": "Konfigurationsdatei '{conf}' entfernt", "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen", - "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace" + "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace", + "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", + "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet." } From 18cc2ecbc8a9e2c1494f8f1968b96bf5ef9e118b Mon Sep 17 00:00:00 2001 From: Radek S Date: Sat, 6 Mar 2021 13:22:33 +0000 Subject: [PATCH 345/363] Translated using Weblate (Czech) Currently translated at 1.7% (11 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/cs/ --- locales/cs.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index eafada5e6..3df59e0fd 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -1,3 +1,13 @@ { - "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé" + "password_too_simple_1": "Heslo musí být aspoň 8 znaků dlouhé", + "app_already_installed": "{app:s} je již nainstalován/a", + "already_up_to_date": "Neprovedena žádná akce. Vše je již aktuální.", + "admin_password_too_long": "Zvolte prosím heslo kratší než 127 znaků", + "admin_password_changed": "Heslo správce bylo změněno", + "admin_password_change_failed": "Nebylo možné změnit heslo", + "admin_password": "Heslo správce", + "additional_urls_already_removed": "Dotatečný odkaz '{url:s}' byl již odebrán u oprávnění '{permission:s}'", + "additional_urls_already_added": "Dotatečný odkaz '{url:s}' byl již přidán v dodatečných odkazech pro oprávnění '{permission:s}'", + "action_invalid": "Nesprávné akce '{action:s}'", + "aborting": "Přerušení." } From ad4ca718f7131ab41e24c366393eba804d1b8e32 Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Wed, 10 Mar 2021 19:07:07 +0000 Subject: [PATCH 346/363] Translated using Weblate (German) Currently translated at 84.7% (534 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 4fcc7a7e3..cdaa714eb 100644 --- a/locales/de.json +++ b/locales/de.json @@ -604,5 +604,6 @@ "regenconf_file_remove_failed": "Konnte die Konfigurationsdatei '{conf}' nicht entfernen", "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace", "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", - "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet." + "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.", + "regenconf_updated": "Konfiguration aktualisiert für '{category}'" } From f0827451caeba1d29de86bd1f9dbbaa84a837fa3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 17 Mar 2021 16:37:07 +0100 Subject: [PATCH 347/363] Fix translation inconsistency --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index cdaa714eb..67a1d1a2e 100644 --- a/locales/de.json +++ b/locales/de.json @@ -585,7 +585,7 @@ "password_too_simple_3": "Das Passwort muss mindestens 8 Zeichen lang sein und Zahlen, Klein- und Grossbuchstaben und Sonderzeichen enthalten", "regenconf_file_manually_removed": "Die Konfigurationsdatei '{conf}' wurde manuell gelöscht und wird nicht erstellt", "regenconf_file_manually_modified": "Die Konfigurationsdatei '{conf}' wurde manuell bearbeitet und wird nicht aktualisiert", - "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {Kategorie}) gelöscht werden, wurde aber beibehalten.", + "regenconf_file_kept_back": "Die Konfigurationsdatei '{conf}' sollte von \"regen-conf\" (Kategorie {category}) gelöscht werden, wurde aber beibehalten.", "regenconf_file_copy_failed": "Die neue Konfigurationsdatei '{new}' kann nicht nach '{conf}' kopiert werden", "regenconf_file_backed_up": "Die Konfigurationsdatei '{conf}' wurde unter '{backup}' gespeichert", "permission_require_account": "Berechtigung {permission} ist nur für Benutzer mit einem Konto sinnvoll und kann daher nicht für Besucher aktiviert werden.", From 9e032b04bc33dc8a3825adf5e7b5efd3460fd89c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 16:49:41 +0100 Subject: [PATCH 348/363] Fix i18n key tests --- tests/test_i18n_keys.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_i18n_keys.py b/tests/test_i18n_keys.py index 799dc0d0c..2ad56a34e 100644 --- a/tests/test_i18n_keys.py +++ b/tests/test_i18n_keys.py @@ -29,8 +29,8 @@ def find_expected_string_keys(): # # i18n: foo p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']") p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]") - p2 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]") - p3 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") + p3 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]") + p4 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?") python_files = glob.glob("src/yunohost/*.py") python_files.extend(glob.glob("src/yunohost/utils/*.py")) @@ -49,6 +49,10 @@ def find_expected_string_keys(): continue yield m for m in p3.findall(content): + if m.endswith("_"): + continue + yield m + for m in p4.findall(content): yield m # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries) From 09d306924ac3139999aad96b9fe874e97636ca74 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 17:46:32 +0100 Subject: [PATCH 349/363] Fix minor issue with main_space diagnosis: duplicated error/warning, missing / partition when it's on a /dev/loop --- data/hooks/diagnosis/50-systemresources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/diagnosis/50-systemresources.py b/data/hooks/diagnosis/50-systemresources.py index 1e8e2201a..a8f3cb6df 100644 --- a/data/hooks/diagnosis/50-systemresources.py +++ b/data/hooks/diagnosis/50-systemresources.py @@ -77,7 +77,7 @@ class SystemResourcesDiagnoser(Diagnoser): # Ignore /dev/loop stuff which are ~virtual partitions ? (e.g. mounted to /snap/) disk_partitions = [ - d for d in disk_partitions if not d.device.startswith("/dev/loop") + d for d in disk_partitions if d.mountpoint in ["/", "/var"] or not d.device.startswith("/dev/loop") ] for disk_partition in disk_partitions: @@ -139,7 +139,7 @@ class SystemResourcesDiagnoser(Diagnoser): status="ERROR", summary="diagnosis_rootfstotalspace_critical", ) - if main_space < 14 * GB: + elif main_space < 14 * GB: yield dict( meta={"test": "rootfstotalspace"}, data={"space": human_size(main_space)}, From 11c50c018742647237315f5b7222e19a466125a4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 18:49:25 +0100 Subject: [PATCH 350/363] Fix log_ref injection --- src/yunohost/log.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 3f5fa8e71..7a45565f8 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -631,17 +631,18 @@ class OperationLogger(object): """ Close properly the unit operation """ - if self.ended_at is not None or self.started_at is None: - return - if error is not None and not isinstance(error, str): - error = str(error) # When the error happen's in the is_unit_operation try/except, # we want to inject the log ref in the exception, such that it may be # transmitted to the webadmin which can then redirect to the appropriate # log page if isinstance(error, Exception) and not isinstance(error, YunohostValidationError): - error.log_ref = operation_logger.name + error.log_ref = self.name + + if self.ended_at is not None or self.started_at is None: + return + if error is not None and not isinstance(error, str): + error = str(error) self.ended_at = datetime.utcnow() self._error = error From 33fab1c99f1775da0a32dcf7072be13a5cbd3c59 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 20:45:03 +0100 Subject: [PATCH 351/363] Be more robust against non-int values for level in app catalog (e.g. for apps 'inprogress' for which level is '?') --- src/yunohost/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 9d53df815..62e4ffd59 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -257,8 +257,9 @@ def _app_upgradable(app_infos): return "url_required" # Do not advertise upgrades for bad-quality apps + level = app_in_catalog.get("level", -1) if ( - not app_in_catalog.get("level", -1) >= 5 + not (isinstance(level, int) and level >= 5) or app_in_catalog.get("state") != "working" ): return "bad_quality" From ab454ff62f56e83d978f0a77995b1f578cb0fb4a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 21:40:33 +0100 Subject: [PATCH 352/363] Missing decode() for Popen output in certificate.py --- src/yunohost/certificate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c48af2c07..dade85285 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -197,6 +197,8 @@ def _certificate_install_selfsigned(domain_list, force=False): out, _ = p.communicate() + out = out.decode("utf-8") + if p.returncode != 0: logger.warning(out) raise YunohostError("domain_cert_gen_failed") From e63ca06d371e9e4eb5a724f11e6580404fec90a0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 22 Mar 2021 21:58:15 +0100 Subject: [PATCH 353/363] Missing import --- 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 b020c0f34..ba940058b 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -63,7 +63,7 @@ from yunohost.hook import ( from yunohost.tools import tools_postinstall from yunohost.regenconf import regen_conf from yunohost.log import OperationLogger -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from yunohost.utils.packages import ynh_packages_version from yunohost.settings import settings_get From a5fe21fd3864c818615f9c86e94f2e09c8844e86 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 23 Mar 2021 00:32:21 +0100 Subject: [PATCH 354/363] Zblerg multiple forgotten import / typo >_> --- src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py | 2 +- src/yunohost/domain.py | 4 ++-- src/yunohost/ssh.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py index 0526c025d..cbdfabb1f 100644 --- a/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py +++ b/src/yunohost/data_migrations/0017_postgresql_9p6_to_11.py @@ -1,7 +1,7 @@ import subprocess from moulinette import m18n -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index fdf247f89..8d8be57a0 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -28,7 +28,7 @@ import re from moulinette import m18n, msettings, msignals from moulinette.core import MoulinetteError -from yunohost.utils.error import YunohostError +from yunohost.utils.error import YunohostError, YunohostValidationError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import write_to_file @@ -131,7 +131,7 @@ def domain_add(operation_logger, domain, dyndns=False): operation_logger.start() if dyndns: - from yunohost.dyndns import dndns_subscribe + from yunohost.dyndns import dyndns_subscribe # Actually subscribe dyndns_subscribe(domain=domain) diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index e9e7e1831..39d4b4287 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -5,7 +5,7 @@ import os import pwd import subprocess -from yunohost.utils.error import YunohostError, YunohostValidationError +from yunohost.utils.error import YunohostValidationError from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" From c45d1010d6ee0b3a0fd0da697234b10387f62c3f Mon Sep 17 00:00:00 2001 From: Christian Wehrli Date: Fri, 19 Mar 2021 19:58:19 +0000 Subject: [PATCH 355/363] Translated using Weblate (German) Currently translated at 85.3% (538 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 67a1d1a2e..832fc1b91 100644 --- a/locales/de.json +++ b/locales/de.json @@ -605,5 +605,9 @@ "postinstall_low_rootfsspace": "Das Root-Filesystem hat insgesamt weniger als 10GB freien Speicherplatz zur Verfügung, was ziemlich besorgniserregend ist! Sie werden sehr bald keinen freien Speicherplatz mehr haben! Für das Root-Filesystem werden mindestens 16GB empfohlen. Wenn Sie YunoHost trotz dieser Warnung installieren wollen, wiederholen Sie den Befehl mit --force-diskspace", "regenconf_up_to_date": "Die Konfiguration ist bereits aktuell für die Kategorie '{category}'", "regenconf_now_managed_by_yunohost": "Die Konfigurationsdatei '{conf}' wird jetzt von YunoHost (Kategorie {category}) verwaltet.", - "regenconf_updated": "Konfiguration aktualisiert für '{category}'" + "regenconf_updated": "Konfiguration aktualisiert für '{category}'", + "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...", + "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen", + "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…", + "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden" } From cf884b1149f231eea590e90a91a561a8b5561f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Wed, 17 Mar 2021 20:36:55 +0000 Subject: [PATCH 356/363] Translated using Weblate (Occitan) Currently translated at 57.6% (363 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/oc/ --- locales/oc.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 1849b3f3b..7fd423617 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -327,7 +327,7 @@ "log_user_delete": "Levar l’utilizaire « {} »", "log_user_update": "Actualizar las informacions de l’utilizaire « {} »", "log_domain_main_domain": "Far venir « {} » lo domeni màger", - "log_tools_migrations_migrate_forward": "Migrar", + "log_tools_migrations_migrate_forward": "Executar las migracions", "log_tools_postinstall": "Realizar la post installacion del servidor YunoHost", "log_tools_upgrade": "Actualizacion dels paquets sistèma", "log_tools_shutdown": "Atudar lo servidor", @@ -593,5 +593,19 @@ "app_argument_password_no_default": "Error pendent l’analisi de l’argument del senhal « {name} » : l’argument de senhal pòt pas aver de valor per defaut per de rason de seguretat", "app_label_deprecated": "Aquesta comanda es estada renduda obsolèta. Mercés d'utilizar lo nòva \"yunohost user permission update\" per gerir letiquetada de l'aplication", "additional_urls_already_removed": "URL addicionala {url:s} es ja estada elimida per la permission «#permission:s»", - "additional_urls_already_added": "URL addicionadal «{url:s}'» es ja estada aponduda per la permission «{permission:s}»" + "additional_urls_already_added": "URL addicionadal «{url:s}'» es ja estada aponduda per la permission «{permission:s}»", + "migration_0015_yunohost_upgrade": "Aviada de la mesa a jorn de YunoHost...", + "migration_0015_main_upgrade": "Aviada de la mesa a nivèl generala...", + "migration_0015_patching_sources_list": "Mesa a jorn del fichièr sources.lists...", + "migration_0015_start": "Aviar la migracion cap a Buster", + "migration_description_0017_postgresql_9p6_to_11": "Migrar las basas de donadas de PostgreSQL 9.6 cap a 11", + "migration_description_0016_php70_to_php73_pools": "Migrar los fichièrs de configuracion php7.0 cap a php7.3", + "migration_description_0015_migrate_to_buster": "Mesa a nivèl dels sistèmas Debian Buster e YunoHost 4.x", + "migrating_legacy_permission_settings": "Migracion dels paramètres de permission ancians...", + "log_app_config_apply": "Aplicar la configuracion a l’aplicacion « {} »", + "log_app_config_show_panel": "Mostrar lo panèl de configuracion de l’aplicacion « {} »", + "log_app_action_run": "Executar l’accion de l’aplicacion « {} »", + "diagnosis_basesystem_hardware_model": "Lo modèl del servidor es {model}", + "backup_archive_cant_retrieve_info_json": "Obtencion impossibla de las informacions de l’archiu « {archive} »... Se pòt pas recuperar lo fichièr info.json (o es pas un fichièr json valid).", + "app_packaging_format_not_supported": "Se pòt pas installar aquesta aplicacion pr’amor que son format es pas pres en carga per vòstra version de YunoHost. Deuriatz considerar actualizar lo sistèma." } From ea5971e355edca8d133431c00e2f1c538714a85e Mon Sep 17 00:00:00 2001 From: Krzysztof Nowakowski Date: Mon, 22 Mar 2021 21:50:28 +0000 Subject: [PATCH 357/363] Translated using Weblate (Polish) Currently translated at 1.5% (10 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/pl/ --- locales/pl.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 7ff9fbcd2..76ce2f408 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -1,3 +1,12 @@ { - "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków" -} \ No newline at end of file + "password_too_simple_1": "Hasło musi mieć co najmniej 8 znaków", + "app_already_up_to_date": "{app:s} jest obecnie aktualna", + "app_already_installed": "{app:s} jest już zainstalowane", + "already_up_to_date": "Nic do zrobienia. Wszystko jest obecnie aktualne.", + "admin_password_too_long": "Proszę wybrać hasło krótsze niż 127 znaków", + "admin_password_changed": "Hasło administratora zostało zmienione", + "admin_password_change_failed": "Nie można zmienić hasła", + "admin_password": "Hasło administratora", + "action_invalid": "Nieprawidłowa operacja '{action:s}'", + "aborting": "Przerywanie." +} From 87b0b10e0e6853c79cb786ada4a28ddf1a747548 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Mar 2021 20:24:31 +0100 Subject: [PATCH 358/363] Enforce permissions on /var/cache/yunohost --- data/hooks/conf_regen/01-yunohost | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/hooks/conf_regen/01-yunohost b/data/hooks/conf_regen/01-yunohost index a6d672f57..30828c462 100755 --- a/data/hooks/conf_regen/01-yunohost +++ b/data/hooks/conf_regen/01-yunohost @@ -50,6 +50,8 @@ do_init_regen() { chown root:root /etc/ssowat/conf.json.persistent mkdir -p /var/cache/yunohost/repo + chown root:root /var/cache/yunohost + chmod 700 /var/cache/yunohost } do_pre_regen() { @@ -142,6 +144,9 @@ do_post_regen() { find /etc/yunohost/certs/ -type f -exec chmod 640 {} \; find /etc/yunohost/certs/ -type d -exec chmod 750 {} \; + chown root:root /var/cache/yunohost + chmod 700 /var/cache/yunohost + # Misc configuration / state files chown root:root $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) chmod 600 $(ls /etc/yunohost/{*.yml,*.yaml,*.json,mysql,psql} 2>/dev/null) From d98ec6ce35489af8b2de53c1e01ffd5f2b7c0825 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Mar 2021 20:25:52 +0100 Subject: [PATCH 359/363] Download ynh_setup_source stuff to /var/cache/yunohost/download --- data/helpers.d/utils | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 28d2352fa..0f02630a4 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -21,6 +21,9 @@ YNH_APP_BASEDIR=$([[ "$(basename $0)" =~ ^backup|restore$ ]] && echo '../setting # Requires YunoHost version 2.6.4 or higher. ynh_exit_properly () { local exit_code=$? + + rm -rf "/var/cache/yunohost/download/" + if [ "$exit_code" -eq 0 ]; then exit 0 # Exit without error if the script ended correctly fi @@ -127,8 +130,13 @@ ynh_setup_source () { src_filename="${source_id}.${src_format}" fi + # (Unused?) mecanism where one can have the file in a special local cache to not have to download it... local local_src="/opt/yunohost-apps-src/${YNH_APP_ID}/${src_filename}" + + mkdir -p /var/cache/yunohost/download/${YNH_APP_ID}/ + src_filename="/var/cache/yunohost/download/${YNH_APP_ID}/${src_filename}" + if test -e "$local_src" then cp $local_src $src_filename @@ -672,7 +680,7 @@ ynh_compare_current_package_version() { # Check validity of the comparator if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then - ynh_die --message="Invialid comparator must be : lt, le, eq, ne, ge, gt" + ynh_die --message="Invalid comparator must be : lt, le, eq, ne, ge, gt" fi # Return the return value of dpkg --compare-versions From e27ac6ff2c07117d7f9c0ab7f4830f185e7b0d83 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Mar 2021 20:28:11 +0100 Subject: [PATCH 360/363] Rely on YNH_APP_BASEDIR to check the sources/ dir --- data/helpers.d/utils | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 0f02630a4..905b7b2ac 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -196,18 +196,18 @@ ynh_setup_source () { fi # Apply patches - if (( $(find $YNH_CWD/../sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" )) + if (( $(find $YNH_APP_BASEDIR/sources/patches/ -type f -name "${source_id}-*.patch" 2> /dev/null | wc --lines) > "0" )) then (cd "$dest_dir" - for p in $YNH_CWD/../sources/patches/${source_id}-*.patch + for p in $YNH_APP_BASEDIR/sources/patches/${source_id}-*.patch do patch --strip=1 < $p done) || ynh_die --message="Unable to apply patches" fi # Add supplementary files - if test -e "$YNH_CWD/../sources/extra_files/${source_id}"; then - cp --archive $YNH_CWD/../sources/extra_files/$source_id/. "$dest_dir" + if test -e "$YNH_APP_BASEDIR/sources/extra_files/${source_id}"; then + cp --archive $YNH_APP_BASEDIR/sources/extra_files/$source_id/. "$dest_dir" fi } From 44637237ce97cfaac21256384030fad04716c59e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 24 Mar 2021 19:31:59 +0000 Subject: [PATCH 361/363] Translated using Weblate (German) Currently translated at 86.5% (545 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/de/ --- locales/de.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 832fc1b91..bfc9c36a4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -609,5 +609,12 @@ "regenconf_pending_applying": "Wende die anstehende Konfiguration für die Kategorie {category} an...", "regenconf_failed": "Konnte die Konfiguration für die Kategorie(n) {categories} nicht neu erstellen", "regenconf_dry_pending_applying": "Überprüfe die anstehende Konfiguration, welche für die Kategorie {category}' aktualisiert worden wäre…", - "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden" + "regenconf_would_be_updated": "Die Konfiguration wäre für die Kategorie '{category}' aktualisiert worden", + "restore_system_part_failed": "Die Systemteile '{part:s}' konnten nicht wiederhergestellt werden", + "restore_removing_tmp_dir_failed": "Ein altes, temporäres Directory konnte nicht entfernt werden", + "restore_not_enough_disk_space": "Nicht genug Speicher (Speicher: {free_space:d} B, benötigter Speicher: {needed_space:d} B, Sicherheitspuffer: {margin:d} B)", + "restore_may_be_not_enough_disk_space": "Dein System scheint nicht genug Speicherplatz zu haben (frei: {free_space:d} B, benötigter Platz: {needed_space:d} B, Sicherheitspuffer: {margin:d} B)", + "restore_extracting": "Packe die benötigten Dateien aus dem Archiv aus…", + "restore_already_installed_apps": "Folgende Apps können nicht wiederhergestellt werden, weil sie schon installiert sind: {apps}", + "regex_with_only_domain": "Du kannst regex nicht als Domain verwenden, sondern nur als Pfad" } From c2af36a6529de465da9b0c6060644a286f5de1ce Mon Sep 17 00:00:00 2001 From: Flavio Cristoforetti Date: Wed, 24 Mar 2021 14:03:54 +0000 Subject: [PATCH 362/363] Translated using Weblate (Italian) Currently translated at 100.0% (630 of 630 strings) Translation: YunoHost/core Translate-URL: https://translate.yunohost.org/projects/yunohost/core/it/ --- locales/it.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 9f8f6a167..45b85d548 100644 --- a/locales/it.json +++ b/locales/it.json @@ -674,5 +674,9 @@ "diagnosis_mail_blacklist_reason": "Il motivo della blacklist è: {reason}", "diagnosis_mail_blacklist_listed_by": "Il tuo IP o dominio {item} è nella blacklist {blacklist_name}", "diagnosis_backports_in_sources_list": "Sembra che apt (il package manager) sia configurato per utilizzare le backport del repository. A meno che tu non sappia quello che stai facendo, scoraggiamo fortemente di installare pacchetti tramite esse, perché ci sono alte probabilità di creare conflitti con il tuo sistema.", - "diagnosis_basesystem_hardware_model": "Modello server: {model}" + "diagnosis_basesystem_hardware_model": "Modello server: {model}", + "postinstall_low_rootfsspace": "La radice del filesystem ha uno spazio totale inferiore ai 10 GB, ed è piuttosto preoccupante! Consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem. Se vuoi installare YunoHost ignorando questo avviso, esegui nuovamente il postinstall con l'argomento --force-diskspace", + "domain_remove_confirm_apps_removal": "Rimuovere questo dominio rimuoverà anche le seguenti applicazioni:\n{apps}\n\nSei sicuro di voler continuare? [{answers}]", + "diagnosis_rootfstotalspace_critical": "La radice del filesystem ha un totale di solo {space}, ed è piuttosto preoccupante! Probabilmente consumerai tutta la memoria molto velocemente! Raccomandiamo di avere almeno 16 GB per la radice del filesystem.", + "diagnosis_rootfstotalspace_warning": "La radice del filesystem ha un totale di solo {space}. Potrebbe non essere un problema, ma stai attento perché potresti consumare tutta la memoria velocemente... Raccomandiamo di avere almeno 16 GB per la radice del filesystem." } From 8751dcdd1b2c9c5133dc1997419ef84861147097 Mon Sep 17 00:00:00 2001 From: Kayou Date: Thu, 25 Mar 2021 01:05:41 +0100 Subject: [PATCH 363/363] Update changelog for 4.2 (#1195) * Update changelog for 4.2 * changelog: Improving error semantic * Update changelog Co-authored-by: Alexandre Aubin --- debian/changelog | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/debian/changelog b/debian/changelog index 0454553ae..f8211b82e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,39 @@ -yunohost (4.2) unstable; urgency=low +yunohost (4.2.0) testing; urgency=low - - Placeholder for 4.2 to satisfy CI / debian build during dev + - [mod] Python2 -> Python3 ([#1116](https://github.com/yunohost/yunohost/pull/1116), a97a9df3, 1387dff4, b53859db, f5ab4443, f9478b93, dc6033c3) + - [mod] refactoring: Drop legacy-way of passing arguments in hook_exec, prevent exposing secrets in command line args ([#1096](https://github.com/yunohost/yunohost/pull/1096)) + - [mod] refactoring: use regen_conf instead of service_regen_conf in settings.py (9c11fd58) + - [mod] refactoring: More consistent local CA management for simpler postinstall ([#1062](https://github.com/yunohost/yunohost/pull/1062)) + - [mod] refactoring: init folders during .deb install instead of regen conf ([#1063](https://github.com/yunohost/yunohost/pull/1063)) + - [mod] refactoring: init ldap before the postinstall ([#1064](https://github.com/yunohost/yunohost/pull/1064)) + - [mod] refactoring: simpler and more consistent logging initialization ([#1119](https://github.com/yunohost/yunohost/pull/1119), 0884a0c1) + - [mod] code-quality: add CI job to auto-format code, fix linter errors ([#1142](https://github.com/yunohost/yunohost/pull/1142), [#1161](https://github.com/yunohost/yunohost/pull/1161), 97f26015, [#1162](https://github.com/yunohost/yunohost/pull/1162)) + - [mod] misc: Prevent the installation of apache2 ... ([#1148](https://github.com/yunohost/yunohost/pull/1148)) + - [mod] misc: Drop old cache rules for .ms files, not relevant anymore ([#1150](https://github.com/yunohost/yunohost/pull/1150)) + - [fix] misc: Abort postinstall if /etc/yunohost/apps ain't empty ([#1147](https://github.com/yunohost/yunohost/pull/1147)) + - [mod] misc: No need for mysql root password anymore ([#912](https://github.com/YunoHost/yunohost/pull/912)) + - [fix] app operations: wait for services to finish reloading (4a19a60b) + - [enh] ux: Improve error semantic such that the webadmin can autoredirect to the proper log view ([#1077](https://github.com/yunohost/yunohost/pull/1077), [#1187](https://github.com/YunoHost/yunohost/pull/1187)) + - [mod] cli/api: Misc command and routes renaming / aliasing ([#1146](https://github.com/yunohost/yunohost/pull/1146)) + - [enh] cli: Add a new "yunohost app search" command ([#1070](https://github.com/yunohost/yunohost/pull/1070)) + - [enh] cli: Add '--remove-apps' (and '--force') options to "yunohost domain remove" ([#1125](https://github.com/yunohost/yunohost/pull/1125)) + - [enh] diagnosis: Report low total space for rootfs ([#1145](https://github.com/yunohost/yunohost/pull/1145)) + - [fix] upnp: Handle port closing ([#1154](https://github.com/yunohost/yunohost/pull/1154)) + - [fix] dyndns: clean old madness, improve update strategy, improve cron management, delete dyndns key upon domain removal ([#1149](https://github.com/yunohost/yunohost/pull/1149)) + - [enh] helpers: Adding composer helper ([#1090](https://github.com/yunohost/yunohost/pull/1090)) + - [enh] helpers: Upgrade n to v7.0.2 ([#1178](https://github.com/yunohost/yunohost/pull/1178)) + - [enh] helpers: Add multimedia helpers and hooks ([#1129](https://github.com/yunohost/yunohost/pull/1129), 47420c62) + - [enh] helpers: Normalize conf template handling for nginx, php-fpm, systemd and fail2ban using ynh_add_config ([#1118](https://github.com/yunohost/yunohost/pull/1118)) + - [fix] helpers, doc: Update template for the new doc (grav) ([#1167](https://github.com/yunohost/yunohost/pull/1167), [#1168](https://github.com/yunohost/yunohost/pull/1168), 59d3e387) + - [enh] helpers: Define YNH_APP_BASEDIR to be able to properly point to conf folder depending on the app script we're running ([#1172](https://github.com/yunohost/yunohost/pull/1172)) + - [enh] helpers: Use jq / output-as json to get info from yunohost commands instead of scraping with grep ([#1160](https://github.com/yunohost/yunohost/pull/1160)) + - [fix] helpers: Misc fixes/enh (b85d959d, db93b82b, ce04570b, 07f8d6d7) + - [fix] helpers: download ynh_setup_source stuff in /var/cache/yunohost to prevent situations where it ends up in /etc/yunohost/apps/ (d98ec6ce) + - [i18n] Translations updated for Catalan, Chinese (Simplified), Czech, Dutch, French, German, Italian, Occitan, Polish - -- Alexandre Aubin Wed, 20 Jan 2021 05:19:58 +0100 + Thanks to all contributors <3 ! (Bram, Christian W., Daniel, Dave, Éric G., Félix P., Flavio C., Kay0u, Krzysztof N., ljf, Mathieu M., Miloš K., MrMorals, Nils V.Z., penguin321, ppr, Quentí, Radek S, Scapharnaum, Sébastien M., xaloc33, yalh76, Yifei D.) + + -- Alexandre Aubin Thu, 25 Mar 2021 01:00:00 +0100 yunohost (4.1.7.4) stable; urgency=low