From 73760c4a7da7b28fa56c748f35b4b3d43c5f88d8 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Thu, 3 Mar 2016 18:50:10 +0100 Subject: [PATCH 001/250] [enh] implement password checks with cracklib to detect too weak passwords --- src/yunohost/user.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ec7dd539c..8e2bf4d63 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -32,12 +32,18 @@ import errno import subprocess import math import re +import cracklib from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger logger = getActionLogger('yunohost.user') +def _check_password(password): + try: + cracklib.VeryFascistCheck(password) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + " : " + str(e) ) def user_list(auth, fields=None, filter=None, limit=None, offset=None): """ @@ -110,6 +116,9 @@ def user_create(auth, username, firstname, lastname, mail, password, from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf + # Ensure sufficiently complex password + _check_password(password) + # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ 'uid' : username, @@ -291,6 +300,9 @@ def user_update(auth, username, firstname=None, lastname=None, mail=None, new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname if change_password: + # Ensure sufficiently complex password + _check_password(change_password) + char_set = string.ascii_uppercase + string.digits salt = ''.join(random.sample(char_set,8)) salt = '$1$' + salt + '$' From 0ec81b43993b383036a4c8e877f0299e9a1e15c0 Mon Sep 17 00:00:00 2001 From: Julien Malik Date: Thu, 3 Mar 2016 18:51:23 +0100 Subject: [PATCH 002/250] [enh] add python-cracklib to dependencies --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 80f562e76..dabfa0566 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.3.5.1) , python-psutil, python-requests, python-dnspython - , python-apt, python-miniupnpc + , python-apt, python-miniupnpc, python-cracklib , glances , dnsutils, bind9utils, unzip, git, curl, cron , ca-certificates, netcat-openbsd, iproute From e2567c82f1a1871985163ad4b308e78aa5a276a6 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Tue, 31 Jul 2018 17:30:26 +0200 Subject: [PATCH 003/250] Add su directive as option for logroate --- data/helpers.d/backend | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 8dce2df06..43cbec1f0 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -1,8 +1,9 @@ # Use logrotate to manage the logfile # -# usage: ynh_use_logrotate [logfile] [--non-append] +# usage: ynh_use_logrotate [logfile] [--non-append|--append] [specific_user/specific_group] # | arg: logfile - absolute path of logfile # | arg: --non-append - (Option) Replace the config file instead of appending this new config. +# | arg: specific_user : run logrotate as the specified user and group. If not specified logrotate is runned as root. # # If no argument provided, a standard directory will be use. /var/log/${app} # You can provide a path with the directory only or with the logfile. @@ -13,6 +14,7 @@ # Unless you use the option --non-append ynh_use_logrotate () { local customtee="tee -a" + local user_group="${3:-}" if [ $# -gt 0 ] && [ "$1" == "--non-append" ]; then customtee="tee" # Destroy this argument for the next command. @@ -29,6 +31,12 @@ ynh_use_logrotate () { else local logfile="/var/log/${app}/*.log" # Without argument, use a defaut directory in /var/log fi + local su_directive="" + if [[ -n $user_group ]]; then + su_directive=" # Run logorotate as specific user - group + su ${user_group#*/} ${user_group%/*}" + fi + cat > ./${app}-logrotate << EOF # Build a config file for logrotate $logfile { # Rotate if the logfile exceeds 100Mo @@ -47,6 +55,7 @@ $logfile { notifempty # Keep old logs in the same dir noolddir + $su_directive } EOF sudo mkdir -p $(dirname "$logfile") # Create the log directory, if not exist From 36fd98e0207da5063456e52ff333f5b00ebd13b8 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Fri, 3 Aug 2018 09:38:39 +0200 Subject: [PATCH 004/250] Fix switch user/group --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 43cbec1f0..94e26350c 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -34,7 +34,7 @@ ynh_use_logrotate () { local su_directive="" if [[ -n $user_group ]]; then su_directive=" # Run logorotate as specific user - group - su ${user_group#*/} ${user_group%/*}" + su ${user_group%/*} ${user_group#*/}" fi cat > ./${app}-logrotate << EOF # Build a config file for logrotate From 06276a621b9005f08f4878ddd8221b31a675eea3 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 27 Aug 2018 03:40:50 +0200 Subject: [PATCH 005/250] [enh] Check password in cli/api --- data/actionsmap/yunohost.yml | 3 +++ src/yunohost/app.py | 3 +++ src/yunohost/settings.py | 15 ++++++++++++--- src/yunohost/tools.py | 33 ++++++++++++++++++++++++++++++++- src/yunohost/user.py | 7 +------ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8509bfb23..9c8ac191c 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1489,6 +1489,9 @@ tools: --ignore-dyndns: help: Do not subscribe domain to a DynDNS service action: store_true + --force-password: + help: Use this if you really want to set a weak password + action: store_true ### tools_update() update: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 1fed09425..f405a4070 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2251,6 +2251,9 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): raise MoulinetteError(errno.EINVAL, m18n.n('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')) + elif arg_type == 'password': + from yunohost.tools import _check_password + _check_password(arg_value) args_dict[arg_name] = arg_value # END loop over action_args... diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index aba6e32b3..e694a861a 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -35,6 +35,12 @@ DEFAULTS = OrderedDict([ ("example.int", {"type": "int", "default": 42}), ("example.string", {"type": "string", "default": "yolo swag"}), ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), + # Control the way password are checked + # -1 No control + # 0 Just display weak password info in debug + # 1 Warn user about weak password + # 2 Raise an error when the user put a weak password + ("security.password.check_mode", {"type": "int", "default": 2}), ]) @@ -90,9 +96,12 @@ def settings_set(key, value): received_type=type(value).__name__, expected_type=key_type)) elif key_type == "int": if not isinstance(value, int) or isinstance(value, bool): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type)) + if isinstance(value, str): + value=int(value) + else: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) elif key_type == "string": if not isinstance(value, basestring): raise MoulinetteError(errno.EINVAL, m18n.n( diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f9ee14994..b46d61259 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -32,6 +32,7 @@ import logging import subprocess import pwd import socket +import cracklib from xmlrpclib import Fault from importlib import import_module from collections import OrderedDict @@ -53,6 +54,7 @@ from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation, OperationLogger +from yunohost.settings import settings_get # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' @@ -127,6 +129,8 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password + + _check_password(new_password) try: auth.update("cn=admin", { "userPassword": _hash_user_password(new_password), @@ -250,7 +254,8 @@ def _is_inside_container(): @is_unit_operation() -def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): +def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, + force_password=False): """ YunoHost post-install @@ -268,6 +273,10 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): raise MoulinetteError(errno.EPERM, m18n.n('yunohost_already_installed')) + # Check password + if not force_password: + _check_password(password) + if not ignore_dyndns: # Check if yunohost dyndns can handle the given domain # (i.e. is it a .nohost.me ? a .noho.st ?) @@ -299,6 +308,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): else: dyndns = False + operation_logger.start() logger.info(m18n.n('yunohost_installing')) @@ -1046,3 +1056,24 @@ class Migration(object): @property def description(self): return m18n.n("migration_description_%s" % self.id) + +def _check_password(password): + security_level = settings_get('security.password.check_mode') + if security_level == -1: + return + try: + if password in ["yunohost", "olinuxino", "olinux"]: + raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + + ' : it is based on a (reversed) dictionary word' ) + + try: + cracklib.VeryFascistCheck(password) + except ValueError as e: + raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + " : " + str(e) ) + except MoulinetteError as e: + if security_level >= 2: + raise + elif security_level == 1: + logger.warn(e.strerror) + else: + logger.debug(e.strerror) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 7c5b847a2..ae7edfa2e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -39,15 +39,10 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.service import service_status from yunohost.log import is_unit_operation +from yunohost.tools import _check_password logger = getActionLogger('yunohost.user') -def _check_password(password): - try: - cracklib.VeryFascistCheck(password) - except ValueError as e: - raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + " : " + str(e) ) - def user_list(auth, fields=None): """ List users From c5b5f2eb35e0d7e41de49fee6db2f33609683808 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Mon, 27 Aug 2018 20:03:08 +0200 Subject: [PATCH 006/250] [fix] 'dyndns update' should check the upstream dns record fix #1063 --- src/yunohost/dyndns.py | 61 +++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 785b0dd34..e9542ed95 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -37,14 +37,14 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, rm from moulinette.utils.network import download_json +from moulinette.utils.process import check_output + from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.utils.network import get_public_ip logger = getActionLogger('yunohost.dyndns') -OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip' -OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6' DYNDNS_ZONE = '/etc/yunohost/dyndns/zone' RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile( @@ -95,7 +95,7 @@ def _dyndns_available(provider, domain): domain -- The full domain that you'd like.. e.g. "foo.nohost.me" Returns: - True if the domain is avaible, False otherwise. + True if the domain is available, False otherwise. """ logger.debug("Checking if domain %s is available on %s ..." % (domain, provider)) @@ -187,32 +187,6 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, old_ipv4, old_ipv6 = (None, None) # (default values) - if os.path.isfile(OLD_IPV4_FILE): - old_ipv4 = read_file(OLD_IPV4_FILE).rstrip() - - if os.path.isfile(OLD_IPV6_FILE): - old_ipv6 = read_file(OLD_IPV6_FILE).rstrip() - - # Get current IPv4 and IPv6 - ipv4_ = get_public_ip() - ipv6_ = get_public_ip(6) - - if ipv4 is None: - ipv4 = ipv4_ - - if ipv6 is None: - ipv6 = ipv6_ - - logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) - logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) - - # no need to update - if old_ipv4 == ipv4 and old_ipv6 == ipv6: - logger.info("No updated needed.") - return - else: - logger.info("Updated needed, going on...") - # If domain is not given, try to guess it from keys available... if domain is None: (domain, key) = _guess_current_dyndns_domain(dyn_host) @@ -252,6 +226,30 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, 'zone %s' % host, ] + + old_ipv4 = check_output("dig @91.224.148.92 +short %s" % domain).strip() + old_ipv6 = check_output("dig @91.224.148.92 +short aaaa %s" % domain).strip() + + # Get current IPv4 and IPv6 + ipv4_ = get_public_ip() + ipv6_ = get_public_ip(6) + + if ipv4 is None: + ipv4 = ipv4_ + + if ipv6 is None: + ipv6 = ipv6_ + + logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6)) + logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6)) + + # no need to update + if old_ipv4 == ipv4 and old_ipv6 == ipv6: + logger.info("No updated needed.") + return + else: + logger.info("Updated needed, going on...") + dns_conf = _build_dns_conf(domain) # Delete the old records for all domain/subdomains @@ -301,11 +299,6 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, logger.success(m18n.n('dyndns_ip_updated')) - if ipv4 is not None: - write_to_file(OLD_IPV4_FILE, ipv4) - if ipv6 is not None: - write_to_file(OLD_IPV6_FILE, ipv6) - def dyndns_installcron(): """ From 45ad53eb431c4c73245deeb6dc21c3fa07b0f51f Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Mon, 27 Aug 2018 21:44:22 +0200 Subject: [PATCH 007/250] Add the internal helper ynh_handle_getopts_args --- data/helpers.d/getopts | 194 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 data/helpers.d/getopts diff --git a/data/helpers.d/getopts b/data/helpers.d/getopts new file mode 100644 index 000000000..1cc66da8a --- /dev/null +++ b/data/helpers.d/getopts @@ -0,0 +1,194 @@ +#!/bin/bash + +# Internal helper design to allow helpers to use getopts to manage their arguments +# +# [internal] +# +# example: function my_helper() +# { +# declare -Ar args_array=( [a]=arg1= [b]=arg2= [c]=arg3 ) +# local arg1 +# local arg2 +# local arg3 +# ynh_handle_getopts_args "$@" +# +# [...] +# } +# my_helper --arg1 "val1" -b val2 -c +# +# usage: ynh_handle_getopts_args "$@" +# | arg: $@ - Simply "$@" to tranfert all the positionnal arguments to the function +# +# This helper need an array, named "args_array" with all the arguments used by the helper +# that want to use ynh_handle_getopts_args +# Be carreful, this array has to be an associative array, as the following example: +# declare -Ar args_array=( [a]=arg1 [b]=arg2= [c]=arg3 ) +# Let's explain this array: +# a, b and c are short options, -a, -b and -c +# arg1, arg2 and arg3 are the long options associated to the previous short ones. --arg1, --arg2 and --arg3 +# For each option, a short and long version has to be defined. +# Let's see something more significant +# declare -Ar args_array=( [u]=user [f]=finalpath= [d]=database ) +# +# NB: Because we're using 'declare' without -g, the array will be declared as a local variable. +# +# Please keep in mind that the long option will be used as a variable to store the values for this option. +# For the previous example, that means that $finalpath will be fill with the value given as argument for this option. +# +# Also, in the previous example, finalpath has a '=' at the end. That means this option need a value. +# So, the helper has to be call with --finalpath /final/path, --finalpath=/final/path or -f /final/path, the variable $finalpath will get the value /final/path +# If there's many values for an option, -f /final /path, the value will be separated by a ';' $finalpath=/final;/path +# For an option without value, like --user in the example, the helper can be called only with --user or -u. $user will then get the value 1. +# +# To keep a retrocompatibility, a package can still call a helper, using getopts, with positional arguments. +# The "legacy mode" will manage the positional arguments and fill the variable in the same order than they are given in $args_array. +# e.g. for `my_helper "val1" val2`, arg1 will be filled with val1, and arg2 with val2. +ynh_handle_getopts_args () { + # Manage arguments only if there's some provided + set +x + if [ $# -ne 0 ] + then + # Store arguments in an array to keep each argument separated + local arguments=("$@") + + # For each option in the array, reduce to short options for getopts (e.g. for [u]=user, --user will be -u) + # And built parameters string for getopts + # ${!args_array[@]} is the list of all keys in the array (A key is 'u' in [u]=user, user is a value) + local getopts_parameters="" + local key="" + for key in "${!args_array[@]}" + do + # Concatenate each keys of the array to build the string of arguments for getopts + # Will looks like 'abcd' for -a -b -c -d + # If the value of a key finish by =, it's an option with additionnal values. (e.g. --user bob or -u bob) + # Check the last character of the value associate to the key + if [ "${args_array[$key]: -1}" = "=" ] + then + # For an option with additionnal values, add a ':' after the letter for getopts. + getopts_parameters="${getopts_parameters}${key}:" + else + getopts_parameters="${getopts_parameters}${key}" + fi + # Check each argument given to the function + local arg="" + # ${#arguments[@]} is the size of the array + for arg in `seq 0 $(( ${#arguments[@]} - 1 ))` + do + # And replace long option (value of the key) by the short option, the key itself + # (e.g. for [u]=user, --user will be -u) + # Replace long option with = + arguments[arg]="${arguments[arg]//--${args_array[$key]}/-${key} }" + # And long option without = + arguments[arg]="${arguments[arg]//--${args_array[$key]%=}/-${key}}" + done + done + + # Read and parse all the arguments + # Use a function here, to use standart arguments $@ and be able to use shift. + parse_arg () { + # Read all arguments, until no arguments are left + while [ $# -ne 0 ] + do + # Initialize the index of getopts + OPTIND=1 + # Parse with getopts only if the argument begin by -, that means the argument is an option + # getopts will fill $parameter with the letter of the option it has read. + local parameter="" + getopts ":$getopts_parameters" parameter || true + + if [ "$parameter" = "?" ] + then + ynh_die "Invalid argument: -${OPTARG:-}" + elif [ "$parameter" = ":" ] + then + ynh_die "-$OPTARG parameter requires an argument." + else + local shift_value=1 + # Use the long option, corresponding to the short option read by getopts, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$parameter]%=}" + # If this option doesn't take values + # if there's a '=' at the end of the long option name, this option takes values + if [ "${args_array[$parameter]: -1}" != "=" ] + then + # 'eval ${option_var}' will use the content of 'option_var' + eval ${option_var}=1 + else + # Read all other arguments to find multiple value for this option. + # Load args in a array + local all_args=("$@") + + # If the first argument is longer than 2 characters, + # There's a value attached to the option, in the same array cell + if [ ${#all_args[0]} -gt 2 ]; then + # Remove the option and the space, so keep only the value itself. + all_args[0]="${all_args[0]#-${parameter} }" + # Reduce the value of shift, because the option has been removed manually + shift_value=$(( shift_value - 1 )) + fi + + # Then read the array value per value + for i in `seq 0 $(( ${#all_args[@]} - 1 ))` + do + # If this argument is an option, end here. + if [ "${all_args[$i]:0:1}" == "-" ] || [ -z "${all_args[$i]}" ] + then + # Ignore the first value of the array, which is the option itself + if [ "$i" -ne 0 ]; then + break + fi + else + # Declare the content of option_var as a variable. + eval ${option_var}="" + # Else, add this value to this option + # Each value will be separated by ';' + if [ -n "${!option_var}" ] + then + # If there's already another value for this option, add a ; before adding the new value + eval ${option_var}+="\;" + fi + eval ${option_var}+=\"${all_args[$i]}\" + shift_value=$(( shift_value + 1 )) + fi + done + fi + fi + + # Shift the parameter and its argument(s) + shift $shift_value + done + } + + # LEGACY MODE + # Check if there's getopts arguments + if [ "${arguments[0]:0:1}" != "-" ] + then + # If not, enter in legacy mode and manage the arguments as positionnal ones. + echo "! Helper used in legacy mode !" + for i in `seq 0 $(( ${#arguments[@]} -1 ))` + do + # Use getopts_parameters as a list of key of the array args_array + # Remove all ':' in getopts_parameters + getopts_parameters=${getopts_parameters//:} + # Get the key from getopts_parameters, by using the key according to the position of the argument. + key=${getopts_parameters:$i:1} + # Use the long option, corresponding to the key, as a variable + # (e.g. for [u]=user, 'user' will be used as a variable) + # Also, remove '=' at the end of the long option + # The variable name will be stored in 'option_var' + local option_var="${args_array[$key]%=}" + + # Store each value given as argument in the corresponding variable + # The values will be stored in the same order than $args_array + eval ${option_var}+=\"${arguments[$i]}\" + done + else + # END LEGACY MODE + # Call parse_arg and pass the modified list of args as an array of arguments. + parse_arg "${arguments[@]}" + fi + fi + set -x +} From 0c33ad50fc1c7e07746c27cf278a92c605df6c8f Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 28 Aug 2018 03:09:12 +0200 Subject: [PATCH 008/250] [enh] Validate pwd with Online Pwned List --- src/yunohost/utils/password.py | 244 +++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 src/yunohost/utils/password.py diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py new file mode 100644 index 000000000..bacf91502 --- /dev/null +++ b/src/yunohost/utils/password.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2013 YunoHost + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +import sys +import re +import os +import errno +import logging +import cracklib + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from moulinette.utils.network import download_text +from yunohost.settings import settings_get + +PWDDICT_PATH = '/usr/local/share/dict/cracklib/' + +class HintException(Exception): + + def __init__(self, message): + # Call the base class constructor with the parameters it needs + super(HintException, self).__init__(message) + + self.criticity = 'error' + + def warn_only(self): + return m18n.n(e.args[0] + '_warn', **e.kwargs) + + def error(self): + return m18n.n(e.args[0], **e.kwargs) + + +class PasswordValidator(object): + """ + PasswordValidator class validate password + Derivated from Nextcloud (AGPL-3) + https://github.com/nextcloud/password_policy/blob/fc4f77052cc248b4e68f0e9fb0d381ab7faf28ad/lib/PasswordValidator.php + """ + # List of validators (order is important) + # We keep the online_pwned_list at the end to check only if needed + validators = ['length', 'numeric', 'upper_lower', 'special', + 'ynh_common_list', 'cracklib_list', 'online_pwned_list'] + + def __init__(self, profile): + self.profile = profile + self.config = None + self._get_config() + + def validate(self, password, old=None, validator=None): + """ + Validate a password and raise error or display a warning + """ + + result = {'error': [], 'warn_only': []} + # Check a specific validator only if enabled + if validator is not None and self.config[validator] == 'disabled': + return result + elif validator is not None: + try: + getattr(self, 'check_' + validator)(password, old) + except HintException as e: + criticity = self.config[validator] + if "warn_only" in [e.criticity, criticity]: + criticity = "warn_only" + result[criticity].append(e) + return result + + # Run all validators + for validator in self.validators: + if result['error'] and validator.endswith('_list'): + break + res = self.validate(password, old, validator) + result['error'] = result['error'] + res['error'] + result['warn_only'] = result['warn_only'] + res['warn_only'] + + # Build a concatenate message + message = [] + for error in result['error']: + message.append(error.error) + for warn in result['warn_only']: + message.append(warn.warn_only) + message = "\n".join(message) + + # Raise an error or warn the user according to criticity + if result['error']: + raise MoulinetteError(errno.EINVAL, message) + elif result['warn_only']: + logger.warn(message) + return result['warn_only'] + + def check_length(self, password, old=None): + """ + Check if password matches the minimum length defined by the admin + """ + + if len(password) < self.config['min_length.error']: + if self.config['min_length.warn'] == self.config['min_length.error']: + e = HintException('password_length', + min_length=min_length) + else: + e = HintException('password_length_warn', + min_length=self.config['min_length.error'], + better_length=self.config['min_length.warn'] + raise e + + if len(password) < self.config['min_length.warn']: + e = HintException('password_length', + min_length=self.config['min_length.warn']) + e.criticity = 'warn_only' + raise e + + def check_numeric(self, password, old=None): + """ + Check if password contains numeric characters + """ + + if re.match(r'\d', password) is None: + raise HintException('password_numeric') + + def check_upper_lower(self, password, old=None): + """ + Check if password contains at least one upper and one lower case + character + """ + + if password.lower() == password or password.upper() == password: + raise HintException('password_upper_lower') + + def check_special(self, password, old=None): + """ + Check if password contains at least one special character + """ + + if re.match(r'\w+'): + raise HintException('password_special') + + def check_ynh_common_list(self, password, old=None): + """ + Check if password is a common ynh password + """ + + if password in ["yunohost", "olinuxino", "olinux"]: + raise HintException('password_listed') + + def check_cracklib_list(self, password, old=None): + """ + Check password with cracklib dictionnary from the config + https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials + """ + + error_dict = self.config['cracklib.error'] + warn_dict = self.config['cracklib.warn'] + + self._check_cracklib_list(password, old, error_dict) + + if error_dict == warn_dict: + return + try: + self._check_cracklib_list(password, old, warn_dict) + except HintException as e: + e.criticity = 'warn_only' + raise e + + def check_online_pwned_list(self, password, old=None, silent=True): + """ + Check if a password is in the list of breached passwords from + haveibeenpwned.com + """ + + from hashlib import sha1 + from moulinette.utils.network import download_text + hash = sha1(password).hexdigest() + range = hash[:5] + needle = (hash[5:]) + + try: + hash_list = download_text('https://api.pwnedpasswords.com/range/' + + range) + except MoulinetteError as e: + if not silent: + raise + else: + if hash_list.find(needle) != -1: + raise HintException('password_listed') + + def _check_cracklib_list(self, password, old=None, pwd_dict): + try: + cracklib.VeryFascistCheck(password, old, + os.path.join(PWDDICT_PATH, pwd_dict)) + except ValueError as e: + # We only want the dictionnary check of cracklib, not the is_simple + # test. + if str(e) not in ["is too simple", "is a palindrome"]: + raise HintException('password_listed', pwd_list=pwd_dict) + + def _get_config(self, name=None): + """ + Build profile config from settings + """ + + if name is None: + if self.config is not None: + return self.config + self.config = {} + self.config['mode'] = _get_setting('mode') + for validator in self.validators: + self._get_config(validator) + self.config['min_length.error'] = _get_setting('min_length.error') + self.config['min_length.warn'] = _get_setting('min_length.warn') + self.config['cracklib.error'] = _get_setting('cracklib.error') + self.config['cracklib.warn'] = _get_setting('cracklib.warn') + + return self.config + + self.config[name] = settings_get('security.password' + self.profile + '.' + + name) + + if self.config[name] == 'error' and self.config['mode'] == 'warn_only': + self.config[name] = self.config['mode'] + elif self.config[name] in ['error', 'warn_only'] and + self.config['mode'] == 'disabled': + + def _get_setting(self, setting): + return settings_get('security.password' + self.profile + '.' + setting) + From 67e82111c14721acb5d3384b28b5a3a3cfcb1ccc Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 28 Aug 2018 03:09:43 +0200 Subject: [PATCH 009/250] [enh] Validate pwd with Online Pwned List --- data/actionsmap/yunohost.yml | 12 ++++++ debian/install | 1 + locales/en.json | 12 ++++++ src/yunohost/settings.py | 45 ++++++++++++++++++--- src/yunohost/tools.py | 38 ++++++++--------- src/yunohost/user.py | 4 +- src/yunohost/utils/password.py | 74 ++++++++++++++++++---------------- 7 files changed, 121 insertions(+), 65 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 9c8ac191c..6108da07b 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1450,6 +1450,18 @@ tools: pattern: *pattern_password required: True + ### tools_validatepw() + validatepw: + action_help: Validate a password + api: PUT /validatepw + arguments: + -p: + full: --password + extra: + password: ask_password + pattern: *pattern_password + required: True + ### tools_maindomain() maindomain: action_help: Check the current main domain, or change it diff --git a/debian/install b/debian/install index e9c79e963..c616db73a 100644 --- a/debian/install +++ b/debian/install @@ -4,6 +4,7 @@ data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/yunoprompt.service /etc/systemd/system/ +data/other/password/* /usr/local/share/dict/cracklib/ data/other/* /usr/share/yunohost/yunohost-config/moulinette/ data/templates/* /usr/share/yunohost/templates/ data/helpers /usr/share/yunohost/ diff --git a/locales/en.json b/locales/en.json index 074512311..c876f1832 100644 --- a/locales/en.json +++ b/locales/en.json @@ -329,6 +329,18 @@ "packages_no_upgrade": "There is no package to upgrade", "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", + "password_length_error": "Password needs to be at least {min_length} characters long", + "password_length_warn": "It would be better if your password was at least {min_length} characters long", + "password_length_warn_error": "Password needs to be at least {min_length} characters long. {better} would be better", + "password_length_warn_warn": "It would be better if your password was at least {better} characters long", + "password_numeric_error": "Password needs to contain at least one numeric character", + "password_numeric_warn": "It would better if your password contained at least one numeric character", + "password_upper_lower_error": "Password needs to contain at least one lower and one upper case character", + "password_upper_lower_warn": "It would be bettre if your password contained at least one lower and one upper case character", + "password_special_error": "Password needs to contain at least one special character.", + "password_special_warn": "It would be better if your password contained at least one special character", + "password_listed_error": "Password is in a well known list. Please make it unique.", + "password_listed_warn": "Password is in a known list. It would be better to make it unique.", "path_removal_failed": "Unable to remove path {:s}", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index e694a861a..2151ee6fd 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -30,17 +30,50 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" # * enum (in form a python list) # we don't store the value in default options +PWD_CHOICES = ["error", "warn_only", "disabled"] +PWD_DEFAULT_ERROR = {"type": "enum", "default": "error", + "choices": PWD_CHOICES} + DEFAULTS = OrderedDict([ ("example.bool", {"type": "bool", "default": True}), ("example.int", {"type": "int", "default": 42}), ("example.string", {"type": "string", "default": "yolo swag"}), ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), - # Control the way password are checked - # -1 No control - # 0 Just display weak password info in debug - # 1 Warn user about weak password - # 2 Raise an error when the user put a weak password - ("security.password.check_mode", {"type": "int", "default": 2}), + + # Password Validation + + ("security.password.admin.mode", PWD_DEFAULT_ERROR), + ("security.password.user.mode", PWD_DEFAULT_ERROR), + ("security.password.admin.length", PWD_DEFAULT_ERROR), + ("security.password.user.length", PWD_DEFAULT_ERROR), + ("security.password.admin.min_length.error", {"type": "int", "default": 8}), + ("security.password.user.min_length.error", {"type": "int", "default": 8}), + ("security.password.admin.min_length.warn", {"type": "int", "default": 12}), + ("security.password.user.min_length.warn", {"type": "int", "default": 8}), + ("security.password.admin.special", PWD_DEFAULT_ERROR), + ("security.password.user.special", PWD_DEFAULT_ERROR), + ("security.password.admin.numeric", PWD_DEFAULT_ERROR), + ("security.password.user.numeric", PWD_DEFAULT_ERROR), + ("security.password.admin.upper_lower", PWD_DEFAULT_ERROR), + ("security.password.user.upper_lower", PWD_DEFAULT_ERROR), + ("security.password.admin.ynh_common_list", PWD_DEFAULT_ERROR), + ("security.password.user.ynh_common_list", PWD_DEFAULT_ERROR), + ("security.password.admin.common_list", PWD_DEFAULT_ERROR), + ("security.password.user.common_list", PWD_DEFAULT_ERROR), + ("security.password.admin.cracklib_list", PWD_DEFAULT_ERROR), + ("security.password.user.cracklib_list", PWD_DEFAULT_ERROR), + ("security.password.admin.cracklib_list.error", {"type": "string", "default": + "1000000-most-used"}), + ("security.password.admin.cracklib_list.warn", {"type": "string", "default": + "1000000-most-used"}), + ("security.password.user.cracklib_list.error", {"type": "string", "default": + "100000-most-used"}), + ("security.password.user.cracklib_list.warn", {"type": "string", "default": + "1000000-most-used"}), + ("security.password.admin.online_pwned_list", {"type": "enum", "default": "disabled", + "choices": PWD_CHOICES}), + ("security.password.user.online_pwned_list", {"type": "enum", "default": "disabled", + "choices": PWD_CHOICES}), ]) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index b46d61259..ccfbbc28a 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -129,8 +129,9 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password + from yunohost.utils.password import PasswordValidator - _check_password(new_password) + PasswordValidator('admin').validate(new_password) try: auth.update("cn=admin", { "userPassword": _hash_user_password(new_password), @@ -143,6 +144,18 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) +def tools_validatepw(password): + """ + Validate password + + Keyword argument: + password + + """ + from yunohost.utils.password import PasswordValidator + PasswordValidator('user').validate(password) + + @is_unit_operation() def tools_maindomain(operation_logger, auth, new_domain=None): """ @@ -275,7 +288,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Check password if not force_password: - _check_password(password) + from yunohost.utils.password import PasswordValidator + PasswordValidator('admin').validate(password) if not ignore_dyndns: # Check if yunohost dyndns can handle the given domain @@ -1057,23 +1071,3 @@ class Migration(object): def description(self): return m18n.n("migration_description_%s" % self.id) -def _check_password(password): - security_level = settings_get('security.password.check_mode') - if security_level == -1: - return - try: - if password in ["yunohost", "olinuxino", "olinux"]: - raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + - ' : it is based on a (reversed) dictionary word' ) - - try: - cracklib.VeryFascistCheck(password) - except ValueError as e: - raise MoulinetteError(errno.EINVAL, m18n.n('password_too_weak') + " : " + str(e) ) - except MoulinetteError as e: - if security_level >= 2: - raise - elif security_level == 1: - logger.warn(e.strerror) - else: - logger.debug(e.strerror) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ae7edfa2e..4299f0df7 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -120,7 +120,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas from yunohost.app import app_ssowatconf # Ensure sufficiently complex password - _check_password(password) + PasswordValidator('user').validate(password) # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ @@ -309,7 +309,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if change_password: # Ensure sufficiently complex password - _check_password(change_password) + PasswordValidator('user').validate(change_password) new_attr_dict['userPassword'] = _hash_user_password(change_password) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index bacf91502..1a660aea0 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -32,21 +32,25 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.network import download_text from yunohost.settings import settings_get +logger = logging.getLogger('yunohost.utils.password') + PWDDICT_PATH = '/usr/local/share/dict/cracklib/' class HintException(Exception): - def __init__(self, message): + def __init__(self, message, **kwargs): # Call the base class constructor with the parameters it needs super(HintException, self).__init__(message) - + self.kwargs = kwargs self.criticity = 'error' + @property def warn_only(self): - return m18n.n(e.args[0] + '_warn', **e.kwargs) + return m18n.n(self.args[0] + '_warn', **self.kwargs) + @property def error(self): - return m18n.n(e.args[0], **e.kwargs) + return m18n.n(self.args[0] + '_error', **self.kwargs) class PasswordValidator(object): @@ -114,13 +118,12 @@ class PasswordValidator(object): if len(password) < self.config['min_length.error']: if self.config['min_length.warn'] == self.config['min_length.error']: - e = HintException('password_length', - min_length=min_length) + raise HintException('password_length', + min_length=self.config['min_length.error']) else: - e = HintException('password_length_warn', + raise HintException('password_length_warn', min_length=self.config['min_length.error'], - better_length=self.config['min_length.warn'] - raise e + better_length=self.config['min_length.warn']) if len(password) < self.config['min_length.warn']: e = HintException('password_length', @@ -133,7 +136,7 @@ class PasswordValidator(object): Check if password contains numeric characters """ - if re.match(r'\d', password) is None: + if re.search(r'\d', password) is None: raise HintException('password_numeric') def check_upper_lower(self, password, old=None): @@ -150,7 +153,7 @@ class PasswordValidator(object): Check if password contains at least one special character """ - if re.match(r'\w+'): + if re.match(r'^\w*$', password): raise HintException('password_special') def check_ynh_common_list(self, password, old=None): @@ -158,7 +161,8 @@ class PasswordValidator(object): Check if password is a common ynh password """ - if password in ["yunohost", "olinuxino", "olinux"]: + if password in ["yunohost", "olinuxino", "olinux", "raspberry", "admin", + "root", "test"]: raise HintException('password_listed') def check_cracklib_list(self, password, old=None): @@ -167,8 +171,8 @@ class PasswordValidator(object): https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials """ - error_dict = self.config['cracklib.error'] - warn_dict = self.config['cracklib.warn'] + error_dict = self.config['cracklib_list.error'] + warn_dict = self.config['cracklib_list.warn'] self._check_cracklib_list(password, old, error_dict) @@ -190,7 +194,7 @@ class PasswordValidator(object): from moulinette.utils.network import download_text hash = sha1(password).hexdigest() range = hash[:5] - needle = (hash[5:]) + needle = (hash[5:].upper()) try: hash_list = download_text('https://api.pwnedpasswords.com/range/' + @@ -202,7 +206,7 @@ class PasswordValidator(object): if hash_list.find(needle) != -1: raise HintException('password_listed') - def _check_cracklib_list(self, password, old=None, pwd_dict): + def _check_cracklib_list(self, password, old, pwd_dict): try: cracklib.VeryFascistCheck(password, old, os.path.join(PWDDICT_PATH, pwd_dict)) @@ -212,33 +216,33 @@ class PasswordValidator(object): if str(e) not in ["is too simple", "is a palindrome"]: raise HintException('password_listed', pwd_list=pwd_dict) - def _get_config(self, name=None): + def _get_config(self): """ Build profile config from settings """ - if name is None: - if self.config is not None: - return self.config - self.config = {} - self.config['mode'] = _get_setting('mode') - for validator in self.validators: - self._get_config(validator) - self.config['min_length.error'] = _get_setting('min_length.error') - self.config['min_length.warn'] = _get_setting('min_length.warn') - self.config['cracklib.error'] = _get_setting('cracklib.error') - self.config['cracklib.warn'] = _get_setting('cracklib.warn') + def _set_param(name): + self.config[name] = self._get_setting(name) + if self.config[name] == 'error' and self.config['mode'] == 'warn_only': + self.config[name] = self.config['mode'] + elif self.config[name] in ['error', 'warn_only'] and \ + self.config['mode'] == 'disabled': + self.config[name] = 'disabled' + + if self.config is not None: return self.config + self.config = {} + self.config['mode'] = self._get_setting('mode') + for validator in self.validators: + _set_param(validator) + for param in ['min_length.', 'cracklib_list.']: + self.config[param + 'error'] = self._get_setting(param + 'error') + self.config[param + 'warn'] = self._get_setting(param + 'warn') - self.config[name] = settings_get('security.password' + self.profile + '.' - + name) + return self.config - if self.config[name] == 'error' and self.config['mode'] == 'warn_only': - self.config[name] = self.config['mode'] - elif self.config[name] in ['error', 'warn_only'] and - self.config['mode'] == 'disabled': def _get_setting(self, setting): - return settings_get('security.password' + self.profile + '.' + setting) + return settings_get('security.password.' + self.profile + '.' + setting) From 783c5126287a042007bab7f268811b74bb60e9a1 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 28 Aug 2018 08:56:12 +0200 Subject: [PATCH 010/250] [enh] PasswordValidator without Moulinette --- data/other/password/100000-most-used.hwm | Bin 0 -> 1024 bytes data/other/password/100000-most-used.pwd | Bin 0 -> 405035 bytes data/other/password/100000-most-used.pwi | Bin 0 -> 24132 bytes locales/en.json | 21 +- src/yunohost/app.py | 4 +- src/yunohost/settings.py | 37 +-- src/yunohost/tools.py | 12 +- src/yunohost/user.py | 7 +- src/yunohost/utils/password.py | 301 +++++++++-------------- 9 files changed, 147 insertions(+), 235 deletions(-) create mode 100644 data/other/password/100000-most-used.hwm create mode 100644 data/other/password/100000-most-used.pwd create mode 100644 data/other/password/100000-most-used.pwi diff --git a/data/other/password/100000-most-used.hwm b/data/other/password/100000-most-used.hwm new file mode 100644 index 0000000000000000000000000000000000000000..e78de4a46cae924d3c0bd341a0be942e6133a032 GIT binary patch literal 1024 zcmeIwzY75Y0LAg|lSNT7*>5s18f3OJUCF30806$1xLXxcerzU#zu?LwshgCtFk-w# z8Kl_x1E_oTS>E`1@ZY%@a4o~YfnpC@eYg$bG=k_DoC!RqzhPsFZ)OXqxsdfBd4Sjv z%rlgHSl_{ZKs6+m5#c6;Z3sUlGHGGu#5^y&g4k6=y#DSQV$~F8TZApK?TCFh5Nz=I Fffq=>4Kn}$ literal 0 HcmV?d00001 diff --git a/data/other/password/100000-most-used.pwd b/data/other/password/100000-most-used.pwd new file mode 100644 index 0000000000000000000000000000000000000000..c794b055035f3aa4d1ab8e6257d3e97a7cc842fc GIT binary patch literal 405035 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ð9}G$?vMz!g!r1N6X!NI(KH2 zut2SEi%kOLnM*okpA1@^8Uv03410JLW*unuzDNPMjSF1dX@!>#Da_r`5&8^ME6pmb z8_=re$M%KDhyQk=S^9(+$2sfiH@^K%+S>cb)3aH;{+@g$N)cf54aqC2*48Ukzv_Q^P zPeVHH&kI5#ytah&$!z)W>4cl^WCeW5(a6>UjjekK2V+K;~ zj_`ntK=3>}f9MTDWMaxPi2|7uWKry%OG}7P9@&sSKwWW!Gy5bsRSN6AMVThWk&l2# zkK8(b)iCnX2nEC$T>x(~j;ejlI#pf7H|YS0&iN>j_}4Ifdi zMb$*qkCYH7Tg?ngf;`fZ$dz@R&lzDeCHP2B)G`pRHMQ`ZV$?0oPM+?rdl)+fowm>* z%4$?5*@N_SXk?KK7A&>7XcwkCD4q~k4_pHDG*HR1)op)2nXLc@7pO|2%w1(Clyn@5 zr^Oy6*<9YPpD&(@TNX=QKb(nM;S<=ChAYSpdk)uPz_u`TU;9^vwn4gi$4B@X5>1yMiCKD2F+VH%1ABCuIUK zKn(&rW-kQ~^|S<}v-4F zzhE@%D{cxqv`dr-YNlWmunnOU%>Ati2;*rQH|HU#&v_en(U|G_x56LGsz>5+v=UKXVd;Yo6tEW3CF*2 z;-Qx!cs`=6;)D}8%9j^*Qy1s#y5$4~z4{6z?6Cy~OG03pM=))hQyLMGa258lg^4pZ zV33n-BNpXLqX+H1ty@$lAp?RK)hJ;a67tu=*I5g1NyKA`(GkT8Y#s`QXBP-9EKHb; zvVTZOy9;AAq8W&5u)-w73jVB?nmN&|NuwKPM?1czHZ0KLlS~l{N^x+=jMOJT2dZ)K zE}VyRzcikENX~jKQ;rlzr;iImgq1O(n|C}%ob!K)nX7QO>qJR6cC5M%XY zq-K@#?zE;|=ZRh=WfrQvDRQC*&~%PlHDr&o`TzoBF`=+Bq@y+xjeM~~X|kUtLcoYw zE--+ZtVPp3oou&zXQYAUv$_(>VyG`;hioQ+V~8Rnb&GlARoW%N^C-ALun^cu2TsmpqY-sLR&aM(t8#mLA5W zfskU>@1?1}(uFyNGM%>jBl)*`oZ!ahF^wn&h$gy5$btm`1>*P{7BH;sR*)vuFa{4G zD+g050Porys%i0Mt=51>T){G#%000t;$`?u?btWeg9@#steva??*sDLk)4I!=+ZnGkU z9ov@}1m)O45ew0Qw9Sk)X%sEQrdT>)biu_a0?pzfBsUUr5E2Wq5j+>R5^KdUf_I3~ zwGAfFud7G6=;l!45KlNL;7~WSG_57(oyQx%EM%ZARC84JlOGrNKnV-Vxw&)7d?ZF8 zR1OZJgUD+Nob$#h7yvGO3GF0rGYxn_$WaEU{B(6p41!?rnHhlCD1{#aI63Vqd9woc zId(BCqC%OF22&8^`?1=A=}?XPVkZ{ULLG=4B&mZ00Rw#8rgiY*IS6J+=!va%1ayHa z1_2$b)-W~c1U|xx04F10{(?-6@?XIrRoa$iz~S_dEzakF5fR;HE+ChpZP^lP#{FV$ zS_>QjI%hFbg(eb=DJTR)fWG5Pt^PDadIZNQKL~+T5wZeDE_Y{Y%8OIr0UrWHk;pnN zbH%+;I4r7I7xcn+mJj(1Yvn6x2uTy}snG$F3lw0ukUsM_6(p4}*b7Xbq!qwm&0L2G z!B(my8i(YGJe$OYcpKUnz@WDSoDPXZ#5An@hX~*}16s=~sG^9?Ao&sRXGq5^mydoV zN8ENEX9Gci+5rJ-iz&PBm)?1W;Bs}*U1*!N=P@YPu@CKND&w1Ss6?3ftDdgp;=@i= z&h3{HWbB?2OquhMjqHV;bv80Udb7-OjIE_| z#tLEDdW;AodckSqEel4RsI?I-UW$!g^>Lt%688@$dccIt0MteKD?2i9O0oRuX=+bX z;o)B%L&Cz`fgrbTD!XXs%QpKG&?d7o>lW=3obU%QD@92CYeV&s#2rm2Je(X=GG<<{ znxZCVP;Af;5o+t@6Afa&XdAax$&aSMmr+2<>2wSFg9M$TK-IyJ6mopRC1l3VfFKI9 zGYy2QMx{_bj9IuIbtub(s4c4tVHor0Fhd2kV{ve*<0+Te-xEP!ZGSxCKn*Ah(M4kg zW$f6%+-4Ol2SJi1HYZC4SosV#hweK_00@(?q|`7Z76*122wUb}LEyoP(TSYqOe1}y zEC4Xvi4&(BfKenIu406R1NVrl4F8)5jlj&r zV@#l`k!T(lNPAGbazt66goYkGf`j!c4w5+2e*zq|t?;s8_7FoD$eSrt$ZAhv5ID|! z#jNokQ=|oyNcI`T@tV1z$3}DzbwJxg={z8Ejro*7S_Qio z!(x*Wmevu3eL=(-6%zYxbyZ00kg?$0j1rke09A&0;p8ftjEqz)5!kJWcN2qP-b!^2 z%;I#@UO0(fz34$@1j?={{NW(8@SiC@lIF2*g(3MmXq*?95-8`ULDC$`g$o{90|xd0 zGH5&a1UW@ZLCw)^Ac=z5UgHN*3Gbuu!fDzr_Rk(sj(rW}0OqF0jrZO&&Fd#C*SQb6 z-z%QRYYwal$n*7RxOdn1H2e+DVqjA_A#Oif5BD>JG@h4#f`>4mo8i8k5IXJ+h8}{~ z?NM#R_DI^aDZFWK_x3!AC$ZU@Yi{*m8t`Fnz;CAlGN>&vH2^Q1l(_bQk^%VJ9}Alc z!4eflcj#R55mt|M%5+&^WK{{s89+cSDrW;M2;eg8`_wi;H*y^yV}AxqfHJguAq`#) zbY81(kK_PdEDXD}m!2{~PfBJ+0>L4a4<)>TwNtQm@fr%?aJL&LvF0G}2no315drB( za66OM)f90oVMf@m13S7a0MATQ5NPqGPY)tn77QF*&hv^oMq@;e;q;6L%w1@vmHH0p zPyx<;ZS+A5Ijo@=3u$FPJPzoLy!E&}-Q|anKrR}Gfq$Lbxvi;r+eC3}m~&CpA1=M; z1>xtAl)`=H>3|Re*)1Z;Xhu6uRiuv{FX;Kc+~IU1RZC?S`%Hi-8UrAAha@Z`7fo|H zRN~|>Q>c7&df_vvia@Wlk0sqigu$|egSv)<<`aN!*#?+`Oz9d?#w&Lc3d}wt1S(0Z z2wLK+u^UmeLFr8(q+Y<3pnBBNFASZ9$0^Y0f@oW%)q%I6Ra1{dYYCHMwBs?xl5rUk zAzWClvw|_jRy$2e9&eUb{Rw0KI)x5`9(vcl;uC&HWGD~-`R?Ir8BqhPJ9d{<% z1qPFrG6rf*;5{K`N*UDQLt=Hm4WPPd3NCi3usT4bnr)dceG;x%}Tj3?N^Cwg^*%_d(5+ z!Q5z!oTDvkVyTuA@W3|G!$a(BaMB957F1Sp4lA5lPIT;92Fg>o*A zFQ6EMP~nOoUUTgMN|{{=6Fhv+0002+IB+xWp;Fr#P^G)KY*D;rH3q=Ihb6&<6fBullx8m(?+>l(2r z55P)bFa($?v|RTczpe<8fQ2Z4peb)OO*keLdzn_sg1|lml<^91=4m9dL+6Ffe0J~g zVNV9}sA@GUCt1XqSMBK*XOn_WU?pupsA7{*cmVZf^@TE)2?eRQIR`5aze4`uqqSFv ze~`e#FU!@Im~GUPFfS$?TfULkhB8u;14c}X%HHN{KGva)Vm5~6lc=^G6R3c!5luAIeLt zYWUgsbg$%v<6;X0sUy~7%O`Ch=s239`79MtzYyUjEOAi`1h~7Not2wQ~sO8@c=6;L~y_u?ii%>8R^*U^|5GMK&2sh9&!i_SxU906?y`2vQW}3& zjvAtdEGmm-ToS38DE~02lFADsw8xU53}j(vsxmZ9^6rE$t({+LlqH8>bS5&(5AjKR zB(JFsZetR@w=cI0AH;4R><17)%ZH;oYV_{-%|v3-zS9$@Wk&}E870YzMX=tKBQi)pB@WC<8TjMbi}_%m87}?o#^D)h2IlWy0M^_gah2QC7O$d z@UR1=*3*%4yvX>8qw$$GJ_c#2xaEHO)A07f^vBHegNM+s8PYQ`L|tB&8ErS5CHPd9 zzBrATpBOu&71$o1hn)jy=E#V zi$=|yL<+Q1wauHS$|9L(lW7gz61ry^i*+@1PSeE6SXfp*t<{@<3?`k*TW9O0q??1M zi;L;zyP%)cr%ThEm53WCiO2I`e%K+9)8+Ip0s~z4P?VIa_r<4SGT3~|(O|_PxlEBb zQrUix(G+HEw}TlOb2%&w;;_Th0z+UKFKe@g3c5nQFtNVO>+Y)@h$H_Iy@~nDPG7@t zvalEg%Q5YV{Hrowu-3)hTq&Cm%j}00<<>TJm|@m-0FsBFk(b@kF@7$*P`>cO7ES#P@Fpf?9}eIf|mLE zRB7rzU^kH6wEw9?`pJw>kuOXrLEYh7775M8EwMLj z+gNg`U68SA*yYY_!IV2X%vKN*A7BT4)n6|hwF47Wu;_;{S&RqZm`%RB@KN1CvNgeS zmbCPDb$7wo9Vyo2NM3uKf(1-X>wey(q&+%xAGtAAvRtY1UpGdY)Z%5A z6u{%|lB@qvx47JW*0?s(UZtGy%X7+e#LJlZ`eJnlPZ>g(Ajt22mFK&@{L!(1&A8j+ zrfArtTk+(;g-B$&?Z@nxYhkM-a}uhR^(91ax*gCl{t9SNyl<( zb;Qxop^c3Pj~X*oQ1cNz4$%bdBTD!?VdTDK;1WxU@X*fSW;s@L5uQlKbZ@7FuwUIM z0dHXg=o0J(53?OK)Ft+&vm99kEAd7e{b%j}LrOLWLf;`(BbeH#Zsp-(xc@_(Z%6i_Jtp^ygH>`e4^b3F}+lm z}ArUQOTS43tnY zwTT#isC3;zdw81W*`%PL8Bh(Cj%XW|VyOf$6{5)7)6P7JL2D=1u8PgXO2Qb&0B7HC z@_Yy`7l}H~Q}#;!ev<`DRJ47d1R7y&ig*PcV@~WMkb(E!Oc6&~_@uns@R^kc^DucV zCl*Xtnq1MVCH3_h2o7*B9e}}^gP(1+0RjRj?t!r6QXTr$h7ofMyqs$td(xtpo@<<@ zh5mII0UG&)p`Wfr@nFmK4bau~4_3!tJ8SqYUnGA@#Q9A%GeX9W3qY_on1lz1I%gs9 zepWcoP8K#V;EY@_n$^+MGLQs0~CLvhJrh4;cQEgTv? z<>L>7>88GVJ#8JgjH*52Y*;ESrG}1?ztr61J8M{2WkLX;RbQZVCXSq+Jt|V}7LW5O zNA2L`?209)dy=SE&7EGh1Hfrdym|e&(3`#Lxb3utyK1O{0vdwt8sO1?TBn1Z^kACU z@&xC2t-23kp?2i!_y2jYv0d@m{xN=@kK)Q156 zEa%SRakcx9=E>nf41GfXVWUm*9WXlD?;K`v3j$J1J<=AQ!j{`TLqMi`r%H8#sl$`` zdD!|W@4Eyjyn#r|$l$vtZw}4-;gu42%#oJ^UnwOf=3m&XwXu#~v=y#ILA!b~eS9c( z5)xJ&3j*C)Hvk8CLpu84EJJXhQY%q$zp#$v0nUn=PJ&u%)0WJeq1KEE!Hb$ruz>n`jK z38A&GyzAPirK#JMyG`&tMC~j5<+C?yosu;lic)pS7Z_X{;lx6IIfK5tEemx0+f;v@ zUVS^aqMOalnM6>R|2Q8O(mvBJwEgX^TS}Ht#hGI+*-D&N>=1k!Z0VN%Xsvd68WkGx&apbpQKP-uMiYZi;Ihy#iq18q z(TkUKp+m@kS1O8`BLnZeMY}Om2qX3#cIuUFy_X7ryDMX^1Oty^roCx}X<++fmCj&* zt6Na>cyekl%omAf&R3?2b54eXBy;i1sMw@Ft{K-fKx(Q6cqV?7c$ z+GcO{U*v&LkFn)6;L{MGHvlw`j!pJzkwxj$tsWs_ym9{b@3Vb75lvUDkxuSCJt^$3 z8+L-zm`v~rw#!yUDavSNThA}`;w)v#s9QJ{k$nM~*k+Xn#uuC{112t57@h)mUOA<( z%5Er*c?$@#9Nb4DX(AX(Wpvag>dw5+iEdLcTct%Ne8!Y_fd?a0LU)*(KH|lqcMY*e zI%|Ns!%!w0fN*ds1+aPjm{5ZZK!1GCC_mUmoA)>B@*7u?d*3ZI7AxkE%Tc9y)vF0n zCaSMZ4Ruw>L6IPiqH5GER5lLKTc!P*0fx7T8vB`*meZ4qQY(9UVBLPDwOEojFeO@f zcsQXO9-Io31309&gi!&jw*nKgyNa&|%+?Wq@1cpl7Q)hvF1Y=GQ*&RN!oQ>%k#<^} zNPjBwZqjwQ;%UMAI(ysY^r5Eysjntl~whu8Vd?```~ zyLp(#9EpwIvtZ`rNNCVsKhKOLTRZ4p;$yJ*+pBi@^yye+pqyXlw0h3yF%M5=b0r3d z-Ss|RAkD$f@x;Q-A}$_#@FyudhEJOI@7)jv^bUt0UsdV{Ki=MrA+p*%C;V3z->|Ni}tE2UA^+Y@_y|9o_Bj53CeR0aq#WVB*U#S?&4Fk1*%@b0ZGX`4a2xdgei_; z6x~3#fqX{dH{M3yhT1vS%&5{Ml$Hq+GEkniOkb0bwh#Ms(AAwo)nRq$no@?kbZ{d+ zZ;^@p!hio78a>T!CFe3%csD4s@3@k_WYBjUXbOP9*nO3x65gq3x?I%v(Onb6eY z(->p-1teLArSe_p0RgO^GmT#Zte2sz-!{?N!N4PDNAntwAFrNn#w^4dnylRI4 z8kwWd{LpYbin0`;cjJnR^%9+${|ISoh;ddxCM{$$QjCWT5h{hYUT9Q=iXxatbD(<0Gc)L+zX<;a#al`B1jA;xRw%t5C!-LTf?ygdJhbqR6u)fI7_e{ax#B%I1S( zd434gU$(`dNA5wtgBy0?9T(&tmNo$GwrxVOEDq<>t}sYXrN*w+OIWrmMJ(mmFj*GX zUpAW$OfUR*Qf0sUU3=(-322Q_$Kg}R1Ksj{<+zl!13ZKA!b~-v2)Dzvcfs_B3vw1R zSyUD30kw?@X7Ug6w>%s5IQqffA9LcHI}Y0R#32&TN0s0Me7;0;PLCWjR)kJY@jZ^> z3d_DP*f7W1e7SkOvg9XxfQBDBSW%h2>miuc_4&pMsNu1=GRi`l3e15%9cjH8i@P+f zS27to+s+LG;ET7p2tjFZK8TOO7sltmiIHUzj~bQuDp~6~N_}N(P-FCVh@*#_%`J0m z)aZAENtn~YmK&PW4vI~3P@cD+xcZ{3O!#UJ(2F{4Xw~JSx%+0$>??wmMG? zTPfY6n^7e%n?-fGoboR_2AUJpF zcn?xrNbc9}%gw-Dmz9WW>8xT{tktFAPK|>ap3yRWekI|^_K!j!Ot6D6C)hJZ^si57 z8lGth_iBSy(}Jv zv|nqqtFJ3vm{p7mk!DtqC){bar9dp(Gfa$Xa>S>PDA2?J=7QB$;B&oTmK^O(%YS`Dz(?UW46>kCwA|?VwVk>&!W?SmQdK71$L5UHUNPYHLMTdlT&dq9(Ff2Iv8kz zS1Jx)36+P74z_$V6LgR66NbW1%Q0O>*7K64^(v%IuR|HI)h%u@mecC{C4t*co;`Ux zFQ&bfrDcLxR|NuA?$OLV6^zuqo?MhgFK#k6gj{qt?&MCodmW`U#) z6$c&emWNUYV?&LkUiN!WP{rx}#dbM?HOSKl2*VcKm(ypI!_kL_NezhPKt$9Z!LaxS zZtOj}YWQ<*crCT^uN02ilOX_G2zF`LQ|j7uT-%cAR01?-tEbCNqOi=Clwz?KZ=%_nls(QPF%&RPu9?Mn$tK(r`o*P^3vouhe*4YjVOM35ygpC&W8Vy&w8NDQJ%V_hw#O@TXGFYn`{*CPiPK*yoa9wn z(-Ab#O;zBHqjJpb2MA``*j~SRU5Ky{&$P=l&%N0WFr&rsGsGkBol;Fb8v0y^`5|BU zJQ2_`To&fiq-X-*g|Uu1Jp|R7a<=du+{3SCO$SM|ELY>9^WS!STtw^bR6T}x>%fN_ zGlsq9@Aih2k2B=tp&DZ`dDY@ruNlV^h)5M}cDdAkcH%HmohTV{L_toEKYbB;Yl`+ghrsk$~47@KUfTUorPijQx8Ai5ruJX!toXtGTCs=5=U`; zk>jfQi_EXm5UARNwK&r74rGAhVuBj%?$1+C7rGt%pOVph14m_MOgPumV{>iV@ODIv z80?w*pz?lp6b}3QS76#K@nz>uR6My1;h@|+JzGU3V~?0lj@EplFC6$rEX#2)H}`-X zXP}00AnT}E-~+EL#I^@8NW1iVcU3UBbL1sS!A&6J}4RMm8u$z;yz1%n!g=2nYx^QpOcb?=6@6k5cb^*m+HF(IU!S5WR{Qf<}S$U(k&zn!b?&m01N#51P zV_CX~=QrPZ^V}uJkXv;?d-JT?m=bhV%}~b8IpvLmyYH9J=WcrE!Pz)+uFcw`5K}HA zH>os5JkWn>eiQ#qL$sw_ptkpiWILU^j^PohxoR4#yRCT0zJLaNb4C3!dp>s~DiEwW z(C9|D><@u24^KC4n_E3@HY8f0ha|SYoUoVgYPsE<{loS!$2S}PrG-h+Bj`9em?>OC(JZT0$&W_<72`)6mrj;BdJ=g*`%+vYD7oZlW_{(Vb#Jvsh(`sITo@Kvs) z`{V8Lo42PY+poLNpH3-E|J*%Y{e2IG|i~?|*{x z{m-X2o3Cf5*XggWIiZi3gPL>sS5jD}_OWE+CA1>WJOogs-ASzi59QI56B&-WO5sB_ zOV}jbiz!AE^X<@53i}^!Y?z5k*WFLLNj=;XFqlZc=zK|Ae^KTX#P(h`Z*tnEZz|@L z#oxK)dY58GX`Olo@jf6X6mX-%`-~=@*>SS2t4?pL;L2p+VT2a7jk;m(AZYuUd#7qP zSZkLa5XVf_d~e3e>K=V8hv5B((;JAetDet7-9uk#aKa;$R@dA5e3wIa^Yxsw-uZ(Z zQQakYi@46zt{81-HYd106xoI-5z)#^PUT^Ti3TWUOkg(I*0}hAu!KX#?&>>%+Xfhd zUX(bT3*6I;M(N29IF?tbk)je0tG3zgI$w3=+U>rzCWUWPo#y7;iAkJGg1445N7njS zM8pp1<5Dya8Kc^UK9no~)mwk9UW8Uyf)5%y4Nvqm%1T-F~`P%LLgR|rGwwRFO;IV3_CG3fd@+Cn$S;3XmHE5Z11!6 z3sF!j!;b0(1*=-N#rNL)m-0Mmi4Pl+`LW_|B;lOd*Askkzlv8ClCk;cYNEDidu}Cw zZiGLqmhf4>G*5ff#SB=`#F{wWT8?C{cH>hj&QjD6q8n2(pV7#-Oc*z0zazMkr*QW- zu-L!b-^AW$UN7))vNX3EZga7<(??rJ!;r<3kUN>Z3o_*NvR_%NI~3;k5`c>*0g5q5 z^0Xk`!6M$usE`7f;O67BhTx7{5Dt^%I-waE2LA--!ubkL0rR#_XKfM0DIn z`mg7Qq-HV31YIWWfVva-4;&PnkP&mJG>Qbc>FTa!*jeFu{HPSA_7y0bv~7#l3$!uO zJ#}|~bDG7aOOuhe0rlcJkil=Z!i@w6bnnpKo4RbnJKKOu7RxrSy@Gi=dk4{t4%A5A z`9yF`yn|7FY<&-VwSj5@rz?$_kscufiD{49+}8VY^K|px2&R$Q8~=$3mFEa_;Z$GI zA-yhU8I<;PXV+S9X95dE|LDM3f+ve1xWuy~QqBoeuahaKTr6}!0?So%bu?$rfH&IC zeh!r>KEg0_yHz3CsBgZ!Lp}8#F3J~Ka>7;(^L4|l|nBJM@5J+Wl<{r(S zKuRIu^w6H}F?C0!uC;`tnbN~98^zX{v|B0l1UgHHgJH&}w0vdi`(33$oPpO83I9xA z=_Z@K{=;{T*SQ^v0cSl9 zRnAH5IsbZ_;451_fM|Q}ZkfN@#R6xG(*pL=CIKLVt?fO^6Cc}(X9hYC6(Ca&_>^B# z_ONE84FbX@7JvRbD(~%BBgw&|Pn$*jP!2FGS*%o?agtCm^0_2Z0k?pn z32Z&M>a6%$(Zc+^GiB>(9WD%tkfv5TX%8ko9(~}O8)gHIaH#M%8)h!snFA|nTZg%O zedRE|9hQVLLqw^=#0S5$)#ZoJCto6R$Qp?OLQ3VFb5AX;?5sqLJ9sP*r@#^lY$1&_ zT25rFhKJ0EGyTpJJUhW*)kD(u+m!!r_Y7*`o`Z0EH!4j&l-s5K9(5+2*npcpUDVjk zcnikn!116bv_NdJn~tGu2$SU9$(v!{9i7$cHYFjq4w!T4N6VJDXIPB|jTWRxpMpJE zNw3U82Y9vl!r~-|TTSxUmCZL9 zEx^RJVo;A+lU{sKmhV?9$lz!+%r8DS6Ncx!A6YUU*C;Aoe$a_j$*US7KVzk3ac&-V zNpDUJ|ExuSwg9H5hD1oG1JiG{Bg^PYljf@TWpJ$~>>@-2cr0ZZ^|dRP@5E1O{}t5o zI@)4r8oNhWQO<=o|1x2Nq9FQqCEza`QhSHtCRJe0vK&uXcJ1^Zfi+h%e}cbw$XMQPF6Nre%U{ovzeq-)m?lkHg1ugM4pCn@hdZo>%8W!#&n|hrcp-!!n21fzd#B4Xff~d4e=jNf<>t zUUtb|p-pWBgO^}l2|9P7#DLexqLU=2U1x-nESyi{Pdc4^3^@C3oqm4 z^y@{MVZ#xS?b62oX3IHAZT{Ms_t-Kbb<82(`qSJBcU4&chmvr1Fk?N4cdTu3tk<`Mq=h}G-g@)-%LMe8yCpj)RqHIJlIU3vM_3?ZQ&Hf ze)bUr-<}=6|MUQ@zRp!NbJxoKR1H~e&NjRk*WBQaxYsw2&pa5H>M~jVi=93DaqV8D z)m4{rke5pVvU}Pq_4Q+H{v3`FDyO4`y3fW})X@V0SM&GpQ;RBCfIr;sIu5veoG_Qo z8Q^`d7~12@=8SuSujQ?j!7u0BEal&_u4!;l0AzrUlY)LEZw*;q_DhgqGyT-%h*c9RF@K~-=!?9 ze|-PNCkF!Yu+<~i4m|k5qXY`E%zfpGALlrQYtc-KU*+)kukW7A3-tTu`{u{_RtdJh zznpye%EJ2ib@SN{aBIKWs38UeR$3w)VX!ScT#MG&YXP13U{jH8h|o>$Gu!b zkmhYk<$~m>y4ZQK87`b6%ES<5q<`CTLOnRaKe2(C#nt9OZ3%0PRZrN?AlhaJEzu)q z>>{7vVdCc0fwSp#8EQNQK^0%^seOAtA1ZUA%L%yB{|PYY?XSIzu+CC2F55 zN}YHwjNcdMg9zHdmk!!w1$0#!Ksa2+aQyPP8R7MfeL@+$l^1JG6YEM{rE!ov`s344 zG1PVhRhd-l2W%=pQQx`Rj`j5LcwKDd^W!B_UHv*&CS^TkR8}ulaFD>wWca+k*L`DVUx7utA3Yb;ao+)7?H=PxSOVOOuKU;p+M z&T(;G^c+-q_xycxk5#?<_4s^ortRk9@_hUKmwS0k?FUvf^(TIw$+6i4vh(}PClPmU zd3gLlwN+N~;e;Gicn0Ua^|Jc==GylV~gZ)VIKe)^i z0PSj{#iV1KpabIAUm6EAMAW6==fHXjhwK90BJ-cqp7og^>m4$ej`;i zyVwm=K=G4|g$q$}E=0F<*o(JT2xJ)*_n&h=6Pv>D6t_BXilYbP~wik z9yi)DE#6s;vC+$~F{dn~N!tXm5!MulE)WTRqc(^SM$741BbS|dPA9oMX3=+st>JRy zZes*kbCoRS#DDy_wRr{$BAGZaf-e6j>9=3_I#|kcVp|=e!HRTzJb?{ESpw`9Gbyea zK?6AqqQ=DuvV9EO>?_+bfn*=kU@bvl&y3+hoEh;05_f@@wEuC8k8tYzFxQ6JnZL=_gIN z4Fu#??1yLjkj{=x8)}caP;h>2kntBcQa#vN*xvookXCn^c`YuwcM~ALoL<6NJL(Xe zP_S3aWK2af`?oQE{&p6DFP=*UvtH ztZqj^Fzu8`TE3MVExxfQ;iL$sYm+}Z%sI!#j% z!nQKo**4=x7)rbWJ!kk+R!pEl$$_ejv$PZ@Ml!>aKvP@306-piEqcvS38j=7XF#f9 zjoC7cPXOAI)zwg*k94I11D;ak z5C7)BbaWtGX(nBJ2S;6UVvpT9Bd%39zq?+J6{YIE?b^H<``QQ%?Pose&H=bsut|2`Y0OCSqGw;hSYCJEV~R6~XVM)!yoUxWx?%s#f6|y!b|SNv z(DmV5ix(kaa1#|UZ3+4|A>1WTOY*`v*hw9X%X`^F*U`@}ROmK#kaF3L!$NE=^wHrZ zDbFu(SL1Pei%uF0jT-gs!HM&6SDRhnXWvsJlt79$>u0@Nb1Fbp(*kQXsiWeG`wZ6x z54vC$LqN~SNM*de!i_jyBo*v+LS0CtZ~q`9WS*s-d=?Y|gmv}3wu<_% z*bd8%rp;|GuWomi9)<3K3|f{uPLv6dUMmZipBy4>We;^nAl|mX{mp5dFiUQ-_ApoS?+#&{@jH6>MTP}1Gc^@8_4?qCPZNjOz zKim3eEAr9&3+i^q&J48MXdv#UE;bgwMptKajs~JMA0SC&bMpvR^Y_tOd3 zPIm?C{Ac&(zxQwc&*;^A{WEYHjs-TAx~mUX*J<=4=hrWeAaLqJt5*-d3weGjjn?Gx zBXP_vKb=K%G0n=p!GpdV28>h}9=zBlfCgSbQ;#FawNo>9XRSs=g!mW%?C)@zBijy9 zut_hN><_dQ6Ra_$Kus};!SJUmMj$jshZ$yC9aJj>fWy)~m-$z5D!&iXB3Cnz38V;! zR7mMOEoNQ8vmieliwNd`T6iGJMWZkHP^OBqrDIgPsyzE+^Udj=mpXxxn+*_n3yg`z zi4E$cIdyn_$(6&52L~DO+s}%U`l-oc=L&&>POjKmd(5p!auDF@#4Q?Ajli(w0 z&h@gH2{t;ZY7Q1F#CpN1aoXx;2~J-G_3J)_%$5vM;nkA8vfgE%K!oBS%*#EN35PNU z^#RUqcp=amUXE-(p;pmM7Sl4M*n4{?lA`L3Xc}s&9A{hODwReHvdB0X-7kfd>Y!Y$ zNHw9^tboJV^8Eew_!>_VlU>GixI3|v`YfgD-Lau=y>W&*iccH_jLDVw*s93F=X}pF z%muOg;4}1-Xl#l{>YTTjN!i$Z-z=4F&u^}`n@1O;Z67ykb(GouvbouI2SomoOTJCs z5fr}q@qK&!IDXWBU?wjLf?@Z+Qv&<(*9-5*!O!RzUl%#I`F_voTCONhNZ!}gFK%4N znrie4O}pRZ82Q%|a`asxwU57V?oN*lyv<+vr?~F=>YnrGumQ%2L($TqVCHphkt5lxr94WTSNfQ(^1SA7|8xqxtamocz;NT>r8_O%;MgN3~gaDb~HH zkX?VOe-rZBOfn2d(qL(GGh1~Gu@dVSDU{wIw-m@D>KG+k(J$5x#`To5;;=v-;H@Yc z{GX7W=Q&?&BUoF;4&+vQvpXu`2&XBGy%sHkJ3Vx!U*u$9*z1Ed8QdXy>G9SSy?4l< zlVrqPv6WG8>3%ZP;eg$&o$wD+5+f~SVIdjr$Xj#%Q_Nsrf|c&<8R|@6V_d2fF zPV$%r`UqL9B>v-{Ux5O-&}DcTEVDW3t(dlT0^YN88mh)=&3{(Mhru!z)0g=-h0@{W!1M8L4(HZO0o441S_g^r9n>#^( zLL4rjo}#kPGSpVHCx0f4bfdp`6Is-m=b98H#~h@_hPyn`EI3*EHEm~f1l`fc&@Fzj<{`1p z<2MHxw{TNn5IUHtFXdC1)=k|?=Gb)a%#6AgiV8mUSX~Y%HJDvs5}y#PF776UAqF$! z9F3!JG3>0Y&=`W1?Lb5y`ERMTz_2vkzN?C2*O?5R;{p|+Gg7y#nm`YX-kyt&B(-d` z<~qF-Jx?lrTSvyQGe;VY2XQ@RgR3x*7-x3`g4fv{p45RVQhw^qen#~J(HRS}o>0ul zwb&1(-oJ&M?Z4wdjC+kq*^Z67#0n?_EqQym8_fbuU8e(%d3#}68-%O}8!oZMn_^7Yn1J8FmqTO-yLhF!Dn{e-)7pe3n(!jzY*NczX;So4 z5qva~XGNkx)#QaV-nr|n5iM}3gp_w~^2N?2vajbhvrZ#~;x;u94uHv8-F(BTdVZZY z*}ff`#BSp);S(e0mBhOYrF$CwpR+@4l|(^MIBIHnmURwRGVJ1KF!uZshb90BkZryu zUMHpdYRA!Za4k}=Fa4!yhC4*Sq6Tcex6(kl45w){U;0{nYESn6Ut}uTa%mpz-v>;%+YAHf76I z40h{wG7F%`(6|PNx)}seL3wBcK4S~^r(FsZ?q_)2MT$gQh}X3<`^x+*0)Cl%z-<&4 zk|A>dKuT_s+v__^wa_mgKfwaSI9D~}oLm3Ad36&H8RaSVF9obPo*8>C0u4(ZzZII# z9`kZZXmUN7H}2|-YU()upcE0EhPq`f112gM<&cYslV`EtNV2{M$8@052=Dri93 zZ>qTyD9cKK5!-LfgXPF=40ju31V|c2Zi?B%0oKdK@LjHI!RYG;T1o*jFs=)f#J%cgxjWnD=xHFOa2AB+nekomGLBILsyDHO z;almeL(xDtE76YmmrZCHHWHCm@?ReuCwzN+TI@0P z^0R6OE9OL9tEC%ZP?n}uxIdaB%7P`Sn3)G{c>=e;78i+(1riQ~YYypKbD2CxQxQf8 zp0vIc2s~!O&-J2kCcE1FXcU7f`$NZ}LA`wNtR_IJ6Wr z>e)pLFp{l6o&MRrMY7W)4o?&d<>>KUS!*Gl!>(Z=-olhzQ8(XH1`r zkL6}FbQ1ercGuoQL@9wrotrhgm8Y5Oe@>AhK8V1xWum!7;vJMS2LH^t`M_c;kgKQJ z2t<3x_+~|yU0OjjTw*3YMvyVr4VlfM=RsTDFh6KCfVPPLbthm3lV8mXW-W;@IGToN ztL((MF)W8vj0;fJog%=`1WQblKGH%*Y(`#&EvI9~YZ$st#-}ZL?Nuk>k<*qcR0P?I z%fl;N8rOrtI6X5sT`J_EXc_N`h#R2rAqZENQ5(UMJQuIftg-d|+C_z7OmJ`9IV&wqzM2k;4lSTxK-vkyh58(d~lnTkl( zN4JNx5yWuv4@ZGB2-3`#Z7~3HO{h~0kOi5B>%>nOzn~Va`4`5-92M+_C9#K5Lm*%G z+YV{x0f@DgVU%|7#hmmr{-!httH8*(NQzjp1F7C#RYC?Bt5$iK@Zjv?IuOS(S&iM`vjI&xqSRdk&pO$-ceci zGuRI;4N45MlO}L;&}^swg_aI)%;LupBQkj9>g~uzAE@D{t?!+{S}m4 zbnJ`!@h{!Fwb+D6i?ksS_N3$?7@UW|n&R-Fm9y>N0|i+|q@e{H@1bDujk;?7LJ6z3 z2rZ2yCk9LQ*Y_#+_QLDPdKpTly04P#I3x;bwqeBT6Q$TNIa-umQ*No84V{|LalB*0?O_4zlQWVLF@_bTQ;$@H_6t_F&At zVJnSFXqlz;`7KFhg!a$G=U$>VEHrzn8dz|^UHutThCP7VT*MXfHMK`ulioTQs-fM7 zeP%=l;L&jzmkjgdxp)H)@^9w8H9e+{!*BXoUCEHA4QvliA)rbmHgms|7-&%1912acYdoIkY{Jqg!Zzv{8fih_fObYXW0{>iw+lQfO{?!c3nl5@Xd;H zSsy`ea)D^u&PM4VQnm@mp-wfuuUn!srBqC3sud=Kh5S zAW<%HD<<2R1Z8T@9~Qm@>Mi9A1+Wt(&dy;sU_P9m-T(c93a<+^QDzUTRcB*+@2;HH zV-_ur?GO!$$_aRl=3RgP#p(annN(4^TMpR0|5lj+j@#Ac_Kae7^|OVW>HRtB+coyw zst(#M>&g)+Xmu?6_zyvaRjG8-`EbF$KOV25l7D`vd|(e;)+k!{ND*&Q=+3NPzU!?z z2v6rZaUXtu{r=%(iC^aM%h`L-4d;4$`xGq}v3B*1TR0S(OLD;z9sng^;V~O$#_bL2 z!!G_Ptf*=dK!x&n>rp@U#lgeWrbr|5d@j~aZl1iXc7WXTntHYVF}E-HEC2T(SZz_3$twQnz1O-$AZn z?eYj)NI-sPzAB*B(!o0FBaG8|6xxRRuHk6_Op<0ce(5x{efRKAIYG}Q&G*7sk<0)W)kAuLNyLG-)MB$A&6NNeH=-#fqf~#|+rTCJZ=N2P`#)TlnnyU-cI(@VT#i^$ zy>6@(1=)%}R%zK~ZtmLBzh1s}=Q7<*eB<+n(+`w@)!AUv=GlqE<(}}yD1&1MzO~qn zXU?9>DtTmb01@V0sfX0O%Y??abEnST->`X>!xIm9`Ss}|7W8{}e)-{oQ*xs$iWU<8 zJNxJIAXjd;b1|5RsJFj9e0Hd^+WgvxVEPmJ|JN_Q7Uq(qNjk0M1(^A--`j_;yPyA5 zWaXw3Q>#cbXyiRjQoLg&?T?MQw?OJnK(4b9NvGozXT&1W$4hH+G}^vwT<1D_!loRT z0fexBT_m|MmKDLFd$-X(tIumzSVJY@$?@{cJNC1Hh|m&d2watZE%5CFaY z{&D&9&D*XzI^c0#?QpH~F;{mFf8V^^Y=5%9HgDc+-)|n@cVcIVC!Pd@Ixe56kxy!#*jY@yuS z(+}Ivw)yY(7os=ce!TsBXTyHpU~+FieSW&R{AIts?9N=XwqnMx9KOE)@cyJ5n~&dr zd^mrzb(nP$?iy{KD|qdj*cY<*&DDnwT}e;@pCf@TlZq$ClrfCAl%2(va6-3Tc%!q_ zCq1V1;R6KfaO?!~kgN`IhN!yu%)KRh9GM=T)NK2b$~B=NXv#9i3l1VcHu^dkg5H&CoDH<-)hiB)T4MbpLRhI4sdT~de;N{9W%(vb7ISKz*;f3hx|`?}OGq)VSaP>?X^#`H_AE89Kt# ziilctRF42?bOf^g?JHVotsQ;J;xIp{OIH;Li6I}n$8*}i1Ec%*`ORG01j2rE{(JO( ztQbaZU*%XZ==y>;iMnO-c3X{k>Vj!y5}`?lzGm>h^YBq?{E{29JhPeC<9K+7m0uUh z159ZR#4>Il_qNk+#{;8T5xn1;^oS^o1=?Vf>HVg}`hZgqRhFexp7f~Y=?}}#W_Fpfp4|`daA@zo8@!3m2wy<7Vc)1BlqHRrg0vA z&P5{0LVL9-qhaq-YPpBG_Udjt&Q`#cH>-)vztuSSAeGF&KJ6PFOCOAbo%^>o8pj)d5X!fVlL_&f7Ej&$yONn07xeQcm1=ogT1v>Wzcnr8G<5E~88{c3ibm#~ zb)ap5;nl!IuyL#WuIx^78?ZISm)Ma*4fg)PQq?3oD2=Sxu6?|K)aqc1AKIC&8{J9k z1!Wkz9HjRxMU}_L>{9T)@3toO<7V&p@5n6eEyZnN3X8_kg??a8nVf{J!>(VeFYS2l z=eV0LY@k?Wy8J%L2xaXMy5fjg@044+h<|VfOySJ(2z+ajItvkZ+V2UG1zw?UVok6k z;{!$yUAbd)o%u(j2q`)P81AEo390l+EV-S)8+cIqbdR>o{IAby6Ug{GxE$&%SA)F##)a4obM^hyU%jYL&s3ciZPZB>C}Fr{A)I?f zJM4$>;zW%n6Je*Z*ymk0U{evgH*Idh7J@KS&1eX;2RC;)+bobQ8VVT&B)JARj8W$L zmbPXfd@*J^V)}oX92eh_X!6fyfH^E-?9*&y-M`Gx{4{fb@jn4cd-f=DO)t}^Z2H^;H|+H{e}A=Lodp%GssrT|H3s)Vs}^51Lc__TU07-*NDVP(QKRkn-EshLlvN z$}2q4&;EJuB5L1df2@9`CBjF0+3qhjy%T6c(Oig-Wtmj<8NKH1q-;&;ipqW6NbZG& zrihn4mW-CaulX`ql53EKUi zQm4EEtE#-`Y9vgNr76s|EqYtR@;|lRp7pj$$VCxo}-fMS`p=Zi#}877J5R&dKnKZ^mL)BA_fm9+itz1NNSqcAIIfGw%aN3l z*j&gMTjJ4h6hAYS;I!h0nXh_t2bxX3X zyPayu{^Um)3Voa+S@31_Xt^rT9!Fr%Z+C!R6vdR(th0P-F6bT4>k`NFmyudCpOK)u z9k7hzHHI{Yza35|kUjhAU@2HFQDM>mv~2(t#B6t4 z*LdBA!;`M-zzYD^>2K=_@vb+$*nEE=050C~YE?KU%4Rgag@d1*B5Y7yq=9R#Cm@6H{NLb=fQU3B@?eO@|qd_NWrrhEV8B!mGs6uG**LszSB z?^$~5O}-tBcYLMZU2~>$MRqyK{nplRD`JnkOhK3LAod)iE6Q*6gAPt`K^t0vrbRlj zGx_XWx}hR85RKTJK2Zp^bbO`6!P=IMi)~z}yNjO$n7eQmp>$s$648;LKQzHg;s0=Z zhMhiK!-C+5vAsPik!KFWGI)|2fRNZj2#1mB(x2^{U?_*ix-g>&q&;~dOY)=1VFelJEI8|Cj*N_puP-fA>Qxr_y)=f9g0e$zHu$}Ws-)~GOQeE@O zcM&B-h||wCMhV{%Maln6u}RqjH3fXIkSKR6)p{z?$K2*{QO5buva?8tIi-F8*VYzo zr^TXHy>h#LzxgmMPcmM`im^Mez|JkM9KfFJF|M}$f za|PFsJs&;luN>j2l8$`u;2zb;wuP@$MEnCRcKLOXUfTKPS9Cx@8nm#(2qxa*xekRE zWha*WSMIqlpL_G>ufN~?^|7j{f0ZF9bev9%r#EHIXgPt|o897RS*%clJ9_XVR%QL4 zHH+{;V;FTi_v(?+xYl8O^3>ABA;XpS5j-cNNEfS;8Wq zc%~HR$diw@I><%&M!a^ifiV~GURcd2{&G7*ded-&O8#l)Cj zb?ru=(D|aUe%bCG9H`z6Mw!@n_>x444@*5arOU6mmt}Me=%A~uwhtiWseB3dd}p&; zlDc*SkF*a{FZ$oKtcBTpz@h*bxyLb51C4IOoo0x;eK=soSd8X@`Z%YH@0#bFS5-gi zV4}sTw~l+wVDTcQo1?0NT3%fwojr|uQz>U0H_XA-uli z&xS}>QqES)A7hX9bPd>u!a z*y6~=QN%6dncWUxy={K^qND9#y?;52ee-`#hq+B})5o|8yQab$5zI>M z|NX|n@2K_%y`cp~SSls?Z*{S#{O5q>qne-cx&MCHS?A?|t>?AhDx z`PKD5SKl*pE}fs7+nthAY!G3H=XYQ@*GiQ}xq9B7_YcdJcO?9tDR)-x&ss*~=Tkb) zzbTRH-`k}))YmV#$m&Y$(#Q92wilFfTjZ?U@k9k_{Pykcqu1{@&t*ai%3oato7Ll0 zV;-5%+i!Atl+`kxJ0bF`9VHQ=S{EqsYN)VHRTIcAl?k3S1NxuDg;vG_AC>xcFz^3Lt zvxV*94ZeCVe*?O$ru6>S$=8jq@w#4BnT5-Xq;{k!ZD!b_f&2rg6Ydi#Mhc>q7^Z;m z1)7d~APs4?oPmh6Vbr;U@gyZWrE*qGX6kRLUjw7j{U`lXif1 z3oV)WS@&gZze%<+NgCN9nllgTX#m2m@&5^C1ahlMuBFvdz{ZWJrH#I#B2qrEmo0xQ z{vF#|?m6gWi8GD-hFydx2r5322IL?vwYue()n@A@?ZOxQ%z4=8)~()5+T{hc+o0Ej z@Co5PkhhMD*{Zy^;|B`6Bc!XV8O^R}FF-+1DJP}_5^5%I+_-=Tcj(V_sKXPQ>?dE) zs(hpV%T{q`}~Z?M>^{OM;0wWzAOtRfC9) zTjvD5J&g)q1&%JLw_m=PPg?0=3ge=YWE>DB}wDZzjnq03iwj(FZBYOx%v4osg zC)UIxkacU@Jf;#&DP1;+mp~}TsXnMWUEV(E8L7;s_We{o4PB@Q<9&^KaYfxhJjfC9 z_2YgYufJXgEwaD7{bpf8_~m|q^En3$wsr7OX-AiOJbUUBwGV1SosXW}Cy%2@ysdDYfWlLLie;sV zT2DGxn;`5u&Q<+U8~v1Bu%3H4r#Q6E+P;ZJ0#7@bR6|n}gU+|J)hX^jEHD!cnhHYE zM-@LNrJsm0B(|(ChzypD34(}%hjJ%kE~KXHLWq2TS6^?>o)kt+IkebN(VHA4;c1M( zN=OEwm5YlCy`YphZwkgA@f6oHQ{(`?W8jQ;Nb|Yd5g^sg;Z|Jzf|SFmF^uE7^*l66 zV)*qX$|w1M|9-{Y&_E2Vf|{HjUlY}pRT5DmypXyBAj(hZ;PILIvQ=x6E=3tp(;=;A zU3AN8d|KK;)NwzX>FRuP;zc{)Hna0>0N(~7x}*?xfGe${`h`=ak-^2E~j7) z6WJpJHI zu>d4FWzLbdI9P}8JBG^m4^+IsBO&QNsr}dX`j=F85oQrSn zqn)MFL{i>=%WQYqKWiQt+?5Ev#T%3VD|!bmXS?%htdDs!=E!Va7-Ni*d7F&mjMos= zCya-0VEu@rfe^A~N5p4OCgOStoem0miPzb9Bw1Dt(% zrAq)*X0(5X1o)t4$=ZfB@pPoMh>qIKHJVZF!VrdXkLV^{B4}fB%NLeXY9tfZK$Ggu zLlSK-#=35b{h*5%3}THYyI%aIV{+}eo0@16dpUy2b6^R=%uayx0*cMwhq5OQaiu;r z&5nl-Uz*Bt563=oCqM97%$x)vC}y@Ra#PdfM{$>KjHKLY136PD)@Qd016cI-_KL>W z%%9W7M}z)WI-?OL#`W8GOiP1{3MY=Bf2>|vMuZg)66u3l#(n=63DvGmQ1Zy}MWv3- zkv{N$FQH`_hYAew1^GH0Z7O*i1J=9oC(^I}*WxfGc*A8+Is5df{4fu@rg!rLJI$=F zi);U86?P8@J3<>J*{`+()c^)b=R>Nhr6F?^lkl2UFVfEBZkCJ!$OrrJ;Ns&oH!<>^ zeMOa?_kOM{>Oao6BL@vV553TZg%u6DrcH1+Ht>Y3bmEVspA}lhSXO!eu;t6rK5TUw>xOx!G4Hh2r}ev6Zg19;yOum@U&EsSG-&mPcqtOBC9 zuf;Q`J;Jin`ha3^ET8M4et`f`QuyX@Y@F`fW@UXSEOwi|Vh_=n*Zq||QC_zAPjs>F zl2&RY^f8*I3zw;BE5N+hbTNEKY_cZ24b?gpzb6cqJ!`H>I;wyNszd(xWVWSCZE|d0q`Z3#$ZDB$4S_kmY`v<*tS4s7K!WB z9j-%%$>W{}27P5biAlS*L>CC;?90|=>d&kxCu-EBXyhu!cAA9oV9k-U) zfW*pwc^RjjOvPcZPRsrxKXU#;#L>UI9mqS#Ll^_YB3s@uBETF>6nw9j1E(0O3N}w-wj`@80-31@R_*cJ16^= ze~tsG&wC>iz1+AY8Pwh03xhD`g2IsD&q!w7Z4px6{^vU~-}mb~&1_mu5_xZ<{rQF^ ztAk0QI5nlVaCDgBoffJYI$@dtpYadr1~iWEFE{7+Y1Y&z(bHSrMrTE<+s_ZzvIo}Z zLOf{{7m$_RI1+UxLEza!i)4ByCEE2A$RFc@OcZd(+$|v)KPQ!iE((d0312=4wQ_xg zDtB+*VgB@i;EsaJEt!Ca3qD1ULzv?MX1lohv zzc@1Gh6l_rgRJ#zrTOG;!6$D($aZ3G0XU%hlnb)@Jbm?G8Ybu6#KV;_yS-O5jCh^f zgC}SH?ZYQ$4>y$34$637FknDjZ4KZ*g$2d%yJf|X9kjEsXp20QLXsYaBm?}}Ty(IF zXhr2bo*sYvdLkd-@6Qi4*eLq->)gXHzP&*iN*83&EH^soKHFK6e^*|RriKU~U!Y#-Ud zlFsJNOcVkch{_HE1-wA?vdq@IZWx(!pGwly}h+D|go!!uL zwz)>!MR0b}q8`UedF(6^n7K0{&6TRFgHw)ou32PPx` z#3(|*p||m$kHTeHyukEWZhAiAtl!m6U8R3?w8No^b*Fww()DACA>(>qzG`s>wwz@Z zGvV>Nln0YKG<*fa+X~SP$Q4d->WFnr`4e^jz1=?9SQTQjcB?@jwdVzHajo)oiPpE3 zR1eneLF@cNT(mxcyl-8MtO~#~*DZ%%y{`lQ5*JuA$+T!vyE@ z6S~y0j!HQ0a2$~)LJl^glwXU>WbkcWld#WnpeRrnkNSdaT&it=N*DI7N3e}43Fn%0 zC&pCfri!V#a~<;e@M;F|ic&Zh^p_8l*!fA@$Tg2)V&OEk3XCei4b!2`F)qzu8--+yW{hJ zKAr#m{^sq;&*Oi1SO5OI;OW)fx$^99Ki>WReEJWEB&GZ_rv3Ko-zOJ--ChU|T`3Dli4 z+}47VkCvv}4v@V1(p((G5Hp!FD>v>OT)0L%pxHXvY!fzo@;M?59mmf>>+1kw$}LV% z`+{az{2MuAs-#v{7iHsnQO$*Z+z>HK^rg7*g8IVk^gZ7%TC%?JFA0382^9+gM7>HO z`1rH2Rbry`ywn-zVfr31aB!>#UkrCdUgXTV1{3k2m#KywLt{uf%hH$75dkC1>8$T5 zz_NqUmG$IAJ0QPIZnV$vE~7AA?_aFTW9LC?ufjJ!>p(1`lJW;jrHXpX2fr@Wl|um4 zSu@rZ#{~bfC@B+X()Cx4f5s|zpaPZi{%T@@XSX+2_;$02QBxwi6N}!3GD2r)sni** z$3z!{f(W4>A&;zyik{_oy0d6X-R$^oqd@h;?u%XicF#_BM9(EF)~|YG@dXU6S!S)G zoDFam-uhH@;G(##i*fk5+gR{+Y~uET=N zw<{ytLk=sez#RRzzIqlK*)B&@P;p-ioWN@@z+|z2K?zz2=`}*QKzoO&l3m;%Vg^4V z6WUQiRoV9g@vqGHhqAMRU&sp?>tY;~v80nhezU+BhZ*Kk5IXA**&in=TRWF`3aChn zxZu3sr!)*dC#=fBV+`V?J%=ZTj|gtJ^?tjYz9P6N&nuFZDqjM9u0Qa%NfwHHK#sHU z?Jh(>zx~fQBlPy034?7?Z1|iMT)mW`ZlJg4XUs=uKA6+_ocz_HCq=i5YURdx&aY?- z|9-sq`StI|pO+VA`cgH)w|EgRx1;#}5O-cifm?rns-*SaT@y6#7<7KE4%Hy_?!ta5 zin|puk74gFM4shND-h3wZDtOuHs)Y=kyEb|2UFoPPbD`!KuoJ!nj4Rg$he%*D57-1pWp-+bOOs1YLjCya>N^TdzN-K5O6-1|-HedB znj+a#m*9uv@zvGkw~GRmu75jSTHO>|zxwsz*U67BNdN5>>GF}5vHJV;GyDDBs0bHb_fnLUW6SpQuBli`bXS2<{i#wH{ zZGz3AX%spir{bDePG9iHEbO#~DtrT3{A#J8c!*157YG{&2|eXmI~7f}85#iAzp$PE z!a5#(oX+M0KJ4LHlTgtZ)NT)Br|^DwpR0V>;FMEi?nLcRU<##2mw8QDJxU|dXSAy+)Trasx61Xe( z_afj0;n_t2N*o-q0qRLqtSLZ`g0^Q23id3w{6x8Sj`m$4J395+UusY&^}WDu3ybxX zaLl6#ZP~ans$rOijsr47hHHid7w@cSKoKU z-|ot-4td}4PI_guoyF{+&P}X+dBE{R)-L=eM)5j{QK?Furp=Bw)?AIhY}ekdh?#b= zN_BtM57j?w75nsV35LGieYZ}cbazEIHhtMaJV^^b@2>Xbm><37>sij1xF%v9n}F;1 z0Vvnq$)n!Qz*U&IXf135nt_TPCP7eo@N#jM*DH!^@J;aAe(kT4F0Ngg>2Vt4n%}55 zH4dO+#|a4>{h?F_wape#_)TwEIGjD^;lNQ6XarfZf0QkMuLPRzuEgXP@et1^G{y3G zR0(-5wYcj;@%O9WZuf9FEiZqv@HV2G)6xj&9kTyw68n4L{k1t#MV2Mg;sDgVx9toUL)au%Q*$VZx>H?Y={WR zg|9=}R*q`$TNt>fD$CnLVdU5$aFk9$yOFt2vzO7vTaZR3{HksuU=y3Np71=}};d@m?J|CXw1RLA&LA4{pSrQ;i?702(HC^EP`bAxP zSd_l8Rz46Y$-ZnVm@rlHD=IM2(eD5Yc$|rBQuk!23}6R?fOmNC&JB|HyHs)giv~9{ zXHz)!k&UIWz_z7AZKt6L3ayncaw1kS`mqc}Mpr5pFYbnM;dRXl%f-LnG#$iYuG>u}~ZOZ!PI%TE`Ep0zd*QE%0!x&5AFnu=~7AXCCCbt^K zj5-RO$(L>NPoy%Y_V>x(91QEJ7|;|3J5}ySS3vyTOmpMK2<-hyopToc)tm+{IBap8 zqiXW83uB^mD4O9fo@&J#bO_Pt*=9DqC@wSzCBh}!4$cu$HqIF@8Klge+*aFo_4T5Y zukj1qOX5YdH;!8n@~{D@=~%Ss3`6)7$vlWr$*246f&$>Q56%oMU#&({=`fbuNr=PQ z)cZ8~LouFYgioGt-JEE2y8leH12;ziF8Ol8gFF+Ow|y#P;K@_@v?YCF-A*ugbC}Q_ z44~bjO*oT3GgF}}c5b!17n%rPl)g`4NNX3cZ}UcI>1%AbJC)}NASiPSb*muC8F!+zC_4#G@ zz&fY9#@>H5Y8{fwKzdr-bs`~#O4PUEkB z0^jQ9q*$XhVG;K+X&)A&Cw@+4uul$M$ehV%is@$m`2c%Wf-~nul@gdfdT{CAywf3i zZv4SMV$H3N0ujNSKgh7q71zVXpY-zh(#>pK`>UsIY66d?K9;NxSN~W?s&#*%&C5@k z827~k#|A~meV-{$7*YxWjpwKC+1-$g&FxhNNe);`JGgh4ZWe7MTFW!nF=zL=vx*Mx zsLY00~y9jNe5jnW1vV&Ska4do4tIZtsp8Vz4*7gm1ZyF#b(jGrhEG3sXLzFqO$@H3^-9FgQ~<` zVhP4CBRJ>*t-3qd4vo*T)QaGLRTz|b5@rp=>ZQGUDu~@F&Q>&sVX@1v+D~YK9mS7q zBDDeFVkV@Mz7CwRSjwJ-U~-(9NC-IhQs2aO!Vg1DLJVc@?hB|#<1xv+2#*@XPTSSk zS&TY#=|XQYMhV3YOuM}^9Hr+)Y_?mmyOk1-Ke9g@xf6J|IoX9k8$y+4-Ko77IOu11 z+Uoh|&r|0LTn0Gh7u<|eWs;}l1OV-xidqdhIwY9IA^a@$li{X@}K`UAL(}SWHnx zW4(R;ln32h=Wa43ntYU|qc@dX*Cp(%uM~r`Y#wiui1?xRc0r2`8Ei=6E{;!X+Jm&d z)Nrd0Z57Ds*V23-^?sAF==ew^6yM`5lFGH8ZOX~j;-HKja^*mQ3<~?Hm$~E2+q`WC zG@k4rO(r;*#$YeON&2PxnqcB-eRJQT zi?~^MMfsFIQ7QXbQ93n#+g4-31ateqVO%O@&_vxwoxh9}rhlyd84u102kTnNw2G(A zq1{m@ce|g$No@Gb6GDIcYvNeIwfJE*y}H17xrGN?Q+{zYpK5TUzDbr?^ljEE6#mO9 zcfU4F9KxH}Xu5U9LzI4nz(xfDyh3xAv&;RlaF)WG95B00;llf5v)!*gra0bjCK4Gp zi%`=&a^h2c44WbBn?*>dVhw1CipZ9$e`)LeUOw!8-4#W;=H3!7I~d6lZG-;HjEdAt zvUak!!5pf?7?)TSz9esGmW~G5emVCZJ>(r(64!2>n1*gf(ocUja%qMsR*}{{H>hsr^HG z-abxM%ay8DoMP~4@5Db<*O4IRxn9Bb^?1f{i@D_H{P>(sdyeTHS6F^@_z2?W%gLL! zCl{9=$m>C5Ni_dGhEM9aK+`}yVm>DMji|Cg)lpQilv>o4+esqj~# z)cSlOm2wYRETFe=5Bp%pZr+@KbxfNX6+I1TC!S`^9GOUi7xKLUFSS7v1f!sF`$UcG z_vH9aKQQCqc=^-aDk2E?wJGOZHYAxs5C3y+1POf26J$sOL`gq7P}0edsoNDK3LuQp zlF!io924`OBUI~oIp35VSH2$KVFuU5!YPvf4;w@W$zf$}I zU1Xs1c$}L%`O1CM8zK`yo!?r z+Q7uUxXg<(;Vgr6P&Y@RJ4?Rmt3!w7b-9{pV%qnitfwb}AJ!Y&Q+}EKW52#TO$0_{ zXvRUzu|8ZGTeklZWb$i=M-FB9ddFH?+`NatmOr^bS*wb`Ef3U+^cvZ9$oYQY*A&Yb zk-fRV-A245FVNSalbZd=bmRb!o|&1Q=;|jc`B61Lq>`KARZ1VmdqC0T>)Z4$@JDUq z;)6&6QX?3Bco(|0TVqGh^*$qZ)?1T;yo_L?&y$3@_C#MYxq0G>w&lH&C_Fs! z;V|wFd;q^sn#ZoZzs~1z6q7UL#QSL~AoYb)P^gw|Qn-6UY&x_+3i0sOKj&Zn!392j zBBVO8o!BAHwCmKq|IJ_1I;=T-1SOhi`BHf!aUnXQBb(p^$QBpo46{ z_$cP6Omdoc*X6Xsoms@DJvoGZ1krUO_%Y396po2=5`#3!KsqTT<{+^Du6SORTfstC zhr|#g@5bTcQ5gVwhD>vd(>cXsm}dG{@D{$)d6tnS1t#rGp!Oy)Dhk)u5k<|$dkHr! z)1;e9Pox$o5u<<$Ns`=UXks*Bsl79`v-jQ?b3+#|?$Z{Wg_@nC3=mla=g#G(91WmM zg-Z|DqC*JV;Ai55=5!Eu`*SzIfvPs6bc0WeCJ_XaoAS@#Ucqk{d~VgqON;qx=WBmMtHcJ+rGIzZ|&&bSAGrw*Y%Ccg2g9v_xhu% zoN@V@96a;QXf}@mmk=X4Si^>FxQE6|xQFH;e!Rwt@R8Tk1o_?V0VB17u=GDq2C)2^ z43#pqSA08o?nOSH5!VOf-#&V8p73@0C$x{S*7&>BV&OCA8>R-6^85rsycfrLmA6l> z^}GB;NSz-4{w~7IKBLuC47wv55&Ta--{0!HhdawCVgy_OR#`@lRdo=eyD*8uu?hXR z>W#P0>au;S09uArMLGBKkk-!^Y8(Ci_Ihze0y{MEUfk5^bgH}q@@}j6==N2T=Jb}>whGkn_9Bu$X?5M1vaOg!F zf+mno3qArr-^i21G?d9WglnFwJBIUBvL|^vgFL5%{0gsx0g}jQpIyUr_}QsM3L5Ds z5%<}0s#&{7>%&wp&biZlfl9l$X=_QrZf0&7kK&FVH_?+j{rp$X;3E&R!VncC-T`e8a9;z;%eiOGzgyo?}k!t1;fKBj1hmv9^(KTl=`{!JJX`U%pbhuIYkeQmZC{*Y6&F*?g9TXZ<%Vu4<(>4C2Sx&C%B*bGp_6`||9f|BQ3lPv@zYu^8L98XN8eJFqbvwf^;g z%)*NGJ9<*9c5D_&KvcGD@w^}*-quDyNr>F>t_JY%J9 zj2ihiiIo>ur=riafopsXNbk>Q61y@lcPIVwWm+J@QM}oGyAGV*ZTP2)yB^cjWzNo6 z{kV92x%1`D8z<_UGh+2spdC}WFW|AQBcI_c8)C}sApF1@!#zEvBIoEMqU4q=LA`XN zg*Uz2ykayHb+}MUN+wSx`u;bp>?~@F3#6+}EEVll3%0J@C4+5owq4|MBZOGa5H~OW z*v)9itrW>=5W)b3v(Z3KeyJSZ`g~&O(cE>Z%7KcR3Y;%V2^ZIRyMOi-E)TbXc81=E9rkON15L|X_MxH#`(!9s8#NoKyFE>r_?<3yqJdkm29DGf^1Egrhx{pQ? z&YqDf%?XfQa4WBM=@!7F6lHqZNQJ}n+g**nz})66EI!%{tezLTl$W}ke+?3JzSw8{ z8hA5J2!NTm)sEWNM#vdS<4I;;c+(xXz`Qj@`9nG_-1pC0;;Ud#5)LopdP#aj>_)3E zn--qzn`sWn;|ZDeYzg z0;q44RCAvL-t0DW(`Na*8#hME@Mg}6(BGmjU~sJ@pVFvp!>;zDF0Y=n9>siSI%E+` zXy4vLD!l8x*+7Sw!otpg)v^R6%HF8LLPi^55aK{)7T;)|MTs-uGaQD>0Q z1REoD-h(*IhZM7V>;+=O?22>vMQU5?)$vceT?DaXQR%eQNk1msPjj82;*LFVFL1`O z)N5^dy7=t-GJfhbZVDabG{ByvPkINnsn-kfB(CIW+~fVS9zQB{@9e-m(c5Cblgu>n zg(cIIcKTd&A&t?jk25|}Kn*p@^q5VBH3;wTO?|~mfUY6Sa1OiVIsd&we2z}esI-Gp z!2_1@_I|P@1E;?D;F-l+xIb}~ED4p%`DYHYGV<#8r0B0l|f;`Zt0mDDwvm$T*Mr|XL^vPmt1V0_Y2X)h7Er)9G23@IW+`CrQI|3}x< z|GC)uAFBU{Tq6qVFQn(3zWihNm(EM?1r2{DwS9N=ba@COO8c=wdfCncu*03vVx6Q1 zv^KueDv=6XVM#@VVhb8gR@9(q*79zRUqX=-@ez{lN>N+3SISe&Dxp?OPTOdEKwXj$ zUj-Z<8=yKD$yuh7=^MNH=?rG7XOYb~6BMF-PaP2AkV@W6H`%OHgBdr}ayG+*0U+p~ z6_Rb|Y_w5h?k*@hXEz;MB&#Gs%qVaH2GIL?VF25A!qaB)bA(g$ zYf1G2>=J@FrFadSS&s%7N{-ZA*slKr5%Caft2JgGUF$ z8fMa?zjH0LI-JxD3ws5ICc7L}XT@%{1WXuPZx?YDLY507|I$45C$$CP;mA~hMV
    mOJwi2Za95mj2{;CHp?PEh>)>r{rqDkJOS z{#I+Y6EaeH0nos-?T2I8ECq5}z15+j=86DiF~S%SX$w}=8SyJe2r*BVuDs^@aH;eg zgUbIgm=Bqsn7*RVscQ|mnl#7|`rN8u7f^;Vov^e{BU%opJ@4{QBRcZ)XAXfjp$uRI z*lTgIRmS5u1}|_jLMt19^BE1WQb!Wmqm@Q#hl`E}+Kvk*l9r!s;PBARTo{FRHLBa` znB}y->v1{0B)B~xWc!HMWAq<~-5+fIhmm?9NpR2O0?6ExTq9-HHhGV@S*T6yn<0FoIR>fi1Tye4{aAN5UFPXpUV zmT3BIz`Rz{m3o1`)|i_;WlkvEKaI750I-mV@5;=fA_sLnOfE@`gCuB!`SQHb3Uh#iwFa?vZ`yM^9>S%|L6)c8KsUlX=m8c*a_J?D% zUzMuuV|$S`&n~vc3gW~KD;v-_tUyDOX+zn(+bwm4)Bt=x=+a#n|#*`K>9C0~FZPw>;syZbb1P1x2j#it@$GcJ>Q{6(` z!ihU?v#m{1M2a!X z@fecIw!RI_9)J*kI1n@psNiJf+39{bO%WaG=;1jDhDy=n$sW9H+YCLxcaxW06f}W7h9w z$$Mu=Z|#k^ciT`Iq2Nw^&G*5~WvDT+>O&@^aTG);JqycFuxyG`y zGmr9)CJ5&SUElejanWflb@9--s@sp_G~WK__UC5|^3~yulFJSc&p!HVw6sD2afWpI zCGLD#fB0!jV-GW?Ev2v@_O1;#Rl}qq19;m~bAj-5ww!Q=K}e_q(QhOzpD)BZEp^G( zq>A>g@!4hijGeGg9HZ*xhO9wDHK%z}$wELAf;i{LP;HAX7pk3{x?U z>>{4g31vA2zQJn2GR^FZ8W|yTe?9c&cR_&#)v`)t>^Xc7AI{abL(0mi)!{U~uY)3j z?cYwfTuO5$@f#afDuTQL+#)LA4>Rl0A|H1&C~dsF;8Tzk9op zS=Tch?ic^x2;ZW^oCn^v*f|ysCvY}6e7^P9(;OqwRwYClspJXFZZ9S`o(wPF>%d4u zav-@8iEUbPWIfAPO2th|V8X;7_f+Fdo!kWWV47N;aigFp_e_2shE87{%d*MZuuU5h zjFWql9DtOdwS-^|Nb!UZ6%>j$o0?a)tT=Y?YaV`C1^wBlzX*)`k z$ugA@&w1e9Ip!8MU4#z?vV2T_2RmW}78$UrBC6gu-je9~up#&~ymXP{B#c&5;1Cs= z&QwTr7SM$_mj4HRnx^K!Dbd%MG=g2Tn5;RKkN$T7V4QMD7$dXWxXl=?!ai<*ikW{S zfhS+p`k5Z?v^y!+rS;&oJ(~ethisd>_Zw66rV(6D`Wbda?oO^u8r38zL3mT9h#Ez= z3)=w*`HI9*CO5)VMl|Cd%ko18cxEm4S0}s#TFSwYAy)@!Yb2KeiiTV{gratFLeGS$ ziwMybGkw?dX4SP2)ub#&T2Hb;JyQ=(*Y_p5n@+hfo5HWG$Sidxv~}?0VoQxntH#4J zxesL47%O3(P;avWC9LPcqg59bS-fR@`Z$SRw)176USP0**BH8-V~Z4xv1wMX7Zt15 z(P|t!7Lo+r8D6=4-p$Zkjs2*5$k21Qjl@C+MzcWo*`Z)Ex5=e7T&c==} z2e6BFanURqDvxR8zc;q}q-vC4I5^oLb>5v^*-<-(AJs1=9Sm76n7)}6d+h@TT06lJ zBScFh=xAuNdyCpyC35`n0h5_n)H@>$Hv7XqL$6^-o#M+cN){MpIlg#dpVTSXkSVw) zX#ODvyT3FIO@G+yd~n!=T8IVPV8ez7KjKwok1 zdFJ62>?>z8O;0XemiHl&?EqlYh7Z3V&Y{`&PVFq1MDmTgA1Zh=Q#rz~B*Z38LyuVH zQk{zzi`JMW0^XIemofo_Vy#NtG?S36F<_j4XtQ4FS|p-1@Y z*oDT5C-Z-K3AS+R818khsA2c-=RK}R;1x>2Dsjfn*)&=@^~C*k2?@C z+t`H~7iAbms~s)C)bYv}ufO@UJubJ5wC&FYVg^{ivi(EbhpW3Hae0^EpDr~-|Mt&M z-#&88=thba{yKb7O{R^*h?bMnlOJ_r?TqA$Q}WYc%L2Zeq(P48zj&cFfqmosDVvaE z<|?Q4&drY(mv)~288-jV!b1pFO`_{z?+AAfi%?&@-Ce!BoK1nPlgF-frrf#JGZxI> z{NDjx)_-&DiAL3p)f_fG{82q+VE3eiXx~qZS~n#QnKmyqo}e*Hn%~V&x8Ga!jos@g zda_|!`_FL%T1q^Y-Td^0yJ726E^0e<_IUjBBL>6-Lzm-u({tj}-JK*;?_H8oJGMX- zupOAN%J$*?+vhh-Z}oa3%C-IZ=k2pHo^QYJpFMTV0TH!ou6v1JaW0K;ChnH~Sxglg z?WK&;z{ZOOw&_JU-&oiC_|>9<;!MyL&qlWHdh4OLBo3Q_N`k3)ewLi8(ur- zO2##oJK~Z?72#IJ#tq!T$j0FSWp1F5k&XL*L?Z2_*DFYH#sK00J3VlK0XWNOX%1mS zD^9Zb`~j9IkqA8AMPu3c=fQdGi3D%^hEeq}y?nO=YZpcSwf14_TjzznKD_Fd{|!#f zoeR4RY0Zc4JYlC84`NPEH!8N9iJ)mbv;+=Dm05e#oar^`NbU;@ z+H3l&hx%cFas$#)L8@epG1dUGw2Xj{lx+0g%d2^LC^q{r9e0~^SU$iu&cv2-(QQ2K zMh`t`NC$CfOX3gHSUSTR)%FF$q1>sZ9xmDzK8V@1_`zCpr9d}PMi0FEfr8B*Cgz&X z*JGw0K+%y_^|-6&kGkSWM3=kA_g_Z(+86~2xYTir5oTFD&}XV(8r_Tf6`li_HNrzB zJI2vO*y9Ek+B@g=aNB~6l#+<)f1%SK)A`#xiC%g;JC#jfHzx{!9|dmUv}2>aK(t^M_s*Tc3SPkE$oY0X|TE+cwc zgyN*QY!#r%lzhp3b%poR`BFTPvDP5XnP^vY`|&DKGBCSFEd+yEJxN=33*ue~Zv1DSlTgiMOtY)lE|Yvrwp!xWd*?}KkY zag5Xi`y2K}Aa=%GrORfTtDiXe^!fG&KgjCfP~7J7WKQL*5_Fo{U7lzTpKNE^NHLu+ z*DuozUb59kp=ycC-1G`?}m0n;hji2M0!8i}qXx|spcn1|d2px*md{!~_+IEfGGx$s!BXb*XBG#ZFOdM(E!8%$V{${Vc$JbI~c44xol}S(4Rep&ayG0eIY-Ps; z*l!E%y7Stw%0z&8Ht=4Fpv^2xji{0biKI^>f@N$kD6s2K|2CN~3GsrhuQ9Uy;t3Ku zmhc?|iF?j>n#>vPL7d>RmH}1yDO?6Ye+)u6Rm{hyvy`)3ChWSAj)D7KOzMhY4CwjE zgCjNfd^lQccP8X)#oqK^Z=+~z?*PI_=SioRj-Ss4@P1nOs30iEPOqL0h9O%pNkf}) zYus`GUS14PVNo{3cUZtP#M{St{sNLjpZD?1+v)`Lf_LL7$fI`c^&DBTiNann+xSXe z9eYXd`y(Eci-a6*j?oe9YI5UygXG*hRRgQB1SxomRkcDc;%oR}2(paVa8YjcI)QCA zo+VyYvk6fC0uFq6%JDb(rV=Mv2kTJJ`#Oc;KY{95guqHlq z&#`X64DCXBdU+IHZ_cu1ejF3;CRl59)!X zoadbRw6mPkQP&!YySZUj=VzyE&ei3Y&sXXx#`M~~;ZIzj&xy@ch+)@502c;Z{%_Na z_H^N73`EGqTF&*(8zwo%fyox4ASbgwkyeTC=L`T6Geo65)a?ZpqN#dcn4gsn1{ zCu!O{eWlz2bHA8|lRK9(aOYB+b~|ZT>l(tYUlOqVv>lHC2vUV}{o+@}uI z@p-uN6(iapd!4rZcm>Uv|JopUGgyj7@k!7Vc7DF-?;!u2HTE=2W@*NH)_AZKTWP6} zceCOr5)$C#rid6=COzZo>gem$&DSqSTmM6Qkp@EU-P~em&xn;&zbQFDHo7XK(-UXZ z$lWyR`sC3eGpF~UzCOKt5f{;wIDwK*^Pw!7JsZlEes%FScR5pj)RH}M!M$UBc>(42 zf<4q(;dGY$!Kq#((S7+T>$n9AED(p%^7j4_XIpL)S-QAB0fhemB4klS>%F+|slC5= z7|(l?1z;s&d1(mrYGS^WNWeX4`+gKm<4UClDfthlv*w z<-N(mcZ^d-kJHxgIb9@UL(xdBeesq)GV$h5yP`!HRrZHQ#j@w)dvI)0#JH{x@?I~k zIq?|^_C%{n$-TUC>*(4~=coAQ9=4a>PkfWQB(YZ#w%RA(asT*fEqaX-Gj|t`^6~re zJ5ksmVq0k^lWIFGh{0s5( z6U+jpv~;6Np#Mv`aiip9KW5$e&n(mR{Z)MhjafR-hD`RJvDmXQ#^2cdG^0z3M$#c) z3Qx)O6>|urg&Nk}X@ff_Z&dD1a3#|_hH;MqH+}urbhO?4pj3NnxV)ZV`Ah>h02c@;@HV0CI?P3n@JL~hSLFOj9rOOEy%dMC)udg1cMGu zo(?N@gwHOR_S<1tyCU%%@Riqt31tT=3NdiLBj@aZ6N_f4^_?D8oa@ zaIF+^eAmF>4?lliXB?k9LU3me?xGg&TESAUx-ejD8b_Y+2CKwl{u_*+bkXzyY}R{i zuy5LC8px+3`ve#m;^|~ejSGNlI-~lPIssS@9eU@V1oj>as-wG16X@w-BjQ|8m8yYJ zPPye;xKwtNc7$6yR4!pRYUKLN57Ct+s%MKbkcnBW_$%F}%N*9cVKjx_Q^VEkLlgi3 zK=ZK_!f-9F&EoR?^HER~DyXym2INbV{oFNEJ0a<~GG`qYh_=z1r>%(D(6F4<^o}ENu}C}aU5wilJq*&5d-h=;N|*nZ9sR5^e7aS+)0aXF#eiV>61 z5uGl#aDlP3O?Rr$&ZeSepBN={u767fr8lPDK|hBH0$U*ocHGUpD_IhO^@cR*CU(q*WKu<3}63SAs@n$KQ zcbKWlA!!ybp8haZk}rhe_jz4f!nWeeX>Po}_oTxZNCMp;Pi{XRFkO`u+mk9qQ@!b> z(dNc>x|D#^=O)v&D&!GvLyFUVER zAx?0}1z46}LdTP*x8J_wFc;TDb`R4NZ9!~9Jz&=3O$ID~%9`Aj8D%0>_-apwAfUMk zPTG_gvJ^(k7B(!Wsq-$fc+I0;eyx9XdULJioX4k``TDnhm9_DWKkNVY`OBq$b~PDr zdA~GMp5%jl%CX8U>~M9m^tjG%amwek@Jw$`Mvo2mY$vrSl>aO@}W-k_Q_WdceQ+ek_!-4{_I>W z7Pd<@k~eeQa2eAV7Z*MrEMrOnUNTi&FER7-fh0HCM&n;|C%3vscN%}^HG%nBXXSbn zZ>>|;*TZCf_KgK_&P`6rM^_Ji^ovt8#5wDJ1Jv{t-QB5xAU?$wggbVN1Dr7lEp^#R4sw6U?lVJYJf9Mgs zygH7-E`RKGA+7bfbVQjzNapI;2F(}4Dn@jQbOrW~@v|qF4_6&aH}}lh1<4y-g22Tf zlbeproJHRx2(y*LaS$)1mz0Ffx6?qrksSw!d%J$-NQ2_#fTw+S9?wC;NvhDaWCZ_o;GlUoqvb1si7cTDG|u~ zDI)T(^-8)con*48%b1pV=gEs8_0|0)x#73;h6ybNNNH_pzH!3;3z586v=YxNbfAkF zrM>Th$(wi7KW65{q9Ul0Ih>vi5A<1xdOqRV;~OQ*8pd5RN+M*Ta z1q{6fyBPj98}-1dH4L^)EGk~$^xpHya1xyY(S|aF*bOvy;vc#&M5^r*Cb2}J+Q@XX zi60EAOjeI(hy>&`*`=U!70}H0ft+P$3I9vS@dr-hW^G-Wb(xFA7BK%`z^&rA zjqzUOUleDfU1);}|N#tnLU0u>9ZiXC;{JK+#>z?-zufnjsp4EYRMwCWwu z&3Q)zl2o#Z_GpcsqfR_5boU{#J;}kKI)=!CdULoc*Y@A$g3?9@!qa+uAd-4QO76vN zDX&>jdDPEppYW_O&n^qBw4~w+yH7atuF~0wAo1ts^0hRu zK{z!uL{I*9NVP>vMnPwnP7#*J$Sj3+ysvP05QFn@A2N?0_9~$kfyUAhd^#ZVOp-r6 z20|ndi>slXqRv)ox4tKx^-}Z3DJ+HCZ1*yGkej4`L!zi9t&q6N!%AKtPy_WF5vB*r zC3V|*U~ZQ?t0eSOMlkuXq{l=>L2sQ!D%S`!1kK)a z)0Y)n-CIkIU$-BR4!=5gZ2O$jmHnc1*x3+!(Q$oV9-PQPdp#*fNNM5zUJ@ouAH-O!hOI+g7j>AE z9Oz)0?Vp?Bwo{>O^`xof=ocZ%k?PG-UyRAN2aw%C)~d8W^>{Gi)vv!3 zz{iH%QXl5%-3VbJyAY2kLsd?&+JAofh8AMzc@awtBYd`3w!=Gv5*E0V_>8gZ6Z!7G zu}-FfnK~B^Xq6fo0(Uu?jYM@fgfm7>ccD-prj}h5Mrsk^?(yYR z_D-S!r`7qFZ?~!coo&7WGfA0#xgdmwj~xh_n^u&n-?ay9m z+KX!e3Q@_x1Y3ynb9n=oYkQJ+L$c|TVcKotGK!%naad>hd{?|}`xS(F4~CntkE&p! z8_tdH+U&V?roQbyx;T>o*V02f!;s#eS_rG`_)pRO-aoFqF$>TLzyA)z^%mj|+iZkB zKp286cisWO9Yx{eBT6N+v$^j5*8v?7nhmk|2tx|ebRb{CYLo7R`Qd0W--zm+9;1`8 zb9S+5*9q1gCK;N4{Mj)0J6Fs|r}TShNLIRMAhEOLSi_UwR=WQUcT6F{%vChOT+h2O zT^0H$-*WiTQXn{Zy&&~_iSgNWd~x}DT_n|hf%2?@mXvEPrg5!+fPCq+)f69qZ|NS4 zMUeH8l$Aa_VX!F{IJEV-V$Jwcxs(P;=W*v!HW8*O{3F7w)@bPHJ>R5655pvUAXQ8X zLWC)sb{lPhu`4>4EH_2XSVc1#*%%*5S*s?r)sht%L};Drky)aU54}pOV@YpgY6~F5<^*G?`ibC6~}?{l#O&`?}}Y76Koj- zF-U~Miyu=SOY%2dFs+i@Sd&xHgL2HA&2i?Cjl%&%Bw>hG;yB#h_(s4rzX%NW`Ok~*c3C3I@!=On>{WZd5<$4}<)hG_oR9`v{W4+U zpD~jP%wInLn&i!$ugs%u%0U*DJSPGOu8`u`Yeny?%a==|)E?Hpapfr?6?)?56POc0 zTAw|0QwpTqIcom)w`~)>xZ<32gZQc#p1`L7%+0OkUHOhr@$%*R(-#>**+#3|@1jfU zY`yAUD#%3AjGZfE_weBFZy)_WRo${{)8xNOx|`J1$BTkiJh=~-g6VrlherLiv~yd% z72O&oR0cTZa3u$$#uYgT+q^$nR5N-+!6SevkJ*EYZkWV$HlGTB6=RO^}en0u6U8fxW zSNOv7sXc?)9q_r4@w_BpHCgNGy^XE#CfDZW>-ZM2Fl-X$kRtZT)zRRvts-NGF--KwM z#{(GRt~n0ig0UNR@xGapXU1v_2Q_^ml9amH&xJpZ7H%e4D|mB7QS$C06!=Qw^Tjk% zSrw1&ZG(3gyl?=e=b$y~SPjEsVzlRyC+i=LDpd@nnh6Cw|{rFHits3H$ z!xeC8_a=)uFNl0+WceWSxw?5h#KGR26;-nR-S=N7=ReCNc)7)8w%-olUJjlv86~Ul z9f24O_lt7XzjF1^fya*y;a;g{Nd_gI7t!N&SJhgc)n}V zFW*m(E)PHdy|sP#?d#+H4^fdLFA`k%<;V8dgJ*H@lY1>AzFoh4|MmS;`upD(Z;yZz zj4_MQ6Dpi6nOPk-DJ__=h*5AbB1Ra|^sB8yc%*+Rxr4E|Cjl5v&z`_r!FJKkMWiue zGEPVD3o$KSgJKaM!Ce+cLS-GEPMsp$jn&2Qw0h)%64kw(DtORT4@M*C25GcsM!gh@ z>p|a1#e^Bi=Uy5gkwPLJY|)k5+CCz-2uGDzCuryLj8XPxH z430MN+Id;F$0~Zio{!ELmQHu~n zS0epM5{MTD8v!Y}N7a1imVBs|filPbZ9Q#0gXoV?fuU0o+{5QGR*6}chQCfyW%?~i z1AGy4D7jnIU}3tkep05NO~@FIoS-x>{WW_jlp(BK`3=hC0~>E}#?q&@AeZIhF^~}7 z?R%qSa|;nfXQvhaPR-QPms-bSJLcVy%rpwAThnRLnGkWKpJr44$$XoMHrCs)!%c2P zTA6`C8+RVLG$T&o`N+vhUk`hi5`c&rkUd~1g^W4Sv-~%UeQjyacx1(^*93C^bA;mP3)DM+Bc+xEp@G@olSXXSII zAsC}Sqn;^yUE(#KK;!73`O;8~@yFqv{TYzs2~TGwrd!q+l+$H?cg)RUbfrenHBAXW z5KV`WIa6x%HUJxJ^E>T2Jgaerj(}>Wxa=M*vx6M=T8au)tG2fOQ;scvP> zkx=56g+~=_Bbg2F)p1}<9Mz+-f6uuGF2|QQNW(Dv3vtR0h>!09Q9=g{B<-l=Fffrd z-adq48JP1Sq@nlb1BTC5{vIv%DXbdpEy|m4(lp3fykSaX5njuu6^6>>;FJs1EtV7+ zKDEcrT4YPuDSi{{?~pp*`vxZ?TcCgpW4JuUi0X-(+HyF(n%Rgm6KCTQTCk!3&=q&6 zMT*<=2IpQ}|M0PTm-M%gff=47&iMp1XtyjbvRzA-W|_;$&VR)uY}0IayEIX;05>kF z5g%<^>s+dmfx?8?J7Mk5+ojTJlsFCIIs90Jt6!K4B~^6G>MT<9_-oO2 z9Y_F{T=9OGgmXLDVgMS`4vd2+deruparZ_ zf>d~)m*KR!nnsE`8cQu*J$#qu^s5%^8ooa=dnmY4!d7OY*5ypL>;2EqXJ5}h{cykELcIeK0lsB2-6wXdPh@j3NwNtRi3kPUvFnt(P1c!Pl zbD`(xvIB!+{nN4=|3`Lm2thU(E;~ zFop0QHh9bq;2xth?T=QskEsna>oKMs&&kk^dXM+a9n1(rE;^KJe)e#m)TFH}3})9$ za;q8Phnm3J7<6=tE$R5-j$r^4gGfWkiaUK$y1c$vGPEb5Ojn)p!<5;zQFd&p=-a(C zbdg668LM#GyVs{L03W6WOLV1re7PY1B&8kP9~o9Y;32}E2vjEIn6BV!j>RgSQAN6qa_5Gr-w^b| z+VR^H9|+%(T1(8OcTrZYZRwG<>8%oPmQe?2o}8w$v2I7m29-v7D7gY&|4VaBwjS11 zj~fUrLn`0ok0HEqMPpwVz`mkt&&~EKMdn(Phw2j_y z<$z1>dXK&GRm|gkoNFby}Jb~&I%CX z32SGeF|$X`+QEqC+=ct|7}aiGaNhS33MnNZZLdplok5Iu{F%vG|?ZlCb`gg-r!#W~Dw39HChreN)u zTrzBw+tJ=J)d=5Z8b)U-cr>p!{yYaEa3w?!zV!5$!xsY9XbW+UDTpKy)ucb9&636FF#dLhaDG7WzznxabtggVSW)o)>`;=S!8;if6 zQw!=W70cGI6$Us7i)ga z;p&wgUrB&ATIyn&$J=W{!tNw~DAU(ZuhlC)FUT;c*vck7-#cezbM-Hm-`~LX(+0-1B zu0Ch`{@}}}Ghu*-Bo4Q~dXQTr6ym+MlE?`Hwz93G@F3DQtxRnk zU2J36LgEaQ<5O?XjB1qWDfh}RA|l`}c>FJ4gH*fyMJ1QoxcC>SNAQA|89~UZ0&#Bk|fgAZ}WLHLF`18?r@bk6lL_4tB28@aT?X#$ZYiAZZ|xaf%MV@&FH~P zf1D{#!rLJ31b^!AKr^6s52cNTvqRV*7hkw5$-%i&H(4>}(flez5r2y69epc!6{`(_Z)jl&a0jj|=R ziit^H-!EJA!Dk<<)=68)?h^V!;<)NYo*JUggEn!TGU@PflG#MHBQ-GI&GPWRrMlXk z{=^pXQ#`SUH+uJU5T1g>T3+wD{%?gS7Yto>aUKF*4}0xrHH!cyt-ZN;% zymi>ydFEs1DEm%R7z^%{+>Hh@b{$r2b2ZIzhZF~PR*!AO#!?43Q;EmwFoY-h8q`L` zhs3G-Ai|nXN9=sLtes#v%K2W-pTz^xgWxo-z#{Khwru}pIk^S*VOkXE6xUv6t}thG zxk%c6zUk883@%70*TKf`E*{^R$}$lbK5&?n+I`fQOOAJ*;oTA%cI%n-ZFw5rDBLWc z(o`zvo%3r;R$^JpT#_mlRhYxx67eDQ%-RBeTo4#0Fzg=b@=q0tH)W18p#49{k^&uc z6we-W*x8CVwtueQeS1fmXKC%8ox`;3;RzFQD)gN__dZ%7tCbrGcz8l>bi&j?DTe`U zT5hOafI z9D=+niZL~hv+*jQnkZ7qOg$hM`si6a``W~QikDpDDwNg+q{3`4CehHp@E69aF9GF z(RNaXvnk#?TEON(=mi+$32PTvLus&}bTF+YnwZ=Uek!7rpUQF#k~&KZ!Q)*j@wG6(AF{B<;K1mW;_ zXpxMhPxvxD{c-$$H^kJ)K?$=l_i<|Qm4fyzk{vkPW>IX}8lKk8)Kk7Sg7r{QITZ{u zF$_&WOwIq%}KkFs&?NF zfiQjsYiPYxN-fh`LLrq$Ho@k|0K$U)VY3>npvUwl%o5zh7eYH~ACc@?q?$CfB)x@U zmsd(@n9-*EEJq5c47Rr&$FJUBU;1LIluv*H?FFR+gImf7D+Xmk&yVG>YvyPOsu>l` zdrYom{*byj5@9RKw21-0>_eRz#Z=Jc73{Iy>W`^B|5crb4U4bTjZPuok)AW7!l z{?`BxBKJzGm&C3+3}E7u)SU%l20xb;{K0(A5bHvqS7QsYMAn-#KP<&egHlb#$0uo` ziFa&jN@l7qO!^5fe66%%a?;gdhzM>YEV$C zFT<9~+nd`t@#JpKSyGUW`|2jJdaSAB>Pc4)zh1RAxLOV@O>_TKC%jV7YqvD2q<_Cs zOk#faMx#Pq<-TrRIk5%Oo-Duor6GpD$LPq)Osj^TMa9>7lG`Ob3vWR<6^|KiH1Ae@ z^?K$%yKR3~=Rep`bR-VmVJF}Jk#$1(SQ`E;Knh=`Kb-{)@WDwQu0VEd>&Rb74WzGLIm)VV9ZH?P5qQn|C0*20zkQDgw9@s2fQ_}D z*G|S>s}f=BrW3`JB*S=%Yc438+h9hsSvb>-V1au1c)T=<*`{~(WO1ZXP_AB+7l{y` z+!$8=YR+23QcJcSqxtbnT#0G1{p;~)(gLK@{`h%{aNnwJnp9JP42EbaPO?GMpoisX zQELIY2j9*lmO>7E3{S~yg+m?Sv!E7ep;ZrJX^A4TXbz2lK(QL>H?`)iD*Ec}sp4>F zsFrk!rfJ9w`Zr5i&YAz$aP@H$p;;lH)!p~*?eG?rF?&OACngpU%lULqS)*z3dZ%!i!%Td!?23x)RAjKrwxdJTw2yuh{KksTTJpIR>5>I%fEvM? zXqpiaPF#m@%}K$DrnVMLPtQjkS%5sQLE1-p$A5Err=`x((dY|U#wZ^tP8}ow9c3?0 z0I-4&V{IgTvE<5`?J{IAoOuLd@&Y4F;pX((b6kkn#<{5q?1A*^TAYADL{;dE<$n5N z%t!yx53BxK4H~X~zHlvw-k#yj2EBp?w(2Ev?$biF(XSBQXX~eP$qwil%Z3a)ouH$N0U};}`FX6X zv1Uy-yxI;U(bmY9x6h<}oiVnqFU0(oz-UJw{ZZVo;8EVS@Lx_&8xQ#Q<-r#K?o3>< zhQcsPC)jKKd?e90)}ODlR2h5@Z^ELOm=?X90$#hoOwC#l43E(%&DMbWd*)z=l76eF zMnV5;g*SQPQ8{kosM?YrxYmv4jahX?+ywN74pN(k@5sBw{Ns((n6|^)7!VlbH-%Z{YVwUy9-*5gr5>IZkQ@iet-P?5d84W z>svopq9?2GuPP3%)fD{v^wF0O^?%>KJGl6XD@ZzmH)p9<%Bp;vc#Lg0|MHC)MTJfO z^k-6iW0=DOC_aaT>}ItP=#23~9=FHmgn|inqX{&1ty@O0?R5ee7Q!?Erx^%? zZr`W^87!|I0j2^&o*RGF{yellF7^KdG3!fUVdhhfha#8ZMwf#!VFWGw58|LMhqnFa z(7A0w%yWVpB?Kyd__{m@>JzLgUR4G?MT;!ykna!$v4*}vZPNujyrP9&3W&R zjVnNqXbi^>44wAA{rzBMO+tR>baK52wAe5C90t>_@QP9W?WWyZ9PQZwHkBq}Cu6iSuJK|&jJI4eA0*`#!Md-%^SVr; zq}i`0OPKGKcbC_y(p8V{(L=|o9g$-|r~!KuY|qlqKj7;kpm5Y!Pv@Q4@CpaPhqO#C zC(85Ibl)P;as2ZDB|YdYmp&!0u$W*Q_*~tdumA$-c~m7B>gibe*7d!kw&_;;EJ8%L zez?4cR3C6QTRndB9aRd}W;$ofA&9SXn!;XGKfVnE2vuY-#^2k{b-hJU$Fx6TQ`Vg= zHH`=+!q6%q5vEcQY`RZif*Sboqt3QG%A!5GD&GRn?8b0~`BXfYF!YT{7L9s1kvuO{ z=MF4->jQ>r?Rj>|T{3sXM`r^iYQ@oslqusipNSt`W6~V5w;P zvfv+(a6K5tX9vg|ruC(E@=`5)L zmJD{gXuN)p+5;#4xdXHPS3P|VW2j+0P`kO%rhxg?M?@J?A<3mRp>Kn&+T( zoqxq87G*DRb}b3Etl4Xo|n^O<^L7&9e8cBYh|vwxEHySK6AA&Aw%TF6_n#~NTvjiNQOR-;n;LOfpHvQKQ zM+X*8dm9(jagK$rA?X}aC7;>b_03_B2G)?La~a{rj+7a~n{s1a#0>NwMiT-#DhaHd z5ySSZGUbPAGP@#-w6%|tN0hHamNJoK3RubYJ*T#XPM848m{b1xnuqXZ2=H`q=eB4s zixoAshcx!puQ2`IPchC~Vz189%&#BvS^I~xqoq}$;nkz^NQ0*Gm56(vEZF8h#9a}| zSV#|_k6vz$KEJ;G%>v9|z-3E3Z zo8BAiQt%*Bk+_&y`7^heAXm})pOjI%wnW_0WMFMp7h)|InJS(bu}I%-7^%tzgvYcT@J0&E76Z3>*Xa6RHB`;>6#R$UOMPNzG}NC z4`a^9UfkyTIcy|LXR?%lBNh`c@b0DaCm1bZ2D(qGrpjs@!chNejk#d049NGLF}(ccEr}7E?M}w2K?6l z{K?7i>$LA4I^= zvGGtcXMmY>c6B}hQaQbCQWbR9D0SQxe?KN{s41h^#NccplMBB9F&JO6`Q`oL0hx2g zOB{Y&Pj9%}S|>v1sIj!lUZnw<>E&`s!Y-a(U&m%riH-*vwfNp!I?(LA`5Y-BJDrS7 zq!U6FbH3!lml#8ZnP)ay4EMczs$ip|f^h96b)rtcx?=0vdR%DWGsnot6F1n8>&Np& zasBn{mp?dg6=P*0OnE8_g4k`*f)@;_rfUaD2uL|FTw)>1Px9+bUX)`>r+Y1cQacuX zkRG&=PaP2=s-lT*qA&djzS*Q$$61 z-N>R3ZF@ZS;XV%pfqm`3cwNxdaaS0jPBb={%?{r)VMgV7SABUGq63cg&b(dU8=6;6 z8c_O4KP44QDtBU$4K~U7?E_OEBU`B2*=FR=`q-nlwSPIJ zIUeZE)>m0canOhRqpjtL&x9uT03=2li+u<0bj~n~M(9InV0%&)_?RXcdq~0DcNeMe zpuxfTJ?<412ul76I;N$~{8=PR^*A$cs{9C+q`JW-gDh|CZjFs2n2_BeP#f>4T(NFZ zI%m1XteT+pP!EaB>4M?+f<0rJ!p9Qh$r0DNUQ2Iigz@2zBRshgedkJsk<{mnY%w{& z;%;@xga#xYmHdM-r*v_rc_#!llW~nnrKh^XRglZI0co8aCN(y}DCP+9-2BM|&e&x3 z#s91WQ|ijFCS0+;H=VPAdtKzu8a2%IOWHCRQ;VBvLk=`9K~{r@3249w$g==<-~2HH zQ`!4<(>br)OA{wwS53NfiK?3R-gK^^6h2J5aSTl4H1V5x*Grbzj`f>pf%Ecv95u9a zGt8;OKx;P$4cpq=^92*3bI3S&z2Ofw$y)p3^TB6-gm~;%Nwu@>h_lE0x9mF`fSTle z;2@}Dm)J;YUGen0(LD%AaF=}maT5|ZI6k_C2DU1GW57jcb&IvBGHZs( zQD3c>a}K;6xA$UCpF89ygANN0Cnf^oGn9iEuui5*6#Q{+ zHw#H1a7UriUnRoHJ|T|gIU;iqjgG-WL=q*@Ouh^!4#{Mm`#)(kRlHfzJCe_{k8QkF zj7P`@qD=sM-?%YS;W9*qLQ|wX9U0g#$Jl(7vhQx-P&BEER=|Vx2y8;+ms_CU771;4^LhJdcHFesTg~SWutKI|+X$x>9 z{_@=EvF*zKU}{`&5)pC1n4D>U#3lZV+wUmt$%p$-P#Y3M>cMM4J{EDUs16s*hW1~v z#ic#fVDvmTnEVHI6%#C&wqWZ!f?86@$y30+)I>|p0qYIVLSyeCMqp%84aQ(gwY$P4 zyD)1mTr}%V}dNB4eQVH**TQ+h0;@Q7PlSD_%vL1`YX(iJM?H$-fxZtk3*EW zUkrz;-rw;Z>^USHI2T%xH+x_wmCg-@0IMA_fdASG2U~hR!)MSu+0wLq6DC3<6KelT zzK)LI;@;^^132=Y2%vY={N&&J>$!%J)$&K_DxVKx+R$+9$>VbdJvenAnLQJa`yt+9l1aWulwBJs>+PVQXqt3lgVlOO;(~qA&%b#00_RQwo zOe-NReLOrVyW3|+N77nXDjHL+a=&!=6n~eRyR)*3@1_GEVzZr-80{IlCxJT))m7<2n;|g!U?unaOiI9Na(Xcuwl+ejlGlHeM1EVIsZbc-d@_Sm_ zXx@Qyvh_R8;ML{R!P!9uK047aGI9FDtsX}EJa)bTsQS3BNqsTQEe6k zsVK2ov0U_vYb9bT(;9X$U!HYMS-N05cwG5KT`O{+U@ILy{~mbt_gRcq4m!y@N2;x* z;E+-M0QGP{HI(JM>-)>$nAtWwMug2Q%s%SzNDvt6)8UDh;W#iuA#wJbLKu9h=5YX2 zF3Di|BAzj@P1a>Xw)gJQ9^LhZHQ`FYheiWc6lg|KC#bI;Ix`B`9BP5{0*D!(dw@LP z3=6D|T`!DgHiqz6{mZ?Sik2%6!kr3IMtewedb6dZo4|7XG+V1NCIE{cI@sCH&cw%y zf#@^F0MvTCb_86DA^);bG_&v%;gt?6<{y_en!F+$j3gleO3o^S>Q3 zNFW_`{$15wC;@&g%#90Rfa#GlP)tCrxgiU7_pVBC(kN1T>s26?2;1{fs!oj*Eoosv z+=r&q83*B~^zvvWEL{-vzl#5&?x*X!^>ld#-l5FV>M57dt0-$nC+sRBf-At0dSttD z3@$*K6M-e$$*?5jZ9}C%=vR>aECpexh^HxS$)V=eG<7=3>Q{j4C~L6ko?R;lJ<#=A-d_MYebaqNDgm~TiaEBpH zjy-pfL*2ITbt~lAK9hT4z^ggq^ZeVlkB|QVSE+aTB%VTXCy72V#8m1o|l8t#RS1@7MXNYb`V_mnx$muOha5^Ye>Vbw#83K5sr+0 zv|dsViNy_5%%}2jej?j23A!j0@yGna@%L=}^*M<|Q&2?NJV&VIaDe7JWx|tsw1{2Q zo(r?u$h2>*5|_SsJ2g!-OK9moQ^}im_|$1uqXam;#}0)K>6vqooJsZR@M<3 zd-vZ2ejD4E<3B^gB#g(hXBj}KnqWaMUbeh;uF(r=gT6!@(v>s&Fqrq;2OjwYR*}L z-~NbmTHRcZK@JeSW(U)r%)k;oCw2ps3MNgZUK=j43>Dzq?RxzmxH6$$T#A!_v{coC z|5Z|#h>~&JOGxDe*V)wOkEf(vv(nEsEczn!*z4@VR2|L8k<@1=y`*bg6IB<^P z?9X}Te(^#xTJ~sTqm$(u06~~Xkd5mDkSvT6L2GSQo^<+ZKnZn8kT+7`< zESoJx7C3QOIOu#M&!vAz+9kFftbDjn4&z$LF7ZQqDn)6BH(+8)|6WVaKFWMgcc{l!{V$wonh9MDnSlo zkLkNPb6VP$Q1hudPC6X9aaC zSu(@zXMs6t;i&+X>0S$ZOHt))+D@AlnK(V2O6t|k%Z;z}B_oLEBvvatK0Tz+$nLks zI4hLA@ds9;nbt$e*zFe5x2x@rPZR`{O{2ETc@9GGMM|kFw zQH+;ZV}1-ljEuQ*j@TLsg2d&V@(0W}uk$g)eZrS8($W^vjIek^jVFy-oiwK~7Owjj zeMWsUW^Z0y$7}z~NRaKq#%h9ZI=W`#8>>Zq${G-3Eel264Ry{ARnuHP&<1r1HsTs=9zn z7MG}43ZRv6`6fj2zB&BQ28vP6axD;>@r@^>o{*l5ToerMiAm zie7BVAHIS!MeCXLyxQms*PS?cG~~Dw+9N!Hz$`?7E!hKV$Dh0lyP{RRDyutp$Q@@b zYR}@1lDzZ!*(@PN)RXP}$?!+D8kVC{_*lEE9Y_RW{w$IUH$xW|LLov z6t?vO!$CWtaQkhJN?ARB`AHA|eew05i-Ys6ty={fAH|i{|0hg)adfIf`t8B3x8B_y z+6Q~FeQ@;gZ$aE!wTY6)x4-^;{B-f;HQ&zeokk!5A4Pv70g=?bPSDdk5B!QQe84)M&0zF#mHCZd9Ae49aFD7 zUVi0BL?UR+U(T4j7cVn)uKWeZnPgVn9$yLQPTN{(%msBO?OPKJC}Es9;Bx1B)3ldQ zquw_x@Rm<;iU2%0$+R!2C52?eOsMx8{<)*<(%uh$^YQ@!WDwPabzVb*io;9CnE!9d z*ch!~nq3%ZueJ2tPe0aD3;pbpVFYQgHpwEPh0%SLFZ9}A<#C{yJ$^_Dp6ZxP7w_B9Fx(V1zq*QhjL0Sx>B0r)l!99334Li9Rtz!6bYYWXvy!o4egzQV7q=w-^(v=@hl)jSs{>xMrY2Cgu*|`4 zFrC?t4EHE^{;l$%cGtlY4FM-sSoO!sD)D^dbHbV^)oPh#kp6xQZFC!>!vsCnX?qNY zRFo?tJ{_Zmc>7Cxw)@I`Lhd5v!jS(J77{;OBDn>nve?q6R(>g+``nhgXvlNt0ZP0EA5EO}i! z^`7+~sBILIL{YIg%7fhzS>ZvE4K_|^Z=7K7?znLpDUzRKHj`CWM*m0Q0qei01EtM| zWkit)et6iN&1JJq)bo$yQ=j(myu1c!Cr1Ru6$9324BXzPW>J&%GrS79(fVy6y6Hh2 zM|B6a64?cS_~6b$2SgP7xzb#lgTSlDJNfp;xBp*VY@ylZd!jtxs9d6PaPR|%G$aTF zntMs5XC+8^XeA8n&WjXX4KK8K<6ob7BeZNmhEcOYSS61_w&8k@zuTx8$2va_ZZ)ARF%MAiq zPQ(4cpC@g0r#dA)(1&~eo?htHA5EPIiULRIV{^uHyFT?2vpk#iIC06MZ5DCk#+e+A zK4iD1ZVc`G*;#=Joo(5WA-~}*_sP}r)!{t1)VF!s5tk<93@lq(_1ZUq! zp$B9Aj)7PLkZh~N`y!6AI-T=%)rMaxs#?#v(ax5>x;$5(xVl{q-@SJ%^kfinZEC)z zFWbtYDm7aBS2L#d+tQxx?WGf4)S&hI@%E-FNA6a6;>!efCn%FJiHj&1qf*S-$IbgOUhyxI#{M(7*`d}|c%3_k!h7A_B?`^=S zF+{3qjbm{&QB3gxa3$vQA^Y4urSEj?`1^L-6g!TAn^r=l;iMgJcnrci@X}jP->Or- zB?0v&wIx+X>gKq<&b1rc90C{=Sx~OoHbEVn(u$Wz#iVo0o4584w`gG*QS9F+x8x(g zvOb#;(;X_2QF-V&Vt}yJ`*9$KhAk_v-{>xK%AcI;7rAEtS>~p)R{G$Y@rDiPh;=d< z5{c{a_jX%msclIgnZi+hi7+Y2zkkB>k)w!Kvutr+(7~b@D{H4dW<#bYz zX&AP10CpX~G5Dmn2Yi_nYtQU@t>7{Ao zP2xMsoug^Sl7~Dg4ZI!>)blMrj5*5+xBIEOwT-kfVeqot`go@xV3q5)OXNRiQqt_( zK|>AJXJ|+h5b1xR0j8RjT`9%>_FMvgJ$z!dYi-~#zL8(!o%ep|F#)a6D3%o_B?IR3 zty0>1f7f6ZGL0#oY69$w1_Sa-&>nJ)T#23akR2op$Oj9(!)W!O6xf5>VpNLGcP=p7 zHxeGd69^ETt^Co!P$*artO)n`=@R-SIRq-Eapy@-5mi%*U}WeETTnd&m$$#*C^LjR?2gRSeWc{1~tqbW3j9jWwM zK*nc{GPkcts{C z?$AUru3@Se;vNR}VOY+l^F@qyd>}pQG~;o7{B;yvL>;~z8o_PFqjS!S{&0oV$ZArK zm&g9y*pF+3@n$P6e>9uh{{$wrtDoGIR&R$=#Y9fOuhf0&z z#;c|u*Y&{JHmz@@PqI=cs5y?`g60Fu#Z0zuzWdG@*|e%3oD!6bolV0{d}^G5Q0_$D zeF1~^+v_*o{H8|MNs*^UO5*>2BX|2pTe~*D=KUBJ0O}mX&R13KsFJLvY5yPwYW>d zBm23kr}eo?Z}bqPSUY78L|SnH6oh~yJJD`rA{O8?VwDN#^T~TgQ4vrBWwF^vm=eBo zNmiTk*O#roilltxYW<+lV^cLVVTA_lnQ@LU%Nu)8O9w1e*_iiB2lmcb1PEr{SQHyR zZqdSqhULr0R7f`E^bF3mZxM&iR}j3%TV~zrlLYg`1g9hWr}8s$h{;Zi12)Vv{+wVM zabzt^_D%5Fhi8$RF}Z?0MQ2;|bGr~so}a&0EVm31zRv zEJrAyk){GIOyOUGdz8O2qnK48o5oyT9ct1dSZ9Gv?L5!8)c?prvzaLowPd7hq91H0U zC&^ryqSI5X#3C=a(`{<|_WIM;uO*3a&>n8B<0|X23uT#{kwKBiVsA0iEpSP><%g4m zB6MSzhx-n7D)-~K#1M5=XAnlF=xIGIlB2h@h(Wyql#7>rEl*@D&L|cs{pe5959|De z^c9`oJDkG@x@#9jn$gjZY0Uykr^%(d!28cvf|mE6s##vilxra&1bLxC-3-%+Ab9;c z;O&z{ygCEmo{-O`vevd_>0Ur(J>jB(_muDvv*hhWvoW1QsKd#SPsu#Pq$lfEFBZ9A z*x3609Y&w?vJCook$N1F?@W=WwjMsG^G^_Q8f#ImQA90WXmdFKLSA@|=iB-b!)a`_ zg=P#?>u3e*GtO%_A!+Ei3~DtGSptay)WwtQO;aj7E}*};$&W@)p)91l$r6<%;-0$c zvsLdYebu-zq|;p<`gOWE2|6~{Gc=%y7+~L@BiP=vJ}xDRCO8a-w%tZt3O?XPfhc>L?3SNdC{DmL zZm}`&&;f6$sl5AJreCHsqp$7WHYzmD;!k(w+4L^C*rX;SEy#LFS zMJ_sYN3FYORPvif>b0?70@0hYm{kCR0(}0X54@|*DPW^@=?mL3e4CiiT?r68JnGM_ zv%R>^C>rC@U!SMbx4qYQ%{=C(_NC2_K+lmt1cakq#wdM`hZM+lqXWjOvfs543l;JP zhCfUXZ`9&)vzZ$eV1(%8Yv-^VTjGbu!%cd_6FuQp5=kRa7A$eeJ`cvtOc&6?9CldZ z!|@NVT{zD>dRg%^mIP))vxdSvjn(rc1Fy+=1!{N{DW|f(dw_2Vo%2&?9SkvZ=1_5? zqL9a&Ui@b%7ZgEc;XSS4MI|b!kPG3BY{FGK{Bf^>X2Qxw7R}oF;MSd&Y44k5uOjBu z^Z0}B7?jM;MLBvA$yM-j*P$@*Gx_yfJ^y6%tvMl3zXnBKH{xge@$}*15?^{dd%ivW zDe2_;^5mOKz`mna47+qyF4Lfjk4x0#uaxK| zaw5Op@@!;JDss*CA{iyEK5Aal8`aPFU{N6aa5nK>z-lM!As1ruzO=2bP8j7@?ew@p zrc==CK)|k8?+hd`9d{ri|6jKL11*kZYuI*C6(IHgCL@g8a71B+ZHy!|`g1=k3+?^> z*Xb5eCNnZ(#qutmb#HfMxM=j^5}S5n0iST4%Ddr&q^U1sC%>lq)Yxq-=@1p2%N;}PK z_s4bn!zYA{p8h@lc;JkJ6>dp8x%)AN+bg z$$%HXk97`2m>0KSa$uumq8eTdO9&q+k1$E*5P2RO;*|H@z8_V~z0w#7fAp1OvGYi; z@u{Srte;#)ux291RsowJPr9m&8uthY%Le9^c{}OPWX4clUGP>wXaDt9h-TY4n+vg^7UaIRlL$#&vmX%#VL&%3%=6@ z(Z-8&PGb7%rEVhF5(dE?mjOr^Eb8wjwyTN2>4-mFaInBme%rZL)sJ=|5AON$6jP;Y z6rFnaI@L&^{93!f6cK#@Jc}eV2mfe^uwbzROX)M(E1lr%!deY5GeuLIJXI(lut*Ql z?FC4~n(M)Y=Rt};oiX=x{^Khb!s_KLhvCl6$Gg*yX7_Y`c=q+_%Xc3<{W|}4Aw7iU zvSeMZJ{^37=4Xd@N8e?{A6|U9mb7y8@r%3&M?D`&(|Pyl>yPWx9~wS5A8=4pq_6MIYhC?$2q8L~we5DT3+3KAnp$p~rgM)V-YxjRsh6^yxek1-uql zY^HqSzoq5w^JTiP3}v4b4!RLuO08bpBtzS}9knS89~ow2^}Ihq@H_91VfFHd*$~{) zWW)`&r{2^M(`vRS-z**%EQ-WRO}sUYakEJkK${UwMz%q-HUad;fc?Kb(-Xb?(Axqw zQ!I#j$-G%hTCyc?8PXi=WrOA9oJeV6<+a;qQtKvI0tF@lv!|YsKKFQ`%Yanieu+@D z2SzpV%$|XOahj3}nDL&EztVp63TlRW>H%qK$+uaow@VeR31p2h{-$y zuV-u&eU4~NWx*ymMR`Q_MtMPA=^YW3oip`zBOpX2p?KV@U1|IYEX)`gK-py=3) zOjf5OWa>76R^D^!0BmT5Px#DaF*9?`I`#e(%IuzfLs|%6Vv|&W1jXWCSQc#WjCL)1 z$=r~pH-W>W8SVjNF?&9i;AQYX8>37y`@b-0nuP){2GwzPiTn#?Y5d`Ut*2E!1di~+zI zq!Qe%)Elhbl_*wf74*?T7%HaC+8kam9+?(MKg-5AchLhA*p2gp7 zKu9MEjoe2`k{QuDrh6Wbnk)=3-0o_6S>|jzWuWK(aX3t=^K{qYvcR@-AN+xjN}o_i z@bXA{N!4fi>Un2&0u%4`P;FMvd{BG*gn0!ZA8p|(gFwj1vdmbdY?Sb#c_8jXpqW%{ z_g>wJ1BTV#vfduq2TQ?;X8bp=ovd;*0OWqyOGDgLJ$9+@^&^!{*X=>wCDk=?vE!PW6^wzCj# zO5f=h#mndoIwjy(9c?zrUD|O2+$8($zLiXvwGOuy6Q)Jo+a^L(=hd$*^7}N8LBj3k z`>(}Mf_oQTnO-~m7@dKp7aWLdTQ=uD=dNLFP2xIOF0t}g!>oQc0+XsUZDd;dD4q!n zfQ(!t@715fZ%4>;j8&SQt*ML576&a>&$bbR-#49irW&8m2*6HK>IKH-aZp89nQ8t8s##(mY&;!LHADZcculCkl4ckaufM zyxT`xWnA9nZ?4AIJiNO6Y90{d=upD(vVUu!)PdWxTeHSCK$jnj(8 z-~SqWy0797>HnqWv&ZbPO8!x4ZSu4;QEWF_FYyFRG1}t!aXxNUOTj@P4n)%7Kt>X# zN-OH zvtp);f#6myKSO19bE{!2ckOkC=depyk#3a_ zt8t!`lyz{45=PG1K7i!sV*?^@3mn{^L75CmdY5m7{W>u|&g5Z)-1Qe$Yxa}uk%MT+ z6J)D$f>wX>kv_G1i}fO@Nn^HlyLoStT2p_89r zmpT&6;@582!{NZ^1^icchcFBo9OFy;DyF|E!0(UiaIC8u2y5j?sxK|ofS8q&1IVTM zddI<%LV?PCY6h78o&@Gv3|W|KaMWdohuM-IZ-JsCGyJc(mlM$lMdVT0ZdpH;G5fVN z7`%^LbnF8#<7dM}&u)Dhng^SkGrKq~Ho>8beGvO4t?6l0T8e7>nJ5?vG`A6fJ+Es) z&9(Tu*Ytuc#d5S_3cx# zbbH2_EdORMm>EyLLWjcwSzQ`N)0dmDsD$`wF$B}jeo1^ z@?p{mfqtJ_QmO2M!>)V!ly72q@Ob9`ite+|RTC2ao+!ALGx11EZTo_3k1H<7fzjAM> zHuKNY5J2Z8PugRor%l7CM6=T=YQFZI6A4LMzF^C$lDN5&yjD&hMUZnaR5=|+X4upg zrWo2qkStcZ3C6P|?m@h{_4$>-{tehO0OBdIfNmHA1I=jyz1l3j$n!?<`KSI~O_?Ov zATh~{+EZT_xG%gs*p+A`xwk1yom6K5p(oT#8o6ipp=N+}D8`$8w*=|AAMg3`MlyR0 z)QI>>%ckX&bcqlIAsz=J&Ey=@2DNzLN!f$;JF1hn^+E%sy&v*QidxdmqUc3obO-`r zh)VU|G%0wqKRw%_pgGO)yEQUevhJT>r^9bOH=iQ0x{<=Nyf=HwQasUyWjMEDx{9`` z9?Y46N1#@S8g3%26HLbu;g`=|(6fQartHzS#QNnVe8{)n2ulfMGUCIi?GU{K2|u_Y zWkhny&t)8HRi5a{TOAcK55E~LjcEbu=`^5M<{NK=3Rb%N>r$9t@ejbC-HowWmNxsu zb!#RgWqMt6>iFAO2=)=v0FN`bXFJLOtB0a3oRMdfj8eaVy^7)O$C6{@7G9JA4T0wy zgWz2MwG9B!0wZTi+90i!kBotNJzWBA^%N&80z6f>59gT9|Ge5 zR7{i3M0K#VJf{hH8X~B2biPR>T%830$D&d8&wtGVCm_m2HBp*PQ-LAQ+>7xxC{l3{uVq2ed5 z@rgpSsj&_@nTiCMOx6Sv*$E7RBgh31; zcLM1j;TVT*O}B8VdND+jeUP65cbO9!yEw)WHySq&mR^hRn=3Kk5j2y%VLFDGGVMjX z&~pRC)BS{m8N&egmVq{prmxkJLM_<$SWtTA!ZyGFv_W9$-%w>wuE$|1@?8g|L8^G+ z0>a$t(_Y?<`9+pL5}5Jf;RyCI@Or>NcQ{MhruL?k(wscTllBPAuJMIZhnhUOo%ym` zyX|CM{*SyESW^xku(9g-fNn_FvQT|s3YM&{bs6R9262_824z>Urapc|A!bId-*h6+KVvx!vxNq{CRlygxTR71XG48}4>U&R z_Ug~qXTdF^!necsMvWxJMw49j>_(&Do|{4sY~roiFmq`3u4r^b`DYOrXYj90D^eq7 zM|UV9;HiHtZ_nZb*|kr zQ*c)u#oiAeUrS+zn`8a|{Bhbxx-wgFL;(Y{{)m|U)0 zvH$$p`445tM+_Y835q(u8>dAHv_T*?3bNQ~+e8xFa|-YUukP=st;{RNiH>gWg{Gw?eaAuD3BLRIAp=VD zd+YumxrmxgF$<0o1SQ$@q;+XP`09;=ak5$shNTni~$^|YiQ>huVyB#3LN++xEL|kGRRC+Sx+R9cf~08_zJw+c{m{kKKLeT&G!FQ%=OV%=67; zxSR_AzSzCR)>KXI=BzNcWq`V4uYU!0CIYo3GkVnc-2hggB}C%K*t>tS(W+&Ha68C^ zp7fS`_t8vzbjQNk&J<3KiRtas`&%Q64-jx{1P>yy#)4zMm@|$#d7RNp4C|2u?M)sx zF=IJ#mg8ihdJj1&VH9U^8I8s1A#61wW-sy?)u1{eVE|JSNuFeNrZa@+*hiUr&M_RlGt;(w1=Tej1GPn_`%V-v} zd}Vp&kW#<&>p>~_XPIYe=b=L~XpY5p#Dy|)G>wwb2_d-@aNIxNUuRf%Ldxpj@SrXz zaCKrkKnEtzm*td@5CA-*7Q>_Xg&U#qz@1#kyBvelaAA;u#aOTSSn3%Ghhl)g3FSY} zbTsWhBdCpnGw0n;9dY4+tdYkfH}memBeNN(Fdj=W(0iAP_aU4Uk;zNdbZ0t17~5Nz z9yM%xC4B%*L(ry)V!-3!ls$lvNtu^%Ye6l=bengU!q%Yvpv#mO(Zcox%PR_o>1;A)A2 z8QbyWj;vZwSX!WYj@S8V|BnyStdDp0{U%1XU0A;6j-1;u1B39hb=x$BG(<9I?sEOU zZIBJ=EzCS_sK)8EO$KJ2O1klwtuo~+LgIU$og}flqzK9`GQsyphw{S>k`aad8*zD| zFdU$9fyP+kB=s|%_Vsi2%72pwFoT*QE==W77(b(6RE?=m+(X3;5^3tQfxE-kafn~k zZ())Pu$4VDuwN{j*Adf6QHXA*9iQDPJ;qg(&&GoN!vSnKo0g|w&m;8+qY6U}45p~- z{wbyz6aGK`f&GG^h8PAH#t$Y^&?!wmj#CU$VEt{`)&Ta=du*Bi#WlBQ2w-3Tk_CHt z?IzPQ`%=Vd@sF7LUsPLa!ilLV`>DhG4D&dx-WT$5xuu{GfaswBnquvafgv~Zbe@Sx zlVaF;a$Sd2mlNl%f|fXKaGcHVus65re;Q<eFAEr5>hnJj2h;43noyA^ zb|t4wpl&@X*+?IZXJ!aET=sqq&?jXvHm^J3bl-YUL;QVHtp&T!>sP7SE>7l8$|N$J zqe31IKYjkXd6GZ7x^{#JJ>ubp_W5txbpbcoZyyh2k*+@LaY;S9p;}Wdb`%MIchvVu z{VcD){`_VuS2s@%%#f(&ge+j8*J?_6`2)wK)s#Vrd;Eyf zlBP{JhT641e_To(THSv8^OalEEDQYGT{Q7{JEceqSEcAKODd+pA;>ZYv^2PQys$`Ib8-eOBrK)U7U z5P#d!XsX4AV)H3r+Vm|V5qI*l<_lm{BEYQ}q*;Xx5Hto8o?-;Uhv4|UU3g4)y~Z}S ztR{wmsSSsS`Gh6%GO%*2`*!C)7I(^ny^rAzTF?E9sP}9Lu8^ZC!0z?Ea|mOhd~TrU zL-SagPeMub#p@RvDH3_ZJM^-7?Z>VP-{vz;qmF>Q>?m%c9+s70laeffEs{G5C2+aoTG<3fee?;Elk(Xv`Pa<&VJ~Y zu4zY>>VX$MlAuduao%~%rHe8EIJ)=ih{(3XsW(O~C zWOoGF(FK}sZTp=12DNSNU|~m7T4sVjsh=xo8{S*1*YOpv22%Zb7+RchLAM2RNG1bq zy(Ee9`dnwVbPW7RnaO0jc}e@mlYzCOzt!*YI&fE;IktK8@~6a}^_7ohjMdhlqsKql z&((?Yn|~uFabY2b({D#QC#-&)J{%FE?|xrg{o-SOhsB|G$HgIeU*|SjrMeDVd@XGw z>=|mbvZq>)#y=OAlTXmav|vn~(VxOx#y5wBzS6#xBT=lR8A5j29}-GUJ2lPK^XaR! zg7+t<=h^~GBoK{N_tlXBj__5El-;qoeBS*zc{+U7t*cgk2cK)awYvFpdM-J5=l0`A z|8`af->OhMIk-83J>O2g`r_{T2vs)O1yi4ICiwoSRQi6XX&cs^ux!a zD@ zOYvz3lh1dVcGiz}CpgI_f<7`~_9Q}|U?nXE^OSpxVu}MZQXtjOId5|7SIorQ7;%Gx z?|nwj^;!|W8GR#Nb8k8py)e`xdWKaI5u|d?h+AL0PN<$@@9CwM8tC$jwv*3lVfMqp z(~@cMp;6W={guFCA;jCu>aX{(hD+{dh|m#X*>1tZCL(fFh(@8)I7IgrrVGP7fH$N& ztZ#PJJ2_ktU7q-KFwQ8*269{ndeMO6n5}W|ahAL%EBMHv;UQK3wHee#%#CrJ?EUi? zfW%XYdJwdZNn8(c_W>vd!ezS@f~wCUKQ4RfSgg0-Ps{R4Wr?Lz-@pIDB$^)R5Oypp z5SX4y7Q!YEMhSxX%I)L@O%0}L#5j3Rwxj5xpJ6ra$nnQpCs&xmROeP|m=9GHCneAr z(=BMcM$K=WLiABvlvcesp4fFMRf&|WFsq0oGs?GtVxu_Fj-MxJjeDUu@e;HfRt^n; zIKQ9Slk0Zgy40u~Gy_Xpr1At+(lQBEXg7v8fp;H(d+Kayu|jlLQw;0`q>NBFTYC#Q zKbkg`3}^gKdQ2tVOC@NKv3R#9YA9Z?uEPsV39>!1Zj0Zgc=(_%UFAeuNDdcu@CQGsy(-{I^asBq9 zh}&ZQ@sLD)b9eg+)&T5NuxC72(n3Nsuu-y_M#OKMyfVs9PK|a3mSDnYnLvI`> zGJr(V2F+BLl1R)x>P~deoEp#tenHMqKAjpoh6e4mF{)R#P1sqXv2c~LS;13`es-Yp zpl3r!dzeaTQy55r@w4f!mqzzfK}Ks;`W8~vZyEc{#ip7|zW{yaO#Wx{cAoEd&HP>a zW^1X~vi=7N*VuM=fq{eCD=dxist{eqFrdR2f%EKzCDpNP6eZUy+p(45nT^Cq{c{@SM8F59ZT%BS75T0= zqlsx_XvTOfA1ZN@r%-idn=b}s%1qJme!kKGqvOfe^Q834(s-|*VxGpS?|{$W^yyf9 znAXqIC}&6=kAkLUleV3J_Vl8`-=wK`IsgGY9S4`+x$j*)#8LBs|ILNk{q)s=o4YxN z2iu|tTqtYgXM;ScVyFy_I>zj>U4o5R6+Le`NG>m=FnUafrh)RZqwCKG1@omEEY z%?L((&v1KqhK8PIep-3!Ba9MN8s;5jL$H(kxR;>q(q%&QDfx?)80fTQMbMA!&1^_R ztt{g(VYYz;aTC$>YXTD%Y^&dA8XT?K<3nVs9y5PLFDoM zVoK>4+|rTuBgLQ}!&)m{q5wv?Dlwz6G0Au|!Qz0RLGm;d+h7%i zKw|#?n~2tzO`-0WBdj^`4fWFCNUfqe<GkmuRC4bxEA>!6aaHd)O^Vk@)yfm(Zo&RKE<$}HZ&QAKyU$v)o z0%iW6dP;pT>EfNnsUF@pdxW#+jaoE0YT6u_SV%wEEBN2k6la2S=^STSN3m|Fv}{3n zTNVrKoQV!iw2H-4tS;jgNa)YvgLMSh7?+ynNK5n7|w6`_nZ7=@#?S0J>@0%VJrV*j`~Myw8QNu6n_ z5$K&t_rZa|bPq--FSZw_GQB|*?*CsNhwI}Aq_rs(Q?PURz+?v-hP(W%WlzVJ;g4Vn zYZ<59o#9e@=vCQ`yy+$u%rZNSl884Y<2-BLY=$tFA2*E^PT@DLb$Olx4s>aeRNC+EhY_gx|&7U0If52B~NGR7g5V)7CC? z>9XOgwF#qC5QKd1Orn% zM)kQ_3}5w19uK%Dgf7@ZF+^Y+lej!0z`%gtOrpo9hJhj;Q)t&xgVu;O5D2yTg*;>##iS6N zZ6b}_$kA_hCamL1SHl+eMj%~ORcw$E;P=93ef4Fz&4?BVxOmrlo6U4~)4#L3_=Cqr zjpic()Wa76?vuH@p13)9IJEQ(49lbpK5jx!cV4Z}EY}R(NmsF(UMwc+nJ?h>DAiq@M8xv@)HECd z;N8L=|4_H(B2DZl$*Svz5!?PWy0%miZ;Qd5m=%joN(FfBNQ5;XT=z7ZLVpn9kf)Nk z4n(9D<(cOZD+Fu|P$s{5RX&YEG)4j(;0T$r#^^KUytmM6sL(`*%0lWshl4NA|1R=j zJfALzH@H`zW~dU4q*w*vagx2U?6g9Vh;@53QUCX-{Fri-&B-k4$yj$E&Af-f4If zGLTrE^pL8O309CIN6d##uQ{_du%kmY?I&-C!P?u=SH>X67e?<-cX&S2Mk2t4MqoR4 zU;|d+H(4f$e6Hr38;H8`%nVJ#g#GW!IC@7OwU?5`fNf!a6PhJ$So!)U)`Kg( z-#{p;p^(azb2#yk{?mK#k351t9I1O>F&ghH0`-LI^75l=L;62aaL-G zt2-*;Q24=YVpIdVyyf4%9m>dH!IzHMHdD!4daY~rj_DWcPlvu9XQc8Tn}_3Jj+R`A zd7bsK*2LF*ew(C?^aPCEwtW+HT4Z9fi-*HiA3*&-EC`aXHl)K6e|L&71##(qx{zl( zY$+r5pulptMOzkf8k@4oA+DV29lgU*Oe8B>iMekL-zjufFeI@#wT~$nf z-89ap8h5c;1>Kj3i0>xZM8L)^dB0<4I6^3A>onu@`Ewz;PIjoKVQKs=C1gTH?+uM9 z#bloI|9jp5-Uct^m3&}r(sLa$a{B$Vz|-FCZ|r!;X3w!37XIG$o^eanLdbycPQD%- zq-d_LUoNJ$RgFyNtbablo>$-J#Gl7gi(SbKfd01|8N5*e;?KP=m0#l&(|x|gimgj2 zvVBX!E>3aeB*FToUW4UHPHF-j4jw7Y#JFxZ>|ZbJKeZ#O8YC+&DBx1#;B9nUU`3b1 z9yO}LdLuyFL5uondqf(VV(H+*NsYL1Nc{A6bD{Un#IdI^W>W7vwa9`rrRmtdy;oD@ zh4vtg+`)AEd|lm5zVx&`)45Q!Qzf@nRTR*+$soO5VuI)Pu&AK!SUDcE#+Qi4dgH9j zCgw}I1}E~^r8N1jCrM^lQCAJNqRlvnAo0D@)w0NJhXdc9D*38m4;TBd?{;yBm>-a& z+5PqDv!`wz{yhxxy5q24pU-Ht!RMx)xPRGYnQ=;o+s@n5+go`CW|~9$jz0vel*5k? z@PmuBnC`Z}_SID(+ZTOaC4*f5SZL4EK zL2eUWPv4WbAf8V}v;{?l#HS+$)|9%1*};j$gdVPi!C~|mc#dF?LJ&4BP0r2Jw&?v7 z0U@5n7x!063nx;@m)gWh+H_y7C4SNi_sJp__cSzx^0#N|&)}0a#`7!oU2DhWZW%AW zw587Bu~SQ{jlKY;l&NAP<^8Uthrk5M2L^LUcKM5YuZ0!DJwn-x->3586pP;aroZEk zZXm{Opv8g1!i5^vQHmgrtFyJ@)Gf%VgIRl%!;Ifou3TU#aYgI7KgK0H6P{$6I$pDAaGKmrmH zUR@U`#)is8S#Y3q)wYxJFdFfDh)>%_uqV2du(1RCjaW?_CF5hFN9}ITIb#yXy&<9J zZTBPH&v9NiR!-$}UG(I{+nMixoGt1#+j3l;^ zNGY8;7YJmZ;GO_|pH^(7kz!Nir0mz@mxn(T)k?c9Hgl#m-YuXNP6Fq|K^!4ZeRHJ& z3h08H%={&Di(-$Z+dw9Xxp

    pCZm`zrtpZ+Ve+>x@Cn-z-cUP$J~mKTg+g}T{$J7 z$*1}YcnE(7y)MX~ld%Mo5=0SEWH$U=xDJ=+N+77q69JHTRwTrvY=p-Rr?aaU?T+kA zEDWcl>4C>+m?Ndl7({I=wk|x12cWLI`i@lTvrZ7h{c#uimE*29Ruf;JPW6abNDXrI z4YoAiZq5Rog*tN4F!mA3Z1wmzYm9)+W>DDO_C_} z5m(^b8gwoP%Edff&?Z*)__GmIl38rki0jMFO0{Lvb_2*dExh%)RcLl8Vw7*71{dDcE6pp zw2cu+-e_xzoZ$~`9s9gL1#^lkcW9&xDJK~?LJ2p>Me3@ZTj>4pV;XqbD2kSrSs5h$ zI3C3hSZ6~7$#12;AAu)o-R-reGrrOX!DfU?tOhwK*PHK}ldpa~|FS#3Ef`*veim#iReGO@MgsADjcw1?6J$HF0bvq1oiB*!)~AIZ>1+h@}cERg*8D81E7o5kp&V z;sNPO`#mx-&@pvtIh%J3(mqY}e^Jzz-zm}C0E=-Z%2qoBsE| zVOLIx^@x&YsNmoMlGaM>xCKgr(&Q804Cj>^i>=I&0&sZ}E5jcEq1&;{FeM3 ziE@3o*#HRuZSn*QAA=ujeNpBcK zE>i)P?g;ch>>|Mo9YzYhxen*4``_lqS4tXI<4WR1`5G9FNl%x^O;N}0MNI)6Fyu-? znw~ha9$38m=`2S(KP??ESkAHb$;Cz~fM-7T>np)yE8oH>2xr176B==LDoIj=e>dx; z$U4rSzH#d5%jwQWNfy$ThhR+2psAZ!EzETH+0*HmOjX|oAWTf=i18}PTI)XnO{Yf6 zdWaSf!RwV9r@WRC=_8GBjBIXi63*o&`^Z!q7F810VxnBLEYp znzy*xhYZ{Xpa@yY>_u8(`jUS-bK6JqbWV(b^al{kK&Rg62>G$JUw6(R`4iBC5_=PN{?c?r7MoEkEGb~LOGE%)61quJY*We_dDdgihQSUu-j5!7 zLn7~bU@6=a+f29B{-<8YZnHSK>0Oz29h*oAv7a^ zdef>$^)KqQO?X}K*8!>pJ2q#r+lXKoG5M4v8UI|RI!mcHu{r*wzxC_OIRx8UaUCV_=c{8j6nHidf;h8`0AHRGXZPms?*oy4T8xhoUdSiE4ROx!w*s>&_z>6q!v0}Qw zcy)Lh9#qi4{@-g?Lo*ppNxdm34m3r1g2H5~8B6pZFzdDjDa-0HwNyG>FGO6nv+VnD zVA5^21n%A^RP7q9K@{i1!^6&hP(&x{Ob+{ksG0AQ^o^qe6XHcQ4_6#C3)5=Cjnr9% zd2$)?QlX;vJZ09&EYP{AnT&K-g(?kQ` zK3sU}^=*YiAHQ97xW-)-em@;h&?!Q?r4SD*?x!g1-7SRsm$PVb5`Ow7Z*}!OhqWXY z{y$E{&Fulao)da@kG~v#dh_B>7nZeSj4+8|NI*p_*@Go7^foDV`}aT7;LTkPt62U1 zeS7Ofos-);EdYKZ;_=7R!X}hOP$>M7wc*i+(9;$fa3kSU5h%FeC4@tY^2AVn2__EZ!&;%PcQAU!^+jJ%xLS8~q~ zr&o3a9*EgA*W3HElR4I@u25{~q%0YE{`vD)6bRIZ@zW9Z0Sf5-jE-0;7t06A-rjVv!3x@wcFF~!LEWKmyBgPN&(nwVc{2^GkJ7r zysZuj$I=Jaf7FCDBNPXlI>NNg`sMf;ZoYT#)vaIBUm)al-a`uBLPO1BoY>=kGVJDM zSdUqb%LZ&PQ;^SwaSN_7RYQILlJMAy?w$9nn4?jLKTR?YUG%@qVw-T;t*>C0i-|{) zWW5Hu#c1^u>{IPl#@aB1?depcgWEo@x-KQc70 z_+PbRtEJn);1t)?(IaCmf$fKKInJJOOo?55KmBRgz&nnqQGnuNHT#5txEETPLl&q) zSUTO=FY-7c$YR;8GB7z9|Bgq_D%ZJy#^iaAD<1-A-!k6Z9GKrohtO^6w97avQ+TZ5 z|JJL1I1bd^gz-oC;O?JKAG|qjJ) z@U021FAgX%Z8z=Ni^aO6^dz`zVP*(H7#QK$4QMia1U$Ct@T@I8nDu9eM`N-nfw{yU zH(&Yd6AK1@y3nHVGV+f{iJ$Vj;UK^_IG3^IJ_^$I8E+1;r69(Ag~Z78!!57?7()7d zxy#G>VBdwN=i0aYg>D)s38Hc*>~kl4+ulTdDetz_D9AziPoZR7zWIRUkh%gYSjv=T zhIif%(e>PZ;3W8JIRa1spj%ZL4;WhVS!x-njK66~0h+%H$MGQ%!@=BM)*~B17{Mqx z%Y7KY7WW7|-mcRMOneUW4CRn3Ug&D3kN);0OQo+T_%pHXxi<^Y;f7;Gc2Ah}sqBM- zB@*zEG94<{Wx#A9qo~cEik+a-VR?)Kz24r|mQK&Rgdr zxp_=3u+$N~!S=~VolVZ|od>c;8fn_T3+q)_!XR91c~cICP-*t_FcP#iBSNqX|11Je zG{DnYFVpWZid?Jm!Z_5EfY8WI116$SXT2Hh*p|NeSm8GN%}LXvCVL=-A@vxvw{&5Y zV)`ffK~n=x#}!KgC&`z7a?>EO)4F+vl59o&mIB44q{)=>Z$h`F3aFXIFQMC1H+c)? zY)t6-c&15&VXAp%0Ft5SXP$pykgUZ}|NmpOV-un5%-H>maCA1I;t9Rhf@T-djL`yl zCkfMam|oq99j24Wp7i@GPlKsLe)?hB`IxQnYm&pqrU>8g2O)?oo^9QS9y>{NCz6AV1ZUas_m&kwBB9p*{*_cM%hzPKM zEEK}!ZJXVhIlOJ_2%qF!+~!89i|0GBO@dIME{a%`38p{eOcLLq}o>m}bEzHk^8f zF%I{&@7gVRN98xfGzGVP7W(q}v-J11LxzAvFh-)nR9UQZstrfVw>91z8VAm9_9fw) zsi5FJciDF_q;0b3_@%Ay0UBo#TpkV%zuXEae7*k+JxXy|i>s7f;^k`chTc6+BSG!lF(W2u9mh;B zI-YEp+sBjs%KVdP@iM2jt;+o%2}}Gay=7wQnyFB97^f8b2S>I!Y^MnGY7V}IDQ;L* zoXBH3<+8{YAZiGE`9_P2z9_QPqvVICxt+JG@m^C&X~Ux2MWoQ?pof+ z@j>kjJ}b#*Y8@PKL&>^`lbk*|xM#N#TN$l)T5^DLFp~+w=kLYSS$Cd(R{<5tBI)dl z+dIu+y*xK5@g-b*r9k)gnNa3AI-aUUgda^lT-0I@y6I?S@J>a|_yJ~yP8fJ5vkL&jN zmr}U{PkTLjbPUAH!GXd$6K42n^tkJ1?Ibp4!YZRdzSd}DOPh|T@9ad((&4hBLDvIg zNp0J?eR0Y!GO;zggz#1yIKNSqhik#iwC51RVnWF&GFClmlS{7%6K+Nw`#I@o5|^qA zzJ1YftlMj{07J?@R(+G3`cw*AG0H`4POnJVp!v}*wmRdjB&cj?64I5u+s-We5&>+# za{*#ClWE&t5076pn;YvAp)Ypjt!56l6-#5A_B*w~sjFf5$WWU+?MJ8jXzk#)jJ|DN z8%VBC&7kQW4&k!ln-D|xmCKE5|P|jF?cAPkF1KXNvnsKazR0j;${T)RH`op8UVDox?H}Ou$Y^x$tK}>EHy6R;6zlE zmlALG%pkPOfKGQ}H?AMXk!G52qHIANoHf8#1!sQaW)(Ws0{4K^(Nt;R@m z8lPeNQYLlI#Hn^@TEiIM82&I0R;&B70;NBg48Xq`O%A2BNlj=D@6*b9^nsMKzp5LR zYAhdvK*WnT8;>zpQt{^Az7s9edn_9on#8&nPKEC$r zPblEDouA*1kF*h9J>NZkas2hW4p=2;{{8&#O?^#%Jv|)@$-m?2ym-FgjePy{@LcQM z*TZesCkNTW)FX+`$;_n_)O;3l`{sDLpwQ{t-e{pad33R*+gAy7CDMdy0C7_%sYKSU z3AWNhoePaVe1~n7q(RraeZ9UxuU?;N4p(2f_}%TR=r^l^sPyFf2Yk@}Qzmj{);4p|)@5o`<6s78b%cva z8&ug~e7q;aa)@#J`Z$e)lid-8coW5Ny|hGwglou#MU-i^2$WQP4BpJ}hh_36ecS1( zav3wJ(UM=$S{kpW&REeOh`1cvw2lL?_?7f-H*8J^c$_?}Vz?acQ2g~*!TW)rx&_+V zulq-Td7$1oZb+`d&fUl7%L9ts|F%Y|Lm;eKeLsBeyvo(}0oTR5!*LY9`*Ql@sH8cK zoQ_X^-0a+aeY!q>qHj2(@(jjO^-Py;m=0Xjka~G5b6kF7GUd^{7cH z^iJLRzSFN9{AZHeb}xKj-*1 z-S@Cnt{`iQF~tI11UTxKHpoOeuLR1!g8 zT;ASjBZk{dF-`dHUIR)ctb}5H<$&55){cAZ(sFEY)K-rd)ar93tv4g7)8f~o`%g=j z3q$n~7Ys*?#YUbxyWfu5nlUPIkvXV?kJrp<&lihUZcVK~i%(})MtS(I%f%GFRM-;W_xL()v_3U!#c&P1FgQ z+82^Y0gCzTrF3eaKE=M!9)-i@FVFW0AlV-yoOvHry7sns;>({GZdH@{NhnYH99_gN z?tkPl;#!q_~U^0ZJD$U(zaB+$XrEzhqW^=)yT_~d<+Z~uJW`RB{dKbEpA zYA!Pj%ply|{Xcy>)3=wC%I_w1PvVV*>~(hY{#WZdWby}@>8-&r=JEY>Lb1&=C#);k zYA&+;Owzs;DOr>rWb%LO6YUkGMh|;s26lf#BC$R%py9^t2}NZ8EF4E!QvEK^IBHZBZm^Zo7kD>V<%0(bf8rGyWOjO$@=?AmqXa!Ezi8ng#oE4p*ngpLv5i za&-KBcW0+k#L}TMu%?V)h5hdycN&MkHk{eqD<;+Uv&%DGX2&^$b%dq4M@f!_J*zoD z21Kv2zBy^d*xmv@INhJJcP}-3v`V1nu+b~h(Z^fxY zV42%<4up3^$IzW;EL@H%JYal+0{Ae48IQ&?@{EK|fgo#bv?d2Glrgm8= z2JH}FK=0&H`tvsf>5FbK^^t~?#J-q5oZ$;UT%|xFgJ}!{A!dfN-n^n&8919+mQqIQ zT+d-6yi^;dUc-BWkDY#b>S1xwGg}l#h>z6M>VwZRPHf2vl}QIfEhn4XW2(W~bM-}( zm~S7YZ8%zZeg5gQL}F`9>#rJmef3axln2w)fRBSY22=AlJc{zO`|GAKSYSvk#tnUE zb|q`vA+9AI+ze%T8%(lCAd?z4(!iwV7`6Adj0)u@rsfpO{_YU7^&kGj2uxO+r$$v$ zvr@6VGLJcre7%qXFJW%&GH~)(28h%jf|Ke5OKEQ(!1t~m-C2h<6!+x?EMFV5iXph zL*xl+&n;{ibDAW|rN8bR+#sMr$xP2WZAh(d@MI>`U$HC{#$Jxp2hN~Y^%!20j!{Co zHa3f2Ef|+4(`tW}xS5G%R;K}Udg!F54n8EZ>@5`pMeib2)5N;sr7&!fAg=%A#1L$EUwLKiQn|e)^ElR`=TV zxuXH?!XJn7?fV)_IXL1%_|A-7YZwc^J7O0rz3!gZY4JTL0Y08=to}KPxqflFk8?ao z<3C}cb3x<9-l-Ch&4O=k^gSX8F(@ICtee%#tM$Iq@k)9ZS#a|5B zkGmh&XHJw^oo$|8&tDuZceyw$H#}ECvcGKPYo#kDg&nZLiXLhgB zf-ZkDP5vMF{rFDYQxv4e5H-vrMGCdnT~Hi4XH}U+GcbOEP~vyZv?~*A-%muKG4DgDY*Y z6A#@#7XdW}JZ=QroXCS_rY0njiRVmkCWvc%G(L;zpC8uuO`3D@)k!}kA{#|FG(m54 z*rCEX1ClJGoVSK~;6i@1(8TL##%|Q*Y{V+kDE(q;FtTQbiY;hTpvf8a6(mcXF}6E5 z#tIX#lM-0ZlD#U8rMn@VB>S&Jb_8F0X;H&NZlA4w{rpEe>_(abL4D2H_0A|Vt#8Jy zQ~}o;cvvY8V_)(#Ju^pHmWj(N90u?MPv$U0sU02v{bVZ`Dc#A3HLF_v zP!(ytyd;@sQ>ZY%L((Ou>lkKu*pib*I+bl8lQBP)FWSpY~oZS z+Lo0w@{Zq(ybREg&B@Ilm}yqDUmMJCZC1MT+>2pJ$Vegt%l7yvA28;Y>Db0DG8S5S zCR97q=2wx#rPmR2;?{9vfxu!pRch1jT&zJIK221e# z1Pk{yV2&r?da>cr&&Y%M_8dM4c76YGr|f7QW1#zFO`E$3T@|32f~ucLcyq2yH2& zLQmU73JF+IGz{AN^BW7sTv*8*i@22=2ZYlJtH-BCMNHHQawW43n_=C2IEDHgJAxT7 zBxX38ZdU55c1+{q_?db%R-!~!Z%hg7SvO!1!f+$Bp^?Ad4<9}*lw$;HEAw6cB3Vl> zTVIYJD=Z-h643&vRGZ-WiI@5ZpHfb9OkU9zJ0v!i|ftZ6VJ)* z;}6o!rGM^NkVQLT!H;C&>fg;T3d9eh=iT+~1sVD7=Kf#q3ja&ZJv#V!ZqdS%x13Td z$*(2NemeXj3xD^O>+5|dVa(@aB zy%xTMePkrL$8<_~p{xT6lgi$kRJx)sa%OD`KQDWv3|lgQ<;xrFtCvjeN5&;a3@>6J zA(WVPWU3;a;Jdp&|4u*_A6btfAl=4iBE4MtXTHd2;qx>cB2(B^Mi2hCxi%5g8>@@Y z&<1v@)CvzpJLp#9S$rjLA}9YsKVXPd7Ve|FE2=sC(r??8NXpsN*DgEq+6nfO>#n{Z z`C2mMfzRS}E+VLU)8Wg1@A}jypIxac@qN`iN>G;Joa6LSnm+N-{r%1S znCUd?-j|QL9f+0&+xt2LnLg3LmfSo01f3y;Yi4qHUkorEY4KeCswTV>+5A4FiNK1E z?i?5H*vErYzzDFhSXqNiJLL{<`mq35+VerAC5sR+re z>Gd$hVBSk+R+b38EGg*S<%v&sGW(en?IN5+*q)Y-XdV=?gX)U(1 z5X&JPaA}s34nJlFIGNpKhE;>PKEE#)ip{$m$jN6tIJ-A`;FguLrY7k>GMh{dlR>CY zXJbX-ms7;X45q5Z!()9;o}$j0_&j>47;b5rvOJZRPzT|vt@#Qkhp>!4T(fZ}3y@IS z9j$_;*fN%8!OPyyRGne*;Ck#^TJ=L~E{ZD;*5nO)_Uifkfph{%r%% z)kDZG41Y?e{A-yywWSOx0%*KvCZBn&q4&DBWt2~!s0C*vWIMK{&sBWy)5y7M^Y-d; z(x82o@<&xw|Gbx3z}wIg7Jm>KdN(Q>FPe#GB74gX3451t(a7qmE_c#4^J^Q@gf+d1 zm(HqC)o7xc7dn54#_@BMw)>w>?@k6+mxLfWSmR7`P?082clUoL##H?J-NR?+sIVx1KZ;!cdwTJku}JxPevr6yOl%TN z-wr-*MD4W4xvV0>Q81E%KAmD@>nkf}O~SO*`P1p+h8}R4)~o1d{rr>jmnZBe7ct|z zLudbk;aop|XV31Qo;>*AAMLK-!Nu8w)vs8Mf<&v=&Z=@c)#>S-{@(}yLD~3Kp)3?A zG)VYwI2{Z>?Y_pyV=u`e8$3dH?pj)JrOVg{Msp#0$_)5iFeLx7pF159YDvlau=;Uw z>7ul7TX0uhrH940CuYh$Oj*JmLa4?1c*D%QmuhM$Up}z8WPvcyWTeYQ7XaGN84Q`> z$0V6^^)ee_*kj9+!Qp55;-RVDQ5XFbrIZkYwEn&0*;>hggKUZP3tP9#A7AJcvZR>; zOs=f5M(-Z$%Ktq&UX*@1iJ;e@I&x&*`kCp2g)5XP=7$851lwB&v5|1`k zLfzrMk_sln#Gge+fhb&i_T=5KO^&!KgI4#)1FLt-=|ro=L;vpeLJA2MwQ|A~{#Lr0 z-`Cl!{^A-tcLET(&vaB6W?H+0NuaU#yH~2Fip2sdc)KhJl;YYlrTzI?yHE$TO;XSj zw0ZYub9Qk^MBjhx|7|E6?%o`p9bE`@A0BXOB{zo%)iOvCJ^ZNcR?M& zlkq!7+i@36xaI`@}`U)-K5Ua%8Z4LTkE^c;qezOPxzx&O5DtW zqvh%H^EZ=peZ0n6WR-)7ZQtk4XsMM?QVcJDx zQ(+oos#V5nT%*bLJr;@C-fy10LK9=n}YWCDO@v4$%=)mhyN-xg-eEngsC>q4URuAzaDPEtEw{?Vh z_ie%R@x$k*pLpgAgKMLQsYkqmut_m}-*KKczts;}YVi5=Mkd8Mxrz&KZXW9&K_Ty0 z-FDt2?w}AP1VV`&IFQ+G*?j-CQXo3&FyZV~%=Q}O*T0Qz|^D^%}8gy=|nuDHpG1^*z zRXkOZTu@UgSGioK1Cjgghs0afpfus>+y1UbaJ$MSol)!+Q=?O!DXNsxvRtnnZzen4 z{3RU`)DMMKa?>#Y|5kCz=|f}JwDL|mrmtJboP{t6HKAV^_s(%oiCP<^BZeo;q0fXN z9}^XIsZ@n|^%oe7Mg+!R1qogL?!{=VS{oVvta zvVaFdJ}(`tuP#jU%t1#9tM|MOjD~MC_x&&45?o&yAc?Jo2Bk8*86*?_@75{BEJ>E8 z!v2G!xSokuMYDF&i^Pn8hSac@b-i|DIA{;jD_J(?SK3K}!t;*~Sk~_cCU%a8#N+jCIb3uHkA)qw1rz=89ZW3a)sZN; z6FI&lME=z>nS$=XH$B&UIE}D51}vN$(>W$MY|6U0Yj{7xxo=;!-h*>i2yu0Im0z)ob=vw(L-syWGk6 z>>SsHdW0`$BGVyfq>jFQhL)s1uFW|b*%}yzOhaa|*yGGz+TWa` zUB2yNPBq2Q?bsL!ZrLO>CBU2}JI~S8>hV7Q2()vC=%fzFd#&*odw^Lm@>x4S#+IH+ zAIiMU8@_TJaSl}3fjm6yfE_mFC}|4*?bp?oZQnlhZmiOwpq4+xQOed3f>_}{9`>N| zfo>ak{P6pCB3W@zyKrAm)6y5m4W*#J^oq`A`(UFkKn7UR)*Dc{k?x%7yujowK$i(` zbM}b7>^>eh+u=g^Rp!PtWD(mP!}Hg0>?j@Rc~Q%a*(yQ*kkw?n2x0y}`YwWl8;<>Q z%}A2yNpYejKiK#w)eQljo_u!y(9v0Y1hSEN5z7CWbf4OU{MSu8WsokAI?tJA;bYma z5W_tUp)K7@Q`nVY@b2G>o6ACT@1|%Ya7{{w!@1w^%IlZotqpMk`Qm*u-71`N*3t0bM_W&c=pJ<*SV+hrm+7oD_T`MRSW4D> zU;%Aifg=6k+)hZrb|24#Pz&_y31FCu<0tYho&x@$HErEu10XbSM`OLTMdpLm0sUM% z$E8zv({>bh*kpkpo0RP_A98wnNGQLoFu~x?ts#6Edo#v;EiM7rOs=o0R~dzV!PRxz zsv%?#d+yz@ijJrQVi?#bt4kVARO3)J5!2*`0Us{Y@3_sAR)1>uSq|%IBQLstLhinE zTh%||M*B+RDEmUI%OSD_h@J{3tf+!kLbrRGewC0eVF>2LP^)MmJ2rX3GFWr+XEL@Y zTej-Ez1g69dj1?oJ#G_bca3t%Lp$~mr}Mxn4T0RmGv?@RonX0$AM+%N*s~V*q=qj$ z{-Nq$Hj8Br8R#^~xHJ~i!!)%?Ej1!eQE*+$UTy#xzHFjRSRoS-VK-{_eEZD@ibc7I<%Xs!m2rHlj+{{99rKGk3jlk*Z3XUr zNjy=Q1=+Yk=;um{CUMp(RGxr1qC<56r0l~^JRkk0*U5DX$uy2m6)d@eYqZ)$whx{( zi2?6HaNDEG8LZ=^T`WZICL4yk-hAi!?=+FKAWfU3?*06tW0F&@8nzmEhjQC3Pd6nAs2QcMX^iA^MH3N0zlOjU6DFwPn; z{_=uy?q9P?rn^WLgX1MoPw^{${NsggoMJF|xcW24ZqcNh)?S$(BIj&y4 zOaHv?>^yw@c<`xNdog}ze^p<6{BrQ|;p`atem#&Rwz~gzMFIA#4-#tyJ?^{e@otgH zY(xcy{xrgKd4x&bx0iFka4&u&j6AkB`1Qqq*%(d)uoYzL!30 zXICeL2%f)9*89s>;Bw~j<|IBv)S9?94t`CXk*_%01v?_Z#tT{svwTB4wDj^5wp@DP zy|*vRM>PzyUsm~CQaR2?;*poTcT4REU%ibmh_vJhf9D6$cFU7ZC)EQN1@#$^Mq~1G zSTLB(W)j%O7lOWN5Pmxk;4k%cW)%Qq^MK(m2FjMjqfVs!ot2=>1MRHs4K0=F6pvPscRg-tRDD zz)-Cy1BA)oTFu*mRrl3Jt2Ue<4N@^VL$I)$U19*5KnbWYhMXeqJ!Qxfr*Y48Rti24 zh%&jv;Sr1>PAO-L%W6Ti(G<%l`0nq0e2 z>e}r1p5@dUK-6W38DwxG!xD6Xe)0j#gVW3frJ|1Sc|k}YFQ)w8f;yK|OzpM$eaWT-E!u@c>j4ZJF-$1f*1KMk)`FIg zQxXUWFX78;Bjl}y{S%3dMTR8<4^JHDnr2Cca#eJJ88Jg_-tciOES1t++hwHs%Cg!FTV> z=Ov3Y#!?|L-W0W?F$ALgPZsOO!21;yo;GCK($Z|2Lj|s^&n3k20d>M`r#-GGo5_eZ z9bS-TsE5|}g~Ar4nJPx?)&xp6t_8!N#S!C90)$8C{o(NXp$~|JdKRy|>!&Z%11>lZ zNTI3X7G0k~hOBVv*|aU#du?sZJG1TxRCL56kRJ}^jDMOb@fUBtrEJHVJh|v!76hyd zn;-9Q=9g!Ka3(I+@A1k{M;+);x>|RtEAr02uG!hm=#R(*9?JIp+hcd%OEX9Tu`=+s zTiw1X_azjf0X@#0seFsFyFeM-$>5R{Mkw}TE*n@rdAqL}K?|g1cgoQm^@7H`<(K8!T504K=U*DI32$*;KlRg@ z%(#{6H@DRvTrZbz()rxz#ee+v?{McoM?3%Tcze&|DJ`nb+VjmnUOV~A@A2!Zp;iG_ z8THqlf4-K&>TL^eqT4_1$cMMj65;=3n>5GwY`yY6O}QDr__aL_A|(EcF9S+2g&&>e znmkNSAs)y-ZPyTm@K|FsDpAoftRy$qh7*IV!QzQT+2rjd%(ilO=)*1V2`?}=(wWDg z=2X|X`XmuvcJn1bkgutK`Z8Elu&*Q*3hVR`fx6S9FgX7_HjQV>!?z+f)q)agEM7La z4$ZTpc`(ez{R;AF044*S{x_$55Z&eV=l}mmSSMhawjduS(K3im;QWIZYb`+I12pWY zdtW!D82;@#I;M-|25L)#6ZlEi+L%a0CHi+#Vg6lOl?bCoY1V-G>Nv-ei!DMQjGIQ_Fs69Imr_T>HCH^tRZ)Z@_cSLWq`71xm`35#ssRoY=<^Y|fk2Y#3 z=ArrgFU3I2C3JfJ(y5yUM3SW+*s>A8vT!W>C6?-D0e>hu#I}!rpU9#q10WIA4eH0h zWlWK!ac5&))K#29v`O^QNsRv<^~HIvr~#tH%wpzqe3k7+O@ zVLZ_vh>BR^M#UDU5B|bJy<5LPNUVw#x-o7}cb(ls_KxF}K1nt$2dx=mhQf zz)v=}rTM&r*Gn6aJwl0H8}$Z}VS$Fw5k}gbt&U~aHpR1fqvHJ?Njw?Uf6ddk2tMw| zmsS8gL)Tgjl;*g_awe7w(r03Ikf#g1D0f>nF;@SK1Rwp*|Jg-G6` zuj4`mCmB9!1wj4b?EV3n4!XQwKuQ~C`nVHEw#PgB1G#{Z-Lu_0cqugXpyi#_z>dMq zMJxW_@TBM2C=cG3k2Qs+On&;1nYtH-B~pXXmREZ~On2S~=59?A92W)LBkYtIyHVwF zHpf~BwT5l0L)29gMA0@JN>{*_Be8!sd zsqYyPAOf<7^%hVp(=Ct6ZiWZJ+b89ok&8Dx{oUH>f&LN8 zbkA_k>A^;-CW{0D5kil2jW@c7!d$i%V{ObPtm{ps*yT=qs7Msd=b$870}mxMG%sTx z95+f-Vl`Z#fi9x^AB5=ZM~cB7^ne>!Dh;6sF{G%)H$njb=ZU@-{7EB9DWTAvd$2o& zfh$NNg?=onb%FOSfXc7@Ydj2lO!5}h9pSg>&E1?0S%o1nFmEv^-FzqkFlIB%IKXce zgT8b#1AI2cce7mPtwBan5NpBQ*&9`l{O7wkLH#?w^UU#O|qe^SGX!Q)#Ja zaONU317&1$?Dzu5^|*fZIwU$ef!+`A&Nr9muL=A@cKrborJ2pQ=XptSFH23`uWw}&*xXU`!c)~Hmv@w#_yN20KXg@ zI%0@>`TSpReOQ_GuEIUPmy-L(%cqo)m-CzMky7*S;_1)x=UZ-LXRe&0-70m8VaPkv zUQkr-y;WQ2u(e5+xW64nk@DcuN3NUf+>P!)d1%bm4XCa{278{UZ*ySJ(IuGHIL*3|!V?dwq3z(HD`3@!OTb}1+~q>#TF!|pz}OlUr301` zBfO&?{&WdwS{Y=rcE-`-q)N_F5uQ79iT+UJqXgK_3Rx?IT7=7R8O$EnAw_?0Q>EYf zW{^xQkvB)*R<5fUOQ4Ow^ZClLZLFYsAK)~Hh zxTXssJxG8M(vdP7hrKp(bWH21nUprbGFFITp|7|GRipH9>N z)mewsh|@9TV!sK+@Ph(V*_G zr#&q&jZT?TwAIUb4`5P)XMtP{e`cKhxtvH$?}WA4m91F#swQ1}dCf7b=cb{IxpcP4 zTS9uxa{J`Qg*2LFO@ei*XgV2mX*0ikRN@7lUEVj*6dB@fkXsCF>e^to5x{0z06l9d zX3e;=-NTuJj4fOYN+fN12VZ5WmjHI?QHUCpM|FmI(ny|TW)xYwxyIcDM%Pr1MMLuq ztHB%6ZyaD4MDCkCo7(0~1lJC(2RQOq7rF#7JiRt<~DhK}gU^$+}%zWYQ zz1K6~U5Zh!EC#0$@)1X*ybBgmkM zL9Wjux7YCjLL_H@dl130G<&N9ww+2cctkD}x!<@Pt=k5uMKSBAxOKXh3*L0EK{*eQ zt&-NhZMT&Re=6QpCgp~Po9@5sp_#eK-g;rW* zM-?mTDPQA3+q3p@gWV+3d^?{5=`j+3B#Ag&zK2iW)wkZ>O?_#X5HlRNh{2Y))P^r& zI4op`!oWDp1XL-fkhsFEDjEK^P(sJ1^6b9HEr(otHN0d0$wa2hYXC`?-(qu&>}2XB zUAh20mihb~;i}!Hp1hm`XQ{_*Vahazjr`dp3HSt`$mxr0tY)51Y_ zg_d@5nV;T@+rOZ7H+P9Y8b?TV8MQ?1r=Wcc2T*}+HtGMLS2$W z8E<=TT?%PNFoy&*%0WdfO`wt~V37LZy=jI7I6EHCqMr0On1_Rhz5wZhRD^P~%&t3G zEV$e5YuA<`IljW8a);IBPmlls$?fmtCd_cQXDRbepZ(i!i!#Io!S!!U^8yHNQ@2g1 zDVx}W4&S8J66ar*9(g*KE*}#TQlU;we0Fa00Zw74Ksy)`Vcq z&FPvaEj*uTiI?Z6?1-~}0qgVSzv>l!T>dz{ZD0qaaEHp3=iC8*JeT2}!jgj1iPV@t zACV3ofx2`o%X8;TjWYS~9&Sh$t;-?sjo&Up_*LU@0h9rdRyssG2sLM>*vpUr-8!-y%KXhhy z4OlOJ+(~*-+5O{Z&8h_^UH9;@664(s&E#G2V^h)Q3t3YcsZop{vMXMc?wXzh3P2U- zME?6=Bud+T~`FkVd_?zm_Z;ye2Pk#(9xG0KL{iT6-1JQc=hk^KUZQ^kKX$!gv=GOB4*6z6&D+o)g#q*tSK%oW9qk-`9vXzQ@BYO?8+AXr z?dw9NipGydv5YMbukMZ?%Lm&e?gS3#$a0CKZ4iD)Q#*D`nA?zmBF4O4GB(&B3yq6{ zrza*|ne|@bannx|ntYPHu-)T=*AE>|(?B%J30maZjIekLnFhIiilrqA-f2cBz}8re z80rMJT&P(ohIbHgiT9Y)FtvDurwpG^CSE2ju^~)mL-W>_L|M&a% zZgBn|zXfmVwt1$G)={`3EwPc z3p2<+}AcryN0=me+>P+WV zBWULBW=OH})LUVe94`%V@8L=Y^-1HHrPp!8Jei(C?OP2i^2B+2g^bGZ@EX;G1qO zX}Oj<-uHgD6>b#^ZpZ$aEslKK#wd(yJ7c+2H;yR0J8rcMsuQKfQ3lCJyWmi=V9-Cy zg*m4+w+wAcU_^RE^FI5!6MZ#KUym*_UTSpp``=>NxahvCs%EZ%F5`o_2PSyU0XK7Z zAiN%5eC~KOH3;_VEV@7uJVvbydwAsP zlZx|q!DyjA9KIx*5p&5UUzWTm6rU1g+WJQ=v~)jtaP?~8*F?hnP84iW~q}UnF@P%eGLY%AdIUQY8QhQy+q6^47>8H6c&ivfB}UtxNC;MLUFm zUo#BMdeCZkwr&qTO}0;C3#4U2AOjye%n#6|HY1DH#7+UMf;V?k>d(-th8)|p*hM+C z4k1;r6+R7E_dg#~N<@$>1gtE2s=q1?IFH_G>u!QUhmJGdVc1c6`=H|#wiU4WS4|t1 z=8C+$4Pm1mJMpTCp9|wT^XrQVTS8deoSss&>*6?;v46(~Ve;@f9L)7I2VHby5x7s@ zyO}Mdwh*u^z&2dbGwLOuuF$Oz`NEje_ls(OB;i#DtfdTM_iLGb6K1VE0?08vbD4Om7f<Pb*%5iJNJ8J9t?%NDoY>Hk6{6-uCNtq% zMoe}OD~Y1DI-l}VAIe3tc$jf9eLT*v`w2Oo)klB=HIQ55K{zm8rGnm6=N2zI^w+XN659@a8Ac7i ztL7{Z@542^)qr_8Aa_GHwl$PV0Iet-!%aI2&!z!HMXdTT%?-D2-JC!PbjyP+Jmr_N zsi)pT9@vnqlPM?B<+k;y&a=6EHAOQ!uyEerk0Slx1~`NpUm*obXiSATp@v0Ud4_7H z-CN(#2xr)S7`}YF8fvW6eOE{L<2?2&yU_+Eb(#^t-m>HfkpR=58W-H zbj&Y}+?1tvyv|U*ZWyrT;8-wmw^|aGCd0w*tf2h8yfklttMAUh2$g^=`R#^PIIeq*}=?dBb=;D&ccZ6bcD7he{j|k@k{%* zC9$gyIH?ow$;&t%Q>f<~6TDAf6(&is2TS(oLv7Y7lyRS8R!3Sg!7!@0z zPM0=_^CI(v@k`|szezPY2fH}p!qOfu+*}BY!OD7)w8YVGJ+wuKH%A|i{XckajtG&D z+VeTRn^e8@JZ%6B*?w$1bL{GV_i6(GCbJ2IfPvBx%zUi6Q@ zF$qOg>6N8us%-~!6&ZSVujLC_d9gnJ^^4@Yy1(G$!1L0_ z^$~j6a60Hw`|IK9@>K1dmO)RaI3f4W`}ZI)CVY`rXM)F7ga+X-FzD{_vVQ?924$$mUsk`nTqZJ?d~mAgBXnuq#LK)p3kGIt z6EWeiX<2nv+)wx|sbP`RE5I@?VdiLysVlSs6<*_&#A~{20>@)-BMNt3#3u2w*zEKn znRtXRjN%-4jR9=kp9kjK`{shetlN=xha(FhvT3@W>Q3RSL`XCW@UiTWq~-bb+(mG! zA*-vRz2&?amTn)DA@fUdb4=_4(@<1Q>8nwH@q#{!Q|V;6A9n9;II)GCO`ahYH8|UM zDeZqO$6|<`p2(r8(>G%p@k4V{iTVc<-D_uHhgC0rfP0!XsQ&!e3Kzo12} zFIYR{&cFJS94aJc=tjW_qQ?X^00QicjxGLSo!s*P1PHm6hKB-G@v8BvFCcXq{!;Zk z0O(iHv<0BpD%;Dt7M)vDt9^?Cq?4Wf;~DvKVyW-OX)Sk1cj0YYSqVgV$QXW#&YgYX zzgmFsy{R*EYP#d$x@oq4=`5xnjf-X-)NJkL z;*&(0)&18e-usK4zc9MXxvh$cpV!`h67=k58{2~%FkwXK-OIiA<*5h^yR2I(k9JPK zzg#PW-ubEhBeU2iVHB3>(hP0aFzlVxak|an_=T#v@a^7-0sksxt!Uu$&VT55KX(p( z?(BP_?=h$Kaiy}K-&N`U<^690=;*hPe!l`TP{OGn)Wwz%I-bJKc+d094-MbsjX7pwU2{w%Xh{BHb zdAe)`oYq7)(|J!_uTSqU9xh+7@|R~RmAk(?j=cpgC$o52oPG0Gp6va8Gqd6=zfqRg z=V!ouqz&nNas$mL@=(=`sNG%gdwX+&06nv!_$7c=yh|xj*OqlaO&ZFOSt&3qJGu z+P_-&Ur!hJ4gc*rrM+Kz1x_l&ZtXtz9pL5BNBwvO)=R^?xT(3x&Bgi22T1Yj{P`0D z=#7AmmWFT0iQmPI9(mE+cS^Z?bNTr6;-4#5sD9n~Iax^K1>L2-qQXm&*r%tnCx$g; z^3Bcq_x-O+6W{$j{`^}vvRhZC`dqH@Z?QPeh7Wo+?XuB(!4KYjZIU2H7QCHwd_TA3 zCOY}HYNy>VHb&}yK7w{XVTvtfVN5DeIJ^2%Gc!Byt1t;wHjV#-Q>ZlZ8@T;Mb46wj z9PYFKL$rPztm8+|&+p&o3-$Yfbh|hAA8xd&{&|1-_rs_AtDmBMSDH7`48J_jWwvMQ zji~qYxoFn8en5g42XiaH1uxh^mm$%%ii;)Ak=uwBS{;^p~k-28tH0pRqws+d*IT5L5!8R=H`Wupgxe_=BCOwGh5xDqV9kZb>O zliv4Qd2QZl&0G|eANI)HA9OYrPCgNPT5Y5p&*IFGqpg;B<|sLL@3$YiDUVu9G}()X zS_V11pDpRdxH?N?@^3TQtzrdLrLnV3>G*3;#ran(XJ37t%+zo0IZI&)yvxh`^`=`U zz?aU#aVH7^sO{{4Yw!=tR$GdPzj~>PdT8x>#6?ygoyv2htDT#yfWoW0}2W@8Y~N z&m}n9L(p?RAC96UXV@|gY3AIYC+UteA7r7| z{oCU+Uo%>K)|IUscCUN;tHu8jAO9Uh4`*j7E}xbayr}4U`*|Y6Ei!j#mlv4k(4Ux| z0VIJx{l1{gtay1VK@LigsID9Hi(1FSMMpN=?|;WcyiCy}g>@N4U-3zgSkoo&Wjzjq?SS1@f1d1(2$|V^GpC@*gmu<~%j#NWS$4!trAlQj?&`&?qAm_n!d6 z&u8b|4;Zit@!tG?{*_FxyD8D(@rSRna16n?Qx;}o^TJV^Ku>17kGjq}h6WMul zvOa#M^-*%}ZjN8It=)%@RI>xh=$pSu9itvE>%jlzlvckwHK|ycKFxKgTBxn#=E3@sqHFnnwpnBIaE2F{K+=-bK#@F`h8z6& zEE2oN=UMN-j8K)2`Fa*L@wAOe&dV5Gm2B&a)%40)=`Ko+*7_5&?&l;)vJKB2&uNtv zc0acQvuW!mz&kcm@@It6aso-~v5#=~xD(qur)cZLs(r*&fUEZiH{q8iA+CvuJb2sY zML;g=;}X!<-=32O_si9(11^j6PP-aWD5LDb9Qi>2bW9Y!5^SC%+(x>!R%g^_=;f9e zM+g~}kvwgZTGHGc=6)+PxpW}UVFFG|3^t$o4WKkZU5h3<&BCI8GrJ4IbVsb#c1*(= zWS>^cw31}#-$!R0d}{O3Sp$SQhu=W6e>mH}IF*F zm%uEPrav{|QU$uWTtF@XK5121?91B4kZe%&N1KoOGo3I?bdk_)*=^H1lDtg z+gs6zAtbd`t!y3ZgIBvZ@joW6hF(07NAuHq(Tsh9hzVhh_vXh4Unqd3xYRts2dm-1zuI`v5IwYtz~LE>UmqlYHDnoZ%zN| zEy28k*P}F89wF@9*08VhLY=l#!=J2}PTK7eLl{DJ5-jsNKi7Eib+lCyIX0&7+VXi7q^1ZTe8G7CMuk z5J?6c6*3NJp@icATL}DRM(2`rT(bLyjo87u9OfKzE4mCLnMZCnaO>k);O7lKEA6yCV;xeo=M;>q*&CN3 zXmM@u}+p&EC%YxnGAaqhB zT>+Y{WSGRTXfji`gnAgsd{lZggt_N;L_Qz^5oX}`=a~6ei z$+`e``a%^6t<-q$(P402g{R@$ulh}5PT*LrW+}fdK4jh0D zbA>l|E%~MU|AKhgzC)B~3KxJI77FnueT>w%G)K{m_1iZ+2NFK%x?6hERp@s6&3XeZ zUOCNMR#<&2m)hZr5|9e?tS*&MLSn1-t=twU7L7=NGPp>h8nuR~Jt4^6vZb zIj(nic5%7>wR3;|^TYG=2~Vm-gENuL&=b>qHM8?U00ZVRss;`W)uwf zPX2y5K~J?*5Df)QkP}MqGjUuPb6RaKDd3B@0&8QYO;13lFhS>YyvU$j&DT4B;l#v; zx91Wr>pQk`pCp(HoQ|$6Qc0NdGCI#~(YHfRNi$P+_O4h=IincIS@DsVhpg4A5u84y zpDTTs@8Pi8ocxt)qK38RQx2U|9uM?C%dr4i$-{V-TnnpXib!@wjMs9bMs(HB$mmJb zn((klqytMmA#5Oj*uJhywvn`Uet~X2QSOSs>|yn7Iw%xJo+z-#s^pjL;YM7~xn$9a zgqmSxLBs++9aB>Z-Abrt4IKJoSZ=E7>g5bR?jnTs`MY-I=A zYM51+c+^GA7~K5TrROO_VvLg!YOMBrvL3>5v!^PW=p5%Ai=@f^baB9nkR{9=vo@g_hiBJ@PW*}L?`h|!ZOx1jU>FyMOyg+ zY97BYE(Z=C%^bnQ7P#=cgo~xEm@m-O55iFDWE?Y^gC*Nqs9RNRut=-fF#5dgJl}D_ z4OMrmdh!3(*e2?18rvM^4Ku6^7TQm!*lpb^RLtTxbz9b^dYwh6*48`|Ow*^(5p@Lx z>qobI4)U48e%(wi{Jm+4&bcs^03%i{NTd^0BX3H-_H8{4x|2p!U~OfoTY8?+>% z5I6}Bb5*V3<#@GnGRDm) zYjqCaOWd$=sX*95&P6KArH8OtGHB^l4~b$eQKriZ(hXIKIOE%rejMf{6Ly=UI{>Si zc;A5msc+-d2T79|U#Ku6H@YwiDpke2=TMEwkY#h|DK=DOuv+KvICFK1xH0ycV61)B zo$?+oKa#g?jhF?_wexJ?Wu^jH11G$Zv#We;;lyb*+wH{P=g{If_SSteN! zA!n;uE{$3JC_QZV+v)w>w|gds5fwo?v_Zt(JWfx7$eF?@b>X|_8sV~Ptm!VnH~ieu zDUkxZ>2`SgFl-%#W@x=SB#Q4ukxo?YHmuFZ@Pd@r7JQ`2b@6~cu+8q=-GY!nNmRw%EIaYotulBFgz&Pyr5^^b z@2EHt!*e8;U-n{W=^V3_wyfCehj4$-t?OxtgNd&OqrWey3r54Gl)8mS=46FP@s&>6 z=Vk=4Y}78L_I#jL?Fq!BX6h}x@o^hjbrFDcQ7irp{2Ig#oY8xA7HuqoTMGY>dYKWc znt@w9OIN{0!P ztX>|TX>Yn|n(pPHeiGR+c_F%*H?ly{`SEc^%EHxJ2MgQu!$+~emHVl+)6P#!5^P>h z@6RmZYj<5sNXR78o8)ng{0T`SGiFh0rD&pKK}G{=L$0uHc3A0+ZVO2^1f|)e@;g1bVr2T z<()_-gX9OOs~l3J)BWtPG}7{=+f;7Nq(UeY$CDHC7EqEBdF^WJbEs{C39=~VhJ&pg zEiY|5%e@gTxi^3eDE-W5orXFYGJ>v!*6P=bAP{fh_6sxo3i!rF`Q}1kTtf<4+{YaiWDo2Fn(Ae7$+P<|BN2WVeZAB} z1>>LVVyVs=svMrFIKxg(Ul736DS0P}Y}HNeY?-lFrfbX6k!|0j_ZRXBeFfRiPzaW` zckXF!DP!U5fv@)NFK*-Ldyf|{WZ~XZ&%fy`wMHk7!kDhk{|*$P#BEBDtaw@%mip73dsgg#{%y;3){Uk4iCO?R=(Q3`gDvS zx(WmG*9mFG+^5_TMrnHJ8VgtugH$*5*ZZ&EPbCFOfwpJljdb&YbNNNyyAS6N-?8`o z+Y(FL_w6b4xcqu1{IYrhU8wPR+D*Yi!@T=150K^lnspPuEFLo!oMQDT({Xu!2B??C z6^qv_ol{QFuF(4!F9^19&h?pe$OP#pXfO(mo&iZXk~;uTaoZ>VQj+3CDE(i7Kf{Yq zG3Z|XfK{6d_Toq0c$a#R$_YkGeHbmn_5*LPzQQK{=yv61F0F%?a8TAjtUf;h4wf}U z;SbCJyj_{Eb+Y!H2hvE$_Pwt)5nk+ztsTd)BolY?Of6&-L9=yEST8_RTlAI6Pc|FKV|rn}9;;mpHgC6hT4CUlF}rtq!y=go-Qucf%C2ol2HT+MNR=(4b?r~L;|Y-z z#@Su#j#0dX1ISw^9d&{+bTI@2OB(MJ^SSr36?i&!@8c{fYd?W6d?2)=#dl%o6U!VAF7|UVG%yR?=IKRGwUuaoG0|jU<=36`-FXgh#{YsRy zQPHSp+fo6Ih9>7>u#e4o%FTz_w$@I>Yg0{zB+?rkN8i}R($NGv<%IMg#BvB!pAtrp z31zcB_pH|Bl=ipKhtQ!JP``ewWm%mi%*V#1RM48>{5NNHpnClN`p&PPa3z4AnG13e zSE(HiX2D|;bxspJ$C_e*)q=T+TBM2u$2|ZdO`l3UqK_^V;W~g;Q0L%TwGHSgZV^~$ zIpVlHO?G8DUC+$@m~s-RM|VQ;&To!b+kv#BT290fJmCB!uNFELP>RsMoQu|6|IP#6 zJqu}|`c!lQ``*>pC;J)(wfEcEKYfL1FBj)v^>BMydr~PtIL85JI;6+J4$>iaFCXeq zwyWI$?5gX)Md?a=J$c&Alpc)=rbcy{nh5!UK=GXHL5(~29?wBLtCUwu^#^(tZ#ZRYmm)5R=u zby>l%&TA^FsSEdCpa1@K5^Nr}yn6*8?%O45_t|G7x$V69_Yxqh+lvYrue!W`_3(Ul z-ncj4P8Xee7fV8r7iV+@dp$qZ{jFs_WaYK{7rNZe{rBI`cOTw~l{-24GA<6|*x6MP zN-7pumT@*+@1x8$q{5S;s@m{e`FA+eTu5;c35H8q_p2I*Ke3(JCJ2mfZS-Y_a_)R?i@uu~zUcp3EA zhMNCmjX=gfd|pmX?49~~)F^&7>doQesyN=^%_~0SThBn@PdlTjj<54z`jh%}8OChP zrrx}|J|;Fk*5e=Qz>M%UDkaYBsPoO}X-1Y5BOk7qBtr<*#-KhyTHrxHft}p3P397j zW9altt9BD{sV6ZbDruvPPS8P8Jn7X-GC+w+QxKYxhXD6%cotV#J020|I4c@Wc=N1q z7464S)a+mv({TszaSY4wzoVUl0na`?OS*dreDND^6VRzyF=0qAc`MYlcUC^#QqdbP z7&Z_B^vj`%W(9g)$kOT0ECERxpMaSKCWyI!)7Df9ghx3Fw%&%xug`hsQoNiutp(HC zjxe;8b3^dxsin<-v&dfm-s-A67g#PDjU0(I{+T0bUP&#?DVhJE4Oy~{cXfzVa4w0# z;lw`+au9j$+uH9Y^g%v-NqpgdgG#^!Xu#$4A=Gm5VJ$7C_LYSmDUBio?XS zXDs)8us^qejp4c=N5U+t_Dd>4qImf_lTdM1rdkopapB|vrMxzu1=YtI%fE1*WN(U7 zCBtvMXidENuIrG||N7?TOUFy8U&I1T96d$TF|Uv3_no_}>yy2Q#@g(%&)^PgE!WgO zcZ>Vu+WzsCiGgVkL<}k@+wF!dS_(rw!sz_xx|cj8;#+g80xj#Gee z;8FgfJ%Ea7{xry+<8yR1&>kt6xsOP!_dFADJXQE?uApU?hYsg6i{TJ{za~!T_ZElL zf?+WztbHM;?b&vBcqD-g_7gP|4dGIVWUE4x5P%pPe`|5z{Ztf3)%8{nT6&FERDh{y z3vMZypl-AG+omd`8hGqI`(cb@2o3(Fc7$0WLUTv0UQ57ES2(nt20fllA{=fagl5r3 z*(~J3?ZR6ZsXlN`<-wEw!**J<>gnS+3{r*~enY8;e)*e=l`IvdqK3{vOe>g z!rnO+AO_kT{T1X7k(~`9;rGWPDs1vaqs?W>W6okQbkMnfBk$>o42DLv7#CRrsoB9O zuMdr9?|ZdK-mo67BogvP#MRN}eFk`w+$3x%4tH8XCfp|!%+0FM zePx3MjywPtPA07r-1aXOgt?lky0^JRApD0(!!MW8Ah)wh1B&VHV_w5j9cE`hVSryL zxo1_h+pp1`Rrv=+EbYa)*i)x^07u`#9ayyYye=x`L+O4NVR=LXGsds+0R17Gl_GBc z+q`XUwhHb{MSY&Tx&N8#tHS;rsUrK3CU>eSIe!-<kW)w0aE-2h~fsJ;c8*FgTVgI>=k zOIMmq2?IVvwl9~b12}3Cuzf{A{`scSzRmmAx#bYcsr5$GW0XI&Qgj9>9Se82>-a8< z7f0XzUmC(+vN~O!f=BKm2=@^YB}X4*_wGJ?Z}!J~mE~{5id7=Lr`UdcaYTUk`S_%G zJ}~V*Jd?k^Yh`ntVf6|5dbOQHG0%j5dW8NeB=Q;E{!$w#TWF7zuNoy&m6=D^i6 zK%0>W$Tzv4{S!sOiaeN3H+(s>%gNuAL^ovMELpPg%6R&zVHOSDJw44 zbdTct6}-$mEn(*QY3Vs+GWl`|*qwd3OigNxDa637I8&`^j(eq;$&u7Nsft$aF<)u_*WFv85a4l4lgj^D9r<`Y{QR5nVMv(+R8%mK`2#k8&H=OJvuEd32VE z721PgPyWgEfP1$MKS!~|y|v8__2dm=(+NQcF(GmE=bBV=Zzg4tMm?{|>E-IvR<1u0UJi0qT zO*&TM#~+XY=mt%z!t#hW*W>Qa#dc zuK$hGN>3bkFExaqzAH^FrE>xYH=(UdQ8tcs<6acq$Bxvu?YbJ0Tky^C=F8Kue&r|c z-=A3Hhq_mlSid>>b$hirw}_>bPa5Iw{ipT$$G=r8eLTAUY~7!CwgrAF{4TDnMJ3?p zhxO-^&+PLrH4^y@G})of?YN_qAM0a3&Tq~xaIpfPLf*{iCHW>TBwf%MZX7V)7a3p0 zkwfPFj6eh*(lv zcG-zDvquI+Q*BX2=0riLn+=Unx~PMBD^l8g=Vm#(pG!RH9wIJ9Ba3-$jT&#g`9k7Q zJ9PS3O>dt>w&fjIIsSxQaQe;a^nd9pjG*ur>sSy6`&xh zS^Z;HPiJMYD$V&M6=(D1VlF+Qo()@~4y4g6X42y)rB@5@y0~{WPI^;Pd#IjR+dDh| zPEh!ep%u6$X|fS~!CVIgmui*f_zQ=u zHka@4DHC)(5?%sDUcP*_1o$G2JI&FPsg+!MLJgA{hyyP?k~LUJlE^Xw^DfAaHR7Lg zFDmaqMtS!XP5z4O^8ml$@`%7N<4EzANkdXC!OS2yI zi4Z~J%lF6YXlU>1%)VQ!7moAWB$|>?U=ATdwBMFVP&^=?7~1SSDArMCHVXihR_1eQ z27T5#mRdzQjRrpFn5DWc3!c?U18^Qd3SrgE@j^L|6lQ_r@xxHE%>ue37KL}H?yKLF zk$mfu1viq>dyhX&+0Uqit1dHKc^GknVL5x5YC#6{F)y$k{>3C_oomZ8?WY%9_;{S@ zn~H2**^qO?CagC26l=1Ab{^J2fJY&rEM@VZa2~`XLh2}w{Qv>HgiahdwO^Lltid-% zLAOE5A;Yhe5bPrd6i6Mdm%w#Ymc-4UYNngT;PpUa+2`{uPJDni2@`8al79N%HmUdT z8TTS>`qlB%Qe#e`hyTT9IDSwR3OHEOP(lvyi+H{DNBLp(w5K%!3(|O@+^z1;mu60J zPAjT;|MXAIdh+XRF*t#kNwUPZyEz2#hw|wtY)cB2BiMFG^VR3*M+97*Gw_lNt!n2 zvg-V=MvUz|mgyY%!F!Ej7O5Fzkm6=WCTP0Dfb4qJ=SJJ(wv}HN()hlVh5fE|))YNx z4F7sRFvDB~&CmV`1Mn0;yL{nxHmWnHJ5jnHsTn1U7IFmO6mrq*9 zKg_c z26K-1Og;$Str!R`XQ~jq>ZY^28uZ(q-SVcj7(iBhHZ%NW2+{D_eS zxizmueW5nG9=3JRttDkf1;e3@C{T$Sgdr)Eik$HjXle%Z zA~e|ep;d_P%ZDG#2!2aa&zWMdl^5z>0K}Iaat;{xpY^22*&8i(8i`k6)FGSfa%mrp zt;ebxY+JoO<$x-E{boApLt-HSfmKE{4TT?AL_0WqkzUTmRt~ z^C#AaavWk=+HvQ}H8S`L0lu>7r5qWXK1iMVq?A{&Zdz9*z^`56rVM%+_}Pu|%Ol7l z3e38}G9zXl$xw?$RuT{e=^$?~`Dbx6dP||Id{d<(r24q+<-;1aesbTTFFev|hHtaZ zpz7b!;IC92JY~7|!*dQ>+Hh>GxMChO#?nAJ6mOMlH^8 zX~DWWPUmN-xlKYX4{7O$CPK^z65Lmt`aZD{ATcF%QQVR!8F5r!&_`v{6(D|0jJ%l5 z3@`cj@&v8UtY|PD+9egHwgkVS>{2hCfBJySOw*1%qaqE96|u_{`i4ak>mZ^VU@X&Z-1Z!1-4NO ztga>7bv2FV`LUd;thOn-mT3!Ak?HDmYGqdEh4`ea@v2y>>`&N|@mG?ps~Y+)0s`eZ zj?I@}9PfltO?e!Z;PPTdCf+Op`!6#t*}SRf=P{G$x$Co(@$?m=Q3cNX_ujgDcKN-! ziOt!&cg3>vx2_(>d7$rS(di2vZjD-u%=mCTxY7)&O_HB#B|1TKuwTOX?90s^0GejR zdxX*PR4zy`%IC0?nWB2f@y^hF=HX66WtXg=h%~b z^Ft%_4NLhuHJfFRy)aHY%?B;^^Py#Hp7nyg^G6WUfY4I>8)__9AQXSwoBP_N413UY z&e<=a{SFVP;WT0u(0iY3hMLGHm(Xc9)x%O${`dtW%5<#RyYXHg&9As9i?r_siRdG97;` z3D#7m!s}?oo@6Bvp;RMGb@(cd(3WFQ=b6fb=L8rVRsjzRon?3gx%^V=6~%r76jkTA=X4OeU=zL>ds{<>=)}D9@8qlx zCmD!X&7DO=C7iMO@<^J{XA5;Z?UdSICxU{D=}C6o;8ZDz(cV>;k}%&9a=sBRjCr9GG8@-+v?l zCgpJiH@$>v1J+X0Pq`Eh1ZqP0Z59<~X0Fu|c#xfpNqc8FRn949I<>I@&xOIo&i0-d zsAn3)frgmUHzji~MdPyJR5Biazq>q#{*Ra#&*bNKb)FVWDaNtO>nM|mL`blJfelPU z7ToURr;q0q=55Ze&u$+6rhPo1s%f}kQ)HA)WQ2LXF{@{>9QBZ=wcK+CtOiI|_J)uv z?gQG%usL*~B-w@8E=r)-XP>K;@p#qev=F~_ty1JE)p2UMsGQ;QG>K27GeWC`VqU}} zo9g>Rhdpv|a#`?}VU8y8NY8K8?a5tn_jLos!=}Efl__)`N*U~$a)B%b@Cn9fD-tyT zyb@-$OYnMMO_MRxTS*9l+`g|{mH#|GrxHpcFqwys95vI#h$2Uf^UTGO+sSg_4?*f^ zU?GHa^2}6m6Oms*6THZuoUbWuC)T_5;O(CIHYrtxX)Ca>hEr7kD1yj)vvvXxqg3Hs z^cYmJp!6tgxYDTg)s!11F;+c6G(?c@79&vOG|BXjXQArU>S(hK65+rCYwvDN^7Sfa zg_xckcI@ha^aBKmFoCAow15S`TV4i^M_3&^JE?{)D$bSRS!H!=clC8)m*dXNuk@la zOjhgfL4?ZCu5n{u+9duipv%a*y4^_Oq{Ao>HCN?Kw-=Qex-ut-JrIdJl-Kv&{b>eR zFqQU~d8zp&bqH&Xxn8D4|GC{HwlWhxts z(*Y!(3(HlQvfLG@+3CE5P^Wb_5QdZ(J3elsy@4}addwtnCK(~D;ZC--wldqbq)Y6w zOppl;#Kbys&qF&0^K|{6lTg?pQq z98erh)P2u5-|mm2_o5^h=dqk=qA=c6`Qa=PYH;W8f^yrS=JQCdZ|EFU8#tc+w(sxO zrQnoHsu^5Yu=CatPrrmIDBJsCl3kr6yy445_hdO&Ld2%Ubq|VHbP=kLm>A}g#cOiL z$4Wel4v@`Gig}_bN@!zqZ;yHCY4HY}OMiS@ikuln#axJNZ?b@qj=|GiZ*uUo?zSNy z>o3N8t2xX+bQpjM<5R;E62H~0?9u6O0eJz7*u7S@hFc$yVb$V`(x(UJ&UNJVhlJ$bHKf%l#3Z{ldKSx_t zQ;(x8CxLBw2B(p9KwU*APRh8a$_Eee>u@mJby1V3o5jdUo>x!#YP6fVKHVd}=St{g za+13B!mj)tW^XaLhte0auJI$uT8~FrdJ-YJCMf4IqTKopf-H+5n?An1&j71hkmlgF zhRd|&ku{KJi!DiB)9Hz7mE9L=&8VX6 z^oGG7$H3;!HQ|U2F007eA4;&04c|UsL;D>r^V}tN2K1`XowwLT3d5obrc+1%>(|R^ z^@&@>;nf>UXX83LJlWYlb_#i#-u1kX`{2Zz41H*g3u z%?U~wLuC?&xD=vMoW8}NOgLPj{{o`tnkekP)Qd~pFYFB|{ioFJ z^cw}7f%>?JQl-HD%9DHVxGZgSVG_Hbi>OL#|`$Xms90F^%gYg~?IdVu|KG`Iw@-egG#_ z0Pc(>$cD6te*%qI@V$>Lc?FyJ3w4-y=_Kbc`bDxwz!_11(gFVOH)NR$E;z`3A|S~M+O zxYv!(E4ss!ZPlAt<&*2$>v4%r(UOP>_J_@LR~q&JY+hCR=L{S#l&LFFtL{_?!`+?s zISx9wUK|jtPNa0c+8_39L^-CzDT8 zJ1W6JokrjX70!gTWl&xGe=&T6Ujc6xM$MM78#&^r=IMcS1-ZW5r_j!zv3>Q94$Sa= zZ5YTH6!5{BYIODGWsCln+gCouEJ_ntaI|^@&@?4I{Q##qfBs$uCxZkTnKa~P!maN= zmoqbK9mCT)=LXRf+q#!f-i(-c&l|T}T3}TZEP+f)9eLIL)7A~d*hQEgWYwMgQn-9+C@{z z=Yg{xT6_(TAjFtubEgLFKby0YD(JRV5pk&X{qM8_zd)by8B57oyyd&%Ra0~7h#xi_ z(x*c7XoyD{gg7-3O96)AaEoDk5m9JO;&{Q5(mJehDF#e~<*RJVD1c68j;yK`);bCxew{8k|rHF(VO6QdFLr8 zSUpl{h}BUBJHqiW*LOn(N3gO$54c%#Z52-JrNb~!7yH1gSfEey{6YxCI!KUF9A`?{ z+9-%;=|tn#TDR^jI*BNekvWH|U~wfn+-zG@F(U7gTJ1wWCQDNlGE1cBCAFl}Y_1X0 z48*Y?7vH$=eKBuu{T4CjLysT8L5EbiMo09yAAW0z=ln8q;d^I36%fl=v)>HTmzsZ&b~{J>zFT@ zY_pLEy8sHb=bR_=plcxpi!!no+F4=<-n`Aa036A_`_s%x95>$bL#Mb>6_)c#%fzGx zF->Np2jzZLa^Wp#f6)(j@6W#jA701bg-4L;n};zQZESO;E_a=2(q%2tl&@2N6( z_VlRim*%&5I>V)PX(tL32V4mk7m#FiRe~}uy3s1NbKB$IN4vRzmZUrf4Nu6a3Ysd( z04D3P;$<^=O%|qTy8KO>D>f&vv@!9ooH(mVk#D~-7^pWajo-{y8!h{0{F`gtf_<_} zwaMkk-y%Dr?7hjD0RBzB3r>l%PoD&kS7+npS&g?U+pZIGnEAij$Rv%=%_3Q!fP+ef zV#<0wA8!LkLV(<;z$EeQq5yeFJ$c{XCOkB|1BwnWfQULBR5w=Ri??7jyM)vWZT`50 zTjrB@Fg}AN8GXfxmx6XN7w}zGCgBvgi2i7~ZhdQM771=VVmQ5vSs3<<)H=jzjz9`j zCAKpx7_65^E{53u3<`MUYEAxJuF(UZo6{Q(aa0=c=GqN_berL%=*K;w@!Du*`n-Wt zP9HlO&y3x}mf?blCu-F7hVu22jSAAIWh=@{(0CwSvODA5+ ziaW5YJ5AZ_DNvmfzmS-H4+{4_daG#8j4@3g6QDSt)4=ruLrwG7_gf&p+AB+HRVyET zI3CaJFD@?3^Evfz87AjosZfOpHdGHIqKLUtbn4(@ZW+w1xoo%I3_W2taF$Bz>HnYybBG-woSZS25qv?Q<&GO3L%5&<7=&?`9tg5~XWoZd?P<(oci z4hNFgC|~h}L%HOv7J5bi9TcXFn20Ls$i=!Z{f0Kyn`{28wEJpGG$I(4$z9z0UAUyL z(m=VB>$JVZQ&zS5ah6N(`d;NAp8c_Wz%w1b({eXxjB-0#mPolbov&{`44Rl@M89)h zEaX)Ejez&d@Lf7k+WTPqvUUB2u3Fb>D_s5vP#4DK3n9k;Uu4KZE)!{FdQJWdapLuL4B+;`6ag3e9lbnt{CsYV9rcs& z$)B*S7Gz9n|7l*6sLRALjr#9qwx3bcJQff<1_`_i?4aoqdV^o%4;=N_EENIY0is|M)Ou7OK{=gR?99@sIlRp`m%;e}2eeWUTgvka zPz`6%+5-Zd=VT51pk3q^7LBKRwImc4SjKzDHlg-H9E@?FT`$>OUuap5iZR~ zEL>vK=6zcM%(s_dn|_grOL)D6e(s!OFef+LtvoI-DWAE%iej6l;W1L#$65DXZPPm_ zUUi@C@}*8OT+i6vob0*9fBTYccDdH(bpLy5(pXY(RiKnw1!XYO^SLJPX2Ak8-7v%E z#HNo#A6O|4XK9Xi2uE-gxb|)zITmZSou1u`!-K2#Am_lq{EwlC<4aeWLtHG9ZD@VC zY!ripg-0avIGu?F&7PhdE96yj>O^%I+TT(U2}`_g@ADXOq)XFAjBwqpcb)io`nwww z3yCg^P3w6jt}TcbOo++3M{c}p(c2vE-7RUM{IrA6OpcNKeLdw^bC-3o4{Moh-i79& zmByz3SPe<9o)ER zphASy2;1`1RH9JIREP786`gL-WxB@zP+bz0(tuFsC}EHPD*>428;liNp)5bjHTJSL_E^PzAmu0OT zY8NWI!?VObqI9k{uzmPJ-~7}ZwXdgoQJE9T>)UIAxtvmjAyg|-Erax~P9#)(5OjN~ z(@k(+Oa!igZALq7@B5?BqBY)k=74K(9(HKytMXx%ougAto%cSR950^KMgLzcEq6Eq zXa@($Ei*?vJRYbWczzu5b%K}{t-~#*gkfW)Z*ES$f1kFyS=N#@Pmt+pd0DH7Rc6OG z=Fm76Tk{akE64g{cwR&ev57G;d*~q#@-l6c%qzSgKUWOZB42s(ew`Y|cW79bcK?~f z#@qjv*cD^(jj+51`;d<-C}HR10D$X4<*Ab+CSI2s#fU^e1gYrxS62#u1iab!)I?W{jG7o$Mz@Pd-8nqQw~xYhyP^6Jf1N0% z(!W--`|a;zxW1Vi=)qxmFM6y*a-Vcd2zp=edxa-NZ9$7X<{fP9aBFT0Y1XZtPhubK z9Qv83osvCj?cp?+z})$VkJ3L3WvX;V#E%5>+9K^QoX@#1ZZ0lthDIM3Ad1nj-_hJ4 zF(J9RVSJZAm!A6#lD{@UO3UcKg_nQqFcAi z`^qIj4sj%fnO1z@A8ZR!~HQ0IA zCx1@X)?-BHS`0PCowG_Dj(}K%lQ$Am%fVH2>_A3jG2V*zUXJSSBs+Oe&9cYa|jK8&v9zLMMWuI z!rKobC`Im7+p#c+0wFfHImv8e;#XT$u9fw0OWvLCA%g_M`g!eYr@qLE|SP!}veUZjUirg?C_`v(aZGqa(0FH$F76R)!>*^hbT+dVXzo9k(N(C1qU1X&D2 z#Dx0YDj)qY4ZiyCJgf5S3Sl~z#UYt-iR)&`$(g`QqBT^+CL7+}6l*VC?xaz^mfiH) z`5C;@7ogl?!U5MoPS0|u(hvIH3bfOK_I`60!lbG%=E&|;+|M~!?_emB=(MgkkEh%S zkp2eW*>YaTBl!8FoL@Vm$9EiFHf&A$9Okt)eI9UAAk!rQrw(|KNqt|oGU%)pnSQW@ zDw$SHeOjBq(-L-c>i@F&4}CSUeTr0(sd;8Iqp*wD2R%hp^z&pnvYo?m2^gVN#Gr%E zmfWjqvns~1y1JHxbYiRdSw=Su4HJhMj{E2?b-;GuN83p^X8$?uED&?`FI_q>Zf(M< z8Dg6f+T$bS2exSu)aAOMEJYetyr5(hyb;@V{duBy#Ch7ImrbYJe{e+cO=dMfbEM~= zlHQArFyeME$$hKh=Mng$HT_6AxtH+|5#8c|%kF+vq06K8<@;B1=UY`YY4|O|ndU7J za%(%oMt3Q?$5MS94qt=LzTYXOA2kijP(iP%Y`=Wj#TQU zs?AMUTb1@levbW$+)O{HXr{7mrT}^8AGgwn%3bUL(vt;My@5mewdLhBl9BX-=lE8p z`P#x!N@ou_y%6*~we9GIxq4MKam)SDr72bW)IMR)Y8ho8QfbA!!W#puZk%r?PTXTnyrdsnn-aoIuBt+p|uW%I*# z>v`M36+4a7i&k&l&q6;;-q^7vo;s)%)&Ub&%S{X&E_>Jlz@RQ#IILydUtW#lv}xS^ zYwutbk#QuqS54S~(W4W;LcftuQ2;QJ*j1aRl|&UY@e;*Pr@m-VdP~NKr>AopswAm% zF?9IrP`r$L%N)x7Sa4Rm`}m1MF{M7Z6oxzrtFBwR$Spff+7z)&MxJ!7(Qqat_fS;k zwHMIn91x-4Qkq7Dc>CpdR7zvV@$QEj7j1(3IrM8yMU{XrWKgFe!6z;96f6(cAW|~b zfxuKcvx#)N4K;;eGv5x$DujVg5OP$-Ne{|O(KHp=*>I3a zq=nA<&TjRv5ql{VZdYlh+3I}A0iYBh(xgfXj`9NZrAfY@kE;L?eEht#_ZecB5jkPl z5fyu$$@)L%zAqzbg0148Q6q-);*F zEw%&*aXbsHb>FL|I*ZMT3>I7bUEx{!(VtGAvz+}?#dk<~@bVJyir>5y*L?dMEy~WR zt+7R|%>ooa(C{wOf!cR|3_+EkgN-pXf-S1&mL7|pZd&Vx?GYU<_;ip*{BG@T8qd6{ zr*#TV%uTO)P@+~;eN=ObKs+2O{Ut(PV=oeVC!XKt6NFodYa`sv;d40SXSt>-A;Pz9 z9t>{9#06gBgQ{=`RUob=zxh(r`CULvVP6n=kli}wpfUA9aP+V}@^{APHp^h)BHo_; zs9D=gF`}$G7c{lt3QoM%B@y#rsN7BV#BA@?0y5EY2RzlhpR*#fAgjmSe_e)QuXZuo zCId=8ORHc&V@&a-w21{KKpFbB{U6d{gfqX<&rIQ3L~J{um0a~$qTB&kW-RMfc{g27 z$XtMUaz!M@3cq%4yI(J2uiwH$Zd3+-7ZRkxq(y;yY$zSloi(^viSXiRv#yF^Hg}^_ z_GDwcIAoFvkn^k}8me8t%3)S;UvfaT7U92hVDFFnxZLB`#D)$NJbHj^-k#>J^ctU3lu-c0tG1rTV1h9ZJv48z zyU}G9q%R|J`!)}l0mbQ}cl3^EtO656zdW-uSWBlFJiF^lb|QtEANjf%KbXoXy%cNdaDDQ@*rl*OPtNMg7urjTHSG3($lea;%oa|uh%}UyeZYNTCmN;lf&ag zU!$W)X+*Q4Gn+voJJS6x5tc=h9%*j@eZC zS7|Pn*bq~V3!id3lgQXu(j4pFkGU<mwjw&f6G;ygfzkBaLfdGd^cv!41k^?f}IX% z2V(vC>Lxx@$DnY_44GePJo~`iQTiyBvGf1+<-`BcTCRV$BPSViLDM+I;#;y?O4B^% zau2UGOO+p8x4EU8Q3(DcOajr4EtJ=&^0Ht?b^I2LfD=_ z8IBol^L|@(fzCG7^)xa*m;JZZW9$MuQdL8%D=s2N@M?f~MPT+;lQ}%B#d+6E*cV8> zOP-if*`6j~AyMXIK^TdbHou>I9-{>=Ym8~QE;7@p`@iJe`w=f_2BF`9Y3g2RivFW` z-;_|DacJ7PBDjUsbNMx-;J9aoyF3twP$j7y(IBa19a{+6&MasEZ2W9pXef;&<{6qo z@L4^rS$P^RV~L`ig-75;&Vd!i;jpLD)9RkqVeTA~g;+ea^2>)s*E}spzA|n|=S&G; zrb2`4#RC8An3_Anxlg83(rE3SfZlM7&8Tn2@kY#{1Z^*lQtSjsIe}5b@);l`6lZq# zGaC5pd40sW5BxiRmDME;i$LL{OuB4!zzN>!=W*Vh9U+$ePo(7^70xRvj8)S)nFm`V z&XGZtjDwZ5Gp`4(ar-{xi9&PE0NbV07}G@O{*kWp1k1MDwpV#c2@&rgym{)fLt-1k5h}A{!O2B{-a5^WAe{eH zIq#qK{zyE8ld+jzJtqelY__bsUeEvC_+%!)A#B@q3)b(IFo^nE@q%}vw~hmrip6g z^kfLt1uC25+5vAK?);B(ssX5luD)Dh@Wd4Oq1Vx}*+WPT{lj|(I8_AoU|W)jtciIh zpR5qpbc|fAj|9My;3oKI1-R(8|AR?U9nFu~ZY=<_zsx|zoTJRI>`fI&`ZDFWAaxlX z=Gjuq-pfp$xw1NY_`E2`mO)4MY}}x!lXFc$@P@z>z?d(5c8R#ZWnwOMCwK`A>5AEA zaZ4iQn;O3ui15UM*n%@T8AiG+L?ote8h@Goc>9?NLSA1ORhgseeX^-tGZZ=UM(?u3oy+~d!GR1?!#5>NwZ_b z)2%_~!yw!0ERU4;G!_y$L$0O85Q{B`Yn2L+(1(DX0HEc!i4P|M>)_dy!R>SHpce|7 z<1W(m7Jo>uug0%gkX4$su{4nSj%}T3l_brvzjCe>FJ`)wHG!|8H>FriQMZrcay>ps zo6WbyuhrfAs33Z`zVQA?vfqr4>#T75v}-I35ScZmEdJa};>Z+7T`PuqMLZx3T-LY+ zbq$An5!#r(bN6d%lpwbS)(@Q62Tj73_THV7bN{+zmZ7{!zMb5?+U}6VEdW6?Gzsa) zQ{6j6v7FH)H0JrVCToyS%-d zo?xQIm*`ypWv*mE^xLydV*rD*Y;&C|`)N1kgIa8Tns|ZW@i2O9asCJI_{#D*A@Ta_ zffR?gbAION{Nb?~ZeAi>uZLaJCjH#z&vyRuu8G#c3I{h!u#Kf4L}bSMoJx87yXTP3 zRIF7Ok~vl!kgEqo(6O8lKQ^>XY2d&&OhcE9)IaHDS~c)a8{}mF?GrGagNAm#9qpfJ z2Ov9JTl^v!@&#IuRhXA|Ii=vLyKcDaG-=I-C#-%fi(9U{g(_3muyB52l(bhISOgHv zZ8J=-)7#!=Za$S`1Wo3zQdyC<&!<*FD>B=x>w^uqo|TAG|6E`K)Wo@{KD70jG}e5K z*^j!kZ{+3!=53zRAVkAnm`n@e3c}>~OggO_lXqnh@!=EZX_UuS!UI|- zj;c*c%vz1HxHRh*<{t@#d@&QV3-XVMBwY~5+cj*gcC0xasd)sczSvs9aB9~_> zMc5lKizN3egPR1f*9&(XSO;=@t7@Wn3)h(KPs^A3k|wLX&psC_%b(`&s|rDIyCHKt z{qc6fcT1zd7w8JhGz zNU8MgTSu4Q;)1Km3|*(2fW)$M-=~sMjag^)_o%VtI)t$DTx#;Zpu=RucReB=`)`{U zWZtH{C!>ig?OPwnH9i9TGJe zS|_SWa`pG1t!&2ytMh`-xi^nG9^QG+Fn?{A2>gF;z*-^+uN)mVbya;CUW^yxmwN}4 z>sT54uiHCuK8h7#l?SDk4Re?C^{sJYtJ6rA-WHu1lq1Ds-C@#vPitps$9wqI@XP+F zjgGLZ8MoBoh|)J{RUN||C4Y~8z12xs*np#~ku7ZEnAYTrpRvxW0J>irOsy`XXkIR6 z(~30SAheU+C?et%h}Q-IyDT#;f0w6+A59$mX><1BMSB;n#;I7^{dFW8$JHdW-0bPc zt|n-a*ngx7+_%s%or^^rn@>{wq)a=Ce89HS_BSr1f310$-|drBP1Z~XNm`mz6Mz0F zNkPGrpKjfFNF(@7h47cF+#*a6{DFh+KAd$y^!=*dvQcMEsMle0F zPU)X_dphbXO70=UL!O<(W1?s{)Dgdez))Q3^Cv1$vG_?8T`c!;y8XxN%#&2vpZwSH z>tvevU_2lW^zaU{v@p$KX^hPyn|P}fWF@km#xKk62?bz~r2y$$YGg~CEC9TF|M736 zCI4;aKnM_)zF0V`Ow(0&e@pOpVsl#hLd~vB5lqM+yw!Q9;#=0{Sy@utKW#TiidTa- zLfCq9m&aQ8^3)W7me$91^j%IJZLf1?%^kEQ@@3k{s6&#|6?+Otb|r~Zw^k%%Aq+Q! zE}%Yk{P8a~cQdJ2LECSBUH$y{0X6^H{Bje=_-_-EGR~Ho6iL&#*Ljl5{{{w!M;%-g z=^j<~T=c-ER%~j6>A;J>fm>gY`jq`^`%HUvTTC2_enxC} zUmny?ZeDKcIEv2YXJVnNGrC*y&*3L+{`2?AJK(F}m*PuZEbC#~1SmA}xQ;}lUh#(D zW(c#$t>;^+sBsKk2mmL?CY2?f{O!JkmAf;dn-Q{loW)m0I4uT&h}ENOd5)b;`!TYd@li=F>q!htMg zpTb#;#dCZ{0Lr-ZJ6&N_dd_%8JX}ab9%eEzW>TphsRL?b1c^g!65MtgtBQp-Lg4UJ?MqBnj@K4=d);GZ|Q$D6{boz1vaEnubkaIk%H0?E7rP^+lp`*oN2~<++g0gd*`tWuX9I_{US}!}k%<=@IGha>c z-*Ph+wsrJQVu85*6|eHo@TU0jtvjikDBBE=opY_8T4*hb^^IlOd{Cw9%H(zw&=ti+g+{a zVlJG}7G6D!f1kuNvIj713%mXaw%%-(@yf*0YFN6uou;*MwN@U>`|0um>sMN0l}DDRJMw)YmAmJ$c zq8EDxaX2j`xK(z?Tj-qq&*$m8o}HfW6_t*=)M>Z53GexKViTwWg+&ODix>K#BMJEy z%^aFW08@8RdQW?*T|BMB(Ba>`nFj z(qK0*)7Oxi-xB}nA@v5XK_2C4jiPp+YfcxNLBo_|)wUg2JoMB|zXr|yJC3|ac#dMR ze*XABQXr2AXza)nf-kS z1J#!H!wUWO=F|nZq)omkB}5ptEskd8cvnxNExTz-!98op2{_S*)15bGm)~7Nc)s&+ zt{Hg~T;G2>J)!{oe7yV)V_qH~zMX!xysNKg54XU4sh7jeC&t;XZifU#HlbU94t|Ez zT2kwZd-Cv%=T37LY;`?xl<{YW_CSKi43jSaI5$hxwkIWPXYZITRgfNAvjtF16?LU+ z`u)TMqU}sE^4HEoo%{1`85~QDR2g(6_7i4ABTbRK4e@QgYEPQo)%jdX_y?A-^>e_% zh0?Z>Cr1+?Td4HL16eaxyx<$)HNkDEAdyvLE3ti}FU6=clTS-R!JYm-T& zwN8Lbs9W%|-**Uv zuWLQcWr}k>@(@t4@rLQ5*KZ!;x*LThn9vTMv^*81upn;BMK31N2U85+y8u*Rh81?^ z_vH3v_YP8MAOpuD$uoWZo1|YXbS|$3B2U5C#nFB6J4r%Wo+QbRUYCEW)KF>GbArct zJZ?~^Ry;IJJHNBGn>(KjZnXSE%O6;zV_4VL8PYwcg#6e^laAp`-_PKA-w0O#JfN86 z2Es8M#Q1mKLRVvZ4-n@iA7B4tO$}s|CEs(KiM59?K%TBx#^O1Ipu^;lUJ>QuNUMu3 zawO@)h4&^h8#BgUN1@r~aGJ>-_guk;4sFnU4ZQpmMrG%+b)#2stnE$y#>^N13{t zZ#AGoPQqYWOln4r&9GKp064^W35<{>;=lLVG^fY_X7lC}cN>(`SwT=6f|au`Q)id% zo58PEdN_?Td}NZjPi0-KmR)QGOA}mR^XsX$zpQ& zbi&hhVAhrYeAMnD*kTM1xIf!jib7V9KH$NWDK^@5_1 zGtzV}bM56K$<6t&24oAJGS>Q)_@ML61lT;rp~m)pEU92>g`_rF05AKS+iap_K_sqH7Z>PU5@aNqhCnu%zK3qO9SDw$G9<}Jy6y6hZ zE!JrQ&_0d6s{&+?qKU}{Pw>V)n5m^H4FAnq7-bQZDTmwQm&kRwOSYqju&J)LKm$R- zW2ETl(L@g$z+bL0Ijg2ty|89d32gv#s{A-cgiu&T0yKlUFU&Sl6ti6Llxq>caBK$; zlvL~Um>($7qt}-Jp775FV{c5E{Y02H2rQ9UXcvcJMJS<(oIoo5eQpvgXOMqY4=*)5 zU3AgwK_MR*SwD%d+f`v{)0u(igE=nrpk9abRFaIFO4>IRMJ@#^v#bg?U zjPi+HbYibGxBq;Nl#2gYY!Rw;E{S2R{gOD?&oSqZeJ5k0S6y$o^?1$!C(kU!g=(-b zUtpU4+ORDlKTf5d2&Xi}gaqEQgojXGjG7ihqEZD=K%_Y0N{dj><;A+2lTHLG!@=yY z)(}5VS}m>cw*$9C$lw9zvFuV58NPt>7;@vVFIR++=3Ig5wH4@qCj&ZOV|;qGn=-?a zWU}cHoC2CPT*`+N+ANKUU@iu8#W=s&fWz@@wJcGx8WK;Yuj}Rz0M6j+>_N9F~Rw-dVM!K62Yz3T$8M zgxMCW^4CQb`kFb0w~Vsf3oKp8zlzX+1P!7x5D3ZkzCWt4if|-BTfRC|CvYi$V|A10 znHs^Zht&{oHwmDhs)ib$bPE?NvO1ufoKP-nm>3>aL2d$__Kiyegxf%eo{s^3XYXW6 zlru;{TROijwIuAQqp)1A<|%^Mz5Vw1Oz)DqarF0R>LG-Op9W5(7J~h;X(V|%oMs|1<7EDRHLZ;kcrOW>>Kz3mcva z(m)MjOL2BCc~GrC&cl@|+i8_V5lzP$ANQ1d>|F$H(>CQQ4s)<1E?};cU=fGSiC%Wa zUh6U$Oz$)y+vJorbob%O&0a2sX9w>sh4tJj*oa3QG>wn9VfL#&dC&z~UHc6{9@HYn zkCFNuK|NMRG|X}RbIYXz`B(hisCu#J_$ZsK3iY>crE_f}W5++{>QrV6V`6RwDoBmY zBHb_Uzn-)Go|7lLzur5$hpYbiwBu6Loi_-A6TK!`)OOMnfD@Y8^21xmYAGz>-+gGD z7R5wk(H`^kgVsxU&|(ctJvk!a|28xc5*LKP7^+`GE1*3fK}*9-p~1w^^`t!xp$Eb~ z0YX3WqqOn)%#2jq;Z>}(@z^vjO=s#0Ekc7ks?t5{o^u7)!{z6^yS2)CPyU}v0oXba1B)fbMI_1XFLT? z+EqmEZ5U=R{>OAE{et&Mzm!W9;K65Jn764J_=61@`>$f2S-0_vb_tb@&42_0U*$UN+C zk_hX$^A<`Ql|mt`8n3M_zF9@x?fv(a0PR(yfz6 z;?4s8MJl(`ijtz`wI4xT~n8!9GT;!~(zp79q_IVtdO?hwDaP8tXV3hH$SilJ4qi$yUQFXUCs}wqlT*0Q zL~A*9eqJr7Y@3obJOqwHWv7W-S~X0-jln2=aM86f#&5?9o!#}B&B+;kN8-NISQaX(6c01Bo zt=$<`19N7%_-!`~%!R?z4JTKs9P}-!JqZ5yAwpdA;-@O-Q`X)qc@zW(P`8PZ z#1lR-xA#PbEQqXx`BorD(eMNfAlFn%{Vk4JP)-s#EXcY-soNaQnMLGpK3XecY2l8G zSoI5l_$9;}r>e#e+?&Pr!|C(x>#DODK4KPeOA05x5Bkq{*~IQW)~9&8_086Jn%r1T z$J-^*GkqUZ;y57|$8?n4o=}oE+PEc?-qX9z4CiD07@ijMg{%sPIcW3JL85;Jkq1wA z&z#@}uZ>(Bmv>6+x-@9EaONRMy6itaJO8ztAgRi)zFrf``UDp=WXwWL^@{?r^{0Q& zJrwifUyhKkfvULLCE=7ns6>4dboe@oX&mA#58n_c*(Ra4cB=SU%!OiaDrQ=nFw{-HggLsfuAqKGFK^o)NtV zQt)C{(zl8or)W14K-B1jFDA7MEAT{mw>4%%ek!Tq1`1xMh#@~^qd!wX)nkH2xLBK7 zI7?peFB!6^K&*l`6!xs`!zF?k;%}U}?VWKxPxdcG2RVNJ(vyQt+w$pwE?IBDNJ3Vf zwUt;BS&!$-*KDC;bE{sKE6of%Ui1mT5w0VNJUS|0R7@fWSi-MyVDH3!Zx1Gvj$WY;%eI}ZEF;dv=t0S5Gol)NASt@_ zO6ifEh?&LP7;(vGWHQ6hO&(y9f-SZ=nGbG?ocrW^a~VIVM-~?e{-I#%U%Gy0u~xlI zl%_E;sccZuRTviAJaWYV!N; z8q`XrNBLJ$rJT=Dw1u1Wwz6&;lOpsGRq1Q2XVz_wcd>-*bNnA623y1UttN_F?P&rl z8Ua{UUxV2HxjKF<96@aZk*EH;&IKEr3;VPO@jx}US7yOk74q{9%h2=lAvR{Jz~Wv2 z@kD(K4vmNDqa0&36qDB6TFG28409_;4EMKSEw zdEvd~T?Xp&hf*z6gTE>g3=XLM<{lr7$|)M;XrxxkC{Q4sY+rqOaq-wKs8}G&owBMD zAkVXZP_!}rNbE4ZCv!tD&48IVvo^kMzL`;yNJvHds zTnUl5nH7yZ9^lktKv=f{ewy2-iZ) zQ8VpXcB*W38Ay_h@lw$tsF|>37@E+M>%BJ}oAN=pAhk4?3YbQehFB^k%iG^0Mcgk1JYJ|Lb za8S-q4ekg_psSRuFr_-tU4miQvxPQ|!6Yo1>z2m?0!5cdoq3>uj-Y`d>g4bAq4`yc z2M~uX+0W?b+%m>KCRHuA6xgAqGs8)!U=)5Y^%={_T;k9UaEa=062E1myqGz}k#-dy zJhpk!Rr$wCCHb*Cj4(;gW+DBmK$re{Pl{*k8?}wI2=_?! zs32iEE<{5OW(6-3n&6F1d=!qPwp~$vra@JY^@Nrh=4N(;mR%@sq z*=2|_;9L|X$2KeZ?rxr^;N&!6CJrZ636zF-ISt?)hF)eKPS-Z;XTWiY64Llvo!I42 zL{$~Lb1j(SJjR$LGX%(US2T15l8pwW(|X#1pUanoLWZh~8nl8mB@@yB-Z;w$s@ggL zjTqL=?hvlkmKte=fm{TLH_J%Zrgec$ScFy@TCmz9T;Zpi9K)jZlIKQ_c@ed{q`YA% za2}N;zN*_Zo9n6~NHB{-qUA(81A&)eWYK}$r^6rsG8X5D3O%bz)`vufU z7KM;rw!qDNH;74g9^B{=kes*|grwS>rb09Nk5u02q~A6JGF8RcWwMk5ZZ*-KHTVW!eu#7$8I=ji2 z)0jMM^*!k^usmcP4ZT1X8^Vko-a_DzQ1-|KJ&9mxHfX#dk8Ddmf>ff#ozg@-;t7=2 zLzaVhaCXNEaBz58^Z8mbO0T(#ptnmSGw+^>@Iq_|B4*}xA!FHc{M7g=gVeu>*L9Up#W4dwv$@31 z-r5s*#pEQGrNk9ln0@sNC$>0_BDsdj&DW#XL2Njpe-TRVE|F$sP2a;$xFWxOva)Hhh6+>yN>} zN@J)4ornI%_UsR>x{XheHx$fRUYaZP)>0HRd7iFR!_Y=w`q{(XL|J;C z+g)wnu{$YbZ=AQC>3BYT51+jO?l*2~4CUl>vC#sq%uZH_%I5RoY_Y7KX?trB4R{}N zb@!(0nsu+d-@W7Fl$+Z4yJ-~R-SqS!Raa_MN|`xt_u>v*0iF49x0uV4lFcjLJRJ`Y zQ)#@t{eoBB^TBqpI&HqKCnb8{JmJDpdr#$p=)5`M_&$#E>}6Vg?cVY~4*dC>8T1yT zwI0;h9|OB}lPSF~M#zdB$vPMBi+$}v?Zs{pXq-rII{b7VNOZK^i(7H}F`O-6xOs`|~hg*p8T{#r!)lHnRa2k4MPZMttZX?DF~>y%Cq}mM6Q*G3Q1c@E5rw zO@o$c8Lqf+e~GCcdQqir_Ov_!`J7CYbM#IlKzQhdOoTyI%9U(@Wr;2Ne) z(lk2(hvZr!=|H|$F;(Zt@|9L~Mp&)34c|Cy<05L_6Nlty%kJ?8#j&VGcZ@$dtmu+5 zWyGvBK#73Gjv@&uVyT!ng&2>q6)Zrs!lp}|imCM5k91G{HM&lBIb0@)Yuf$5`;B%u zF^Fy&-uR;O1}I!sL`U?E1H{41w?|J6-9`V-Mo+Otny%KNT9LQzTKrRBu1zH6q}{O@Na1z2 z>Zhf5Ma;jY5Uu#56s3gac0!IG+uf~R)h;E5DrbW;4L(=J5KS5f>qxwdny^e4!sH}V zPH{UZTkd@s3s`?w1e8${dl=Xq%bvI#iLOBnk>-808IMYHB!DXSS7s-p)VXUvg;nS& zuG5Yr*h?HnL%v7RwZDG{R!AHtJgFR;0xy5kE_z?{0g<_9btg%;L=(XXkB9JgNg=iS zPNNnZm>i`Xf_H2if3sm3HNa?ASYAL|0UJJM3$nJX1gT~x*~*r}OvKA?22lhDWVrWF zPl-Id1VGscXTadHn`N{y>@b+1Cf9`!B1s3(j_qcuypHKL{c$E6wy8s-YUAV6)dRjX zpN@tiKg)7~tIb#>jww|{zwG_A_wnP zRLt0eNfEJ^7NqQ+w(}|Iop9A}Zm8|pGdjB+b#Hg&Wyd=qZ50V65eFgZ<-EK4BRO_I zKFo~rNvW84&f$0>A`h?q@w9KX>yM|AqTJ8%`RzUm$z-)%q`&ZDkE8ESyf+6@*`J-jNXkM>CB`JW9XAwm#j1$qs+>99G z5u>EVGa42)^y2?J>U~f7s0;mL9}x2dX&Lx& z(uHT;tPWUoICjgLNd}FXz9py2Ox}G$52sogGd2FLG-Zz}Yz}JS%JZ&XI&Y$mh4K>J z0FD-808d)Kv7-C(br$jAhCyn$oM&B@M1WY1hvjp3DLDu9dQ3a19ERXwRnwhLIbxK_ z152Fb+8NrcO}EI2HwkRpdVEu9#pzRUDV($kfE1~kQScTy`m8(rgPqKdf(QTP!3-zsjC^GNPHBiC9yvT-N}`R@RIW>mIUDxne6=2MLe| z#UQ}$ljwTmTkr?q-VyYO&ZLnzn=e<#m3`c&dhXnh0^LgF_OJP*bNhWP@n2-S93tM# z7cgQH`u3r%#Xi}MPB<*ef1-@y=KG=9EJp9s@s+pr249^Ax(xrte9LuA;ii*T+GX#w z#Z;-=97Yt>-V~$jMOnw8qOiUX4`H|h6-J5rU1e~;l4~l##3SC5f6_aTOS_#fUUBe* zKQdO;+pULt{$EpiH(I)Ht?cf8xFRgZ`oc{yVE-SZiDmH&dHU@ic67Zlq8jI5F89bq=+S9@_8|S2V?B4Gw zO9ebA+g)ue_f}DDS;3<<*#L=f3MEeY*P;k=Ba_A|^pW-05$$=+u%f-uI-{%NB&1$E z{kaN*@oj7hqiFfn&KC)5iBqULtEXDl)NVpu;oFk;@>!HEUh>aL9+QQKyXJe$LBVRV zq&zRb20~$y3LQ@p_!wY%NG87wuj5vcii@q;IL zR{jCyFTfT}CEPqm9a4m(EXEB{$rainVNQ}6Ov9U2#OVy#+OXs~w0v8+cT^`FGwzSZ z4Z~;%ht+OGHvS~%h3DQ}n5x{oOy~48+PqFU;S%*uMv$GsdCFmyF~b{!4fHtUX`mBQ z0HN&gnN~{mn96rg!qpm*NJo`dRS5U4G5J_x2|BPx~6clcwm~Z#0z*&n3|wx@U!j*-cCEygbuE zeC3Mm59f1v=H2@o=g#YoVf<5}c|d)g+wytw6bHyy2~w6x{$ z{Zpz(FD&I|i~aT{^Nqckb3&N5>MfYC&60cDeZXPDtLf`BuiuMPwH|#+FQ{j_T2IYa znv|Hzjou7dKy|_Lo-0KlFb$<}9BK8pb)6{g;*`EUy?7hygv7WDGx!ewof~a8{lL1f zSLV?>;lcDMPL%7#d-Ux6L?M~0ZwhwXdK=Ke_5Rf@$>PW_SU;S_p>jQ{JVhnaKJ2G^ zFS7#jZ`>oBS0dvYEBASCcuo@21W|7{@BBq5={nE2&i)ksg!00MP9Nps!F}sZu9;7H zb%0F?Tse!*9?_ZoeSUwsLlHxgk8Hqr;snG8?~^v=^e|M!{F_IKrdiivLfzHQ@j-pe zKo)Qaj6kHrE* zhHj-=;8|Z|94tlJm=vUC>7vS_pEMDV0*uw_1;%Zb=G1hK=(OGml#Tn3!30knr_zez z;aB{tH~H74Kf%anye0TqI%d^*g5kvIUaZ5lY49o7=8{B|u^ZhmWUA>K>F_n|_;$eV zjo&2NG0QVb3!dbJ0}CL3qeeCipsc9c)D!CE>XupY>!)F1JK{HURuV&fahf7iCCHL@ zW(%|}ju25S$*sak1+`y&W^Sg&&lI>^z>&m@9I^0A$UA}>fP{3}h;cy$ym10$NdLE1 zAEGGo{6ts2p>Gock*{j;Sp7h;)bcG;8J<26OfOQSSVHmZ%-oji7)uA0dhK2=PeO_! z0bHk2i#4h=kQgaKavXaMh06w$0aO_kCi|=ZctjLK3D#z4TncuK9qc3rDpXdb*~23h zr0ppBOg?fx;WfT$W)T=!Gi}{BK;S`Enr}(099Q91&ObY|$`X2h$0B0RyF^3*QXzpN z*?-!2shzNABtwp)phmHR zpRpFoc^5n#@_GxTRP$!p1Z-O0I>rww6n)08R%gSzQ+{A4h=GTn zq7?ZMH_as`PuukkfXE2{*e*%h(VCd(&dHB(!49f+}XJ19_tcY^IwAf?fu2MU+&opnVf%pe!3C3fr{x)1S`c8Rj|71cR zqyi*?3z*yuo_{@~sg}VEf>=ihit;GSEx{aMKIv0_fSe6Lr8S8B<)ZnJySUcP{j)#m zS8_9rL!gLw=DGiW>VW(`<$C9e6@Jph~|SpCon?u+m9`qIcGk?%Jn2s zD`uuh9zjs_pD#Yx=22rNeY}v;|HA$E_A+@zx1NvEg7k-|1q9hjBt+j*H_E!1O1I?RRzf`*&M^ z!a3?gcm2tc{Qo#fuYNzj2r1nEedbU1=Hugz*3doN9S~~&fXyE$CkIZ_{`li?80;U2 zvdLh5e{~p4?$;07$E(i$<6-l(n?mQw@O`tnH|1w@R1iF{JVq%Xxz%yBk_=xidBKDDwo*m2`%x6#oOhvjR`j95mr^tj{*8Ce(U&+tG@%zFv%EVD-*gzpM11P5*>4F z+7gCj(UlfYTP7SDMV_v|?XQyaq-d+{Pfm!pFCmkxA7Ux98N15(SU zb@;V5Q^Nmc4`S&{2C^osTZRQWUh@FdO6bdaTA!{Y>Q)0ll#~BKFeeEZFLDNelVX{w z_3rm!;BU@=y6O!`D60=Ca6af<^>9hdM=dJ69s zj-M?zKoBPpPIr?~8tzoz+^Tlvp_ye6yb?64zg%{z%~aMg0;8%btmusa`2OnpyNG5( zxd(p}Gm0V8?<%{U38itk1~fqYb@c~NO%N#CkEDtk>8g>hF84&l9LHQ)n_}qpDR?Co ztxs2SvD|1CP_pT_p`Z|k+bbo|&n|B|{w3Wdsa)=#sAU&UWtO&>xD@#+2{lr>Qnuk@ z*_6V68?uM%b!@$Pqa`4%VK!kU;@x3ptx(F(Wq*&4zX zo={KYQ7bIyrQ1$V7#yj5eF^~QOuy1ID?-i_+LXl#4I8Mn?haXeeTYck>S&h~g#SsM zZZhTAZ5Q>)Fx$O9yEGc-*e1o@A7NIQZn`x!C*+iES4t#!;E_u1iPo&lr2J^BuG9P{ z^lW|jVrCnxhC*DTzV^qI!X0ydV{uaY_%$Byl3Ga;-!33X*|H4roDCgd6~E2n50hEH zPobN0wo?jVB48$k0HAww&3Wh6{huWWrB5gCa)O<@kYSY>y84SCm?WSU?Pdqx{GT?n zUQJhY!4H&LW&vu<*$vsX-*q_bum069?59~rWg2nyI#(Y4_cD(QbA!g|^Oo|Ph{IgK zKn%ml4B+uKfnqv999Ck@g<^mZ&vAK=b8br8Fev^#&$)?>(K3Q2@DCvvkJfJ?$ho~!br~0i;Fji?_J$wqiW}jtne#5Clo_s5TJ2F#kkvi@MDmU9TozIS8^LE1__x*6HpjgEyJ_rbsHn*dt~~ z8!gQOv|MQkb*g(EBGhBmP+J|#VLy_pY^;NxKa!{tnk;7DJyihglbD$c6ueS{>ACl>^MzQ(jYyB@?x_|% z;hHYgG>G_>q)!*(QK>FI8Gv@+Rv!QS{LA^fm719!+Ii{YVcg|a5>&bWp@?d^12ZB= z4{1J|^!KFU))Cd=J0Y}6#$8tJ4P3@9qE;Ih2wT&w^o{$ekm7?o%->eE744umO|P;@ z=moDl$#TkHOB;1l@$>34WZBt~I)y8$%bZRUo*4Q4)3ihp667e+pHmTUQ%Q00$$b$s zm~>=xXyV4H6%OApM@y)&Hq!S}L|1LI==eF<qGlvl@ z-4VYh^y`79i6*2-YL(cyQ`OoD4QG*&wZb6ZN4Dgb!E}1qVw|5>Q=M^|CYls-OV%AK@PAunoyL%L*fx z>V%t%Bav_Pj>qkT5HeU^;0iBAv@F>L+$T+js!3zS=xOT7c7l#q=FUHUxNru#eiZ() zm4>`UrS3)+H}ImG8gdMzPnM*b^jz zAWp1-pp}Wr=8b7E;hFel=jt>G)OJI!2!pR~G6|B|GBb~qm%MUggMy!h2ymV{Lh!KIgGOjE z3TytQN~Sz1-$j#4pE{drg07!r=Oc~D-9&9NAh;RKQp4y<43DcItsTFFG1%rC%r70y zdQtAf{C^v%EHC3?)vd+fXH5eG&Ln}WR$>74#T#0LgLW*N{d~lj1Qg`w6lamfYLq&h zrld<-NI4f^Sp-^;JtSSu>9SJAj`GDt2WyNV&OZRLm=~NjMK8`kDn5Y8zqNhk%#f&Q zKg&%48lHfI0M9>guTDo`^;6?k;9seicIQ&4Vb*_yCSOr2T$O@`>VsFxkATlb6w{#W zw4g*pTet*sD9{f&a(a3m>}Uw`gBO{>56b)+WJ`R!b+VFS+D%HFvg_KBfndavuf-4R z#KG>@#+2lm#;TG`7*{=5m|&bT|3D}SMay*ErDMEp6zXK*Wi{ur>UNoHd$9|fCg1tp3ISu%?e@4amF&(Z2rB91vo1%{;4H!kk( zb~?MqTo0cz22pRV~YDkTT8+hVVHoUDiho+fB5b&CtDS1*-WvpKDpwyr126& zAbqyvC{?AH1i13&A5>sce1JU21_psQp%ezP-b7$kGq(KhbV~LStw0OC2dDXO~&h-m*=LX~=@?c+u5m79H${Z;ba z4g*Xl3S5lmFUCYRNt`X3TQkjy*kc`OmY6X=US(JRD>j%G&V{1+q~i$jp`BF3$E|$& z>Q4z?HZ)1*B}^pW<;Fq?ZE-4xSIj{NjY+ zEpv+>?kl}`3ZK5>--v}9EIrV2IxECIIGD==zs6KusYQkgip^L;cltcpT^&~ZGZT)G z&iCh3{F{$V)?WAUEd%=2KMD3JIMwR{#>4QY?`3jP(ESjc@z|L1u~|3U(D#8}nZQ8^ zHH$BGLko_}Ny>PZWXhQHWyu$NE=5~y?&YorV-QjCNvCw};{7gSMD4{%kx-n7N?YM_ zW>_d%hlIm{1dvqsa0^snZFeSX#WD|@v~r^iA2xJc4IlwigRH~mh#&SwICgI`W}JSf z4TNuT*H=*3suK{gnI5+2B7a(AJ}EwPhTW&ip<7DiVY`_wKHubH?d~Uu=e)dV7pJbo zEI+0R&NQAO=5~_qm4y*Rl&soeW59D&Tel+$pL;s{#lwoCbI5463k)2k`?(`A_D z^N2Ppxa?@-<4YVGhXf_kC#sO7pLGq=r7B_EUVJhF4fy|G8n%aFnw4OKWfrPxWoTDaQXEcR zlHYfAbtHMVtH`KKK~f3*fb2Q;k0yM)H*o(vxH|Ul-`)jn1`qpZWhwWE!^8I&l0J-& zP`dNfU(Dw_vAfRe2+8ZUtA~G5Chm(|xb{g@>^JhDC@DvSbgM=;(@6S=LZGXo{uLpa zlB!yM>-X15YnAJ40>X5DsEGN^!LJSM2C13P7l7!2xaU6?C z%^XQFnN5$AS)zgKX=Tiilkxz@NhcJ1FaiI*9FYo{rG=VnNuPF5fOtJ6=23GtMRyeV zyQ|}5owGZfA(p?#$>~GV6mx!*WZLfhU1%}A$%q>Jp(j_q{Z7D_YsTT{c?QogpcFq(7y|@UdI|4gm#4mzOkC3;0y6q zVwnUp6VyfDq=i&5gWh&liP62P8dTB97yN6MI9_+1TEU)$@DJ++!YeN655&98q1;;} z+pe>Fx|_TXpWp^aOl88V3hB^BixqW_A738`N7!dD7=FKwqnKhS|BF1sEm7CwOGvDd zX!@aP7=cw6R9B9?Is9FZmGG2XDF$6}_?)tgbPGFJA0gXg#>#&%`j~Fpe_Wele&-nC z>ZeoASUJu6&#zNkvYkpDy&TxG8h^v#_o(XK@$+e?t5#3q7Otr|`RkQ% zz624SHi&piQFnew-Q;_!9vc{T}wW4gjW{SmKuFoAI#hj|{;y2{lS1V*=gNnEJ;T$cXMfyrF&hE=k z`>GCmv`WczCX$jMxy_2yf}W)&<&T7F64>bM*mBL4GSJ%%u(52w_C)nkWZ};{)u*F+^B;|hlif8I~ z(rxKCQZNmj1mVu8FB`pk*sqUAu1EN<9^Jt@bs_+t3<6|dMOf80rDdHauk+LhmRmO; zD$-IJNl_*cK`S6kuW)A&z?9J}$B<6u{t{l@#nO2|sg49E2}m%DuWAOiCr!7OKd%iCZ_4GJ==cDOh z*5qk#x3Hs!Q zVY3{X#d@+jZ4dTgpZf1{N0nJszj+>5k%CbeJpnPj@1t5V12-evKk+{eeS z{`+}_xjIDwXVOpf^f?&GgmDc?NZ05R`%G#(Ez1O%b!n80hjdJfY0WPFv5A>D&EPmm zJ2QdkQ)(*+9r7h+HoSHGH7!&PkAL5WxF^}?6UaFV#r{lDbC&CK(SnNXn{dZ z_w?lwpUmSXDS}%;NWnK*V#%}XQp?z&$~p(rj9ljj;dR1MqQuN}9MX~%V=5kNv^gAy{aZBx2P!}75izpy*bB<+Mdle_=@Fc5bV`KzF>-f?%42 zk>j}^_46kibfK76ZW-k3ZC8?{gD_dresXxb@X^L(aa}C3VpsidrSL;`NFR)2AHO3 zZ1L{mG#1BsA6q@c`hMnh~V?XsIV!K-5xC zb>{3>puEa~#J&$-*5U&a!*yf9QYID#EUy6qR+p;NPT|R|f1P;QPE}N6J93aVD0F#a z9xSDE*dIgFRQnWEc%X~B@1ytUCnp7*9@fKY&Jd$sMGtFg6So}E*#0YUdcJZ%>+hWV z*ct^i(g2`;zfc)d18{Zz|E9!U{f8Iy?8kr=yRU=wx(_c;4+BeDKF=Rb=-3}&uKRmJ z_(2%_@#=q|5|vKH_N{-t*iyIDvfpRR8H|>FopDP!6TdqPp2u^T8Rvw)OZ=!xph{w) zc}kTlERdk{JDdp9@)yryMrV-mItI-|O0Mi&lrGI9xR)%)D0Vq-Hre^<)g(rg3M4ws zK6$0INkSG&kT#Rwz_vtb9lUUs&*s6hlMLrW@FJ>N%}i5(kd;Xjpg|=f$8Tw{dK)== zZtC26?6;VKJiE@5jL)r}Cj$(zM1$Ie!=BB99M@JtT_--0v63*59M@Y0yq5FD2!SFv zde$bQdECv}(it1q7ZW6oP6eLSl-u|YZeoK_N}?swkw)VQCXw%Ap~Q0e`L0(wAB8MQ^$w_bbW zbdsImgG&4L*G1hCIi3xi#6T0B5Llxq4p3IPYS6R{deR6!08-E_N@!Ex0PP#(*pniCTNRetI?{tU~xs5;;u&AQsOcE@Miq*o7&Evyl9G%pdkau}vV z7mUOAqk%@_rcu;DH}TDAL{!P(M5dkymd#QG9U>B-W`@(7uloJ z`g2e6#GpP?=J7S6?wo_bbhp|J9+~=BvBav?)p?eR*2Sp7y>;UGtiV6wFfUXQx-zP) znD)g@{~N2V`h!|21LSocpl~S4AQTH`R7^7ULb3tvk|U&Q7L5 zZC)mO?5q=NkQIJ|1UOgX@zwfa;WPpBI>VFhc`D1e6HL~R=W`l0{l}~v}nmdQ0?x~wC#1mXR=Du4YB-F={*%2St1W| zUtXQl#1#^241@!4o=c4i=gm%rCjo5e?FFkTP}={3kg7P3tkcFM@IhQqQv>Xs^4NII z1@G(tb{t(VIeyH~?E6Q$vRtfgIFvlQe?aVb%=H&vR*w8K-!xV|ryvO@LZMw$A7L^M zjzljjwOi$vi};(y+f_`Rsh(O(U^(s&95~;L)qHcB%JhGm&URO4Ho!ZR7OHo*kv;MouBY8b)34ro2DyCY37;$6@N?1kS5z<08jn~)HfnSH zTt98j+fS~{?dLQ)VD%dLQKQ60Ejfu%^n}#KB}cr%U_FJt2=RmI)cugxr_%2Fyz?$z zkk&Bjyd&c;xb1I- zX9{T?dGDTB7xGTklm=(f3~5 zm>^ta)eXPXh!d%ooXp$r5N`h5=HgT-7?^4P%W{3g%iu>SG0Z^w;op~MMrRRVP_dHG zav2^~@qAO`mHCq?+?ourT-rYz{xfJJEOYRU-!nm1(@F%WelLgn%!&H-yOck%{CXa9 zBGbHXlc;L|gXg#BZ(DSCHvUFf=cDoSzkEwsbXKpOO|(EUMZy#9@Q;t}nkwZ885L~Y z3G|>&Q}`Ykl>-J%_xbtn(>Z(i`uZJoDSU+Rn9A`uBAw59QMEzqiy#Bi|8nD+b7f8t(!u#}L18K5_P@kWf%1Mf458+L^ zu$FiZXW%>zv zFN9a<$Z|sd5FX%LMsk2W1wwe9mdxiGH4BE&gh`yFSfLQ!pbM*r6?QKgF_t6bEh1)g zWC?evS~P^O7{MO07YpGj{$c@FDPP=K>Ca9wmT*>@F_VjwP7!N*v6<8*#hRa)$QcTk z5)ZnvjznoS%@2&_IQi}m;Z-`aj98fvYSEZU9HVgA5MHMfE4fFtav}VWk?bQ!c{$^6 z7IKXW74#ha*+GViAw0})Oyx8wmF$u3tRYq95bE$F<2Xv8Dsn~#mT{LVRmG7Z>?TJw zHO*iA$92k958(s)vYCuE+@D{W%qfc3lt;R-n$)#Ic!K84;tHi6unxW1Nc!3#JjAa| z;S|N|SclH6B3f4*`G%3~C-;Nq2d!AfJ*w7IdkkkkIqQofZJ5t>$~_dq7Yt-886Q>; z{K*V1QnG;=LoYUwrlES^Hzsk0qL0`k-C0AbNA)m&F`sLcd(5n$KU+!vxNB&}6i!q0 z3A2i>tR?ZJS;dcx=NJW^QZsa91>tEm!*>j44>_K3R$4QU>y&?1?K7C|WPZ;5X~7IG zQtJ5--lG@mN&P|y_4t*EoS@)~<_sNKLi8nl&bN%=5cyvY;YHfAnA=o)C4?^-${w=5 z8p5;u#T+hC_BFAlFPlmCx_s~((>P19H$r%qZmi?JH`O&iGmg^~c}qU%#%dyOn=^dN z7!H#69W_H6mU5eF@9I5}cBhYh6q&N}?U z1Wr)!dvk_=Sx#tVk9^H2_L1`kaik>+xJJ1jL-?41Y$MZ8Aw0_O%;o~+8oP$RY$wCd zYKEpv<2=Qi=qtLhmNdWEBTbmZ35xxyujs}aQvGJ8)0l}IrD#(<#6mJO3!wqOGMTd! z`P~|HWexE^+=oVtz(axU7^kf4Wx_Cy= zm`R+ZP*-(ICsq>err-I2QS2jEckgesVi7ke(?dS#&o(mm451;vGL zq(Sb_k4)tx1qPcX^kzM&hIrP}ih10m+E8(38keXuOdm0yFkGF|n8}=_)Ce`ge_W=_ zNO_q#}r_t1n19HroB?>uy1CHJT_#!O`xd&oIf{n4D+T%^o6Ytx+#q#AD)@(bfR zOo0h%n@+6YF4ZSG6T>+~&Pmp$CG)sSxyj}=J=sLsDf*V*n8-1TOci6gu$JgF@!}gs zu#a5R^)0QK!zC)u&|8e>GzDk6hW4x_d6sz4i=AYjtv_hPQlfM8Fh4MsW0d$${WF;T z6rZaenM3M%_D(aVbCJ^XT~Bw`l31WVXvAdBPL z&M@v!f2mk8m)kU0=Kd_?9uF_qKb)o13VEa_8%ed&=lsGX&QfHRYv{=a(yjK6!Ou+O zI7QcZ?$Ms5+@bnfv1SlE$h1zb`GaYkCuO}}r8nzIyTSfx!Zc1%WTRa3FRO`cGAsCw zQ5+%nX75k@!+h>gVT*pIKikN#)r_PmQ@Kg4ZF+_kWZdpPbYUAgc7*T}z1dIUogsY0 zXbzHlm!D~9%L1-bez*C=0Je~EkNHGXW^tC1d*z=VY$EkOz0FUI=O~5uTbqt7;~q5+ zc>Xey{p398eTbGU;5y|GsS^gWgXF`WOSE7H=O}r^?4dX7NgUO?{KO=Vk@uLh(~f1_ zq2h7ZFrC}fI-$;(#(Bz|ly8P{h?1w|k%^q3*lF>lE31i}ac;h2I0wje)?B6y3%Et4 zb9#Yc>>}%Vbwms1afvb)hc{$M)iDRJGukDv#uNp-_>f%a@9+fB2Ou52drExDr;D@nfXOmt=y z(K}+p_l)5XdG4wSTCNo_|=tEh^-TgpcXNX42-jXMSZWXDC|0z39ps;sveEkBsIJxeHmFb}Zx$RSHMK z*Nk8{*@{HMleA>y$|N zmhoJnOvy<2kb!I?xl|-P#_!DG3S~;WhJI`$?fsEZpC(M@Bt^?a!W(pAHPN!p&i9Pv z2zkos4cf4jTU08q78uAbvQ}_*S}>POl&KgAAJK>HBv-OW{$LL0DOFkj(1XpSu3{Zp zvz}~Koq^F@qIR`N_?wkvt8VXfXCoPFM8eawWeK;aS~C*9V+4E2T+21IVLrDg_kdnx z00+oXTYb`w#YE~v!UHsBDiazgc{1TejKIz6XL=-o_tcwxy`#z zS&za`>jOrxkL=Gx!n6F%9IjC2S+QmSo5}c`UZEMYI7iXv^*7yEL#h|#ghq_!AbDRj zJ7~=!Zc*_iabOU;$nvsT%3sXpJSASSHa*xt(yMxfCQRfs1z*!EbYvOP*CU}CUox1T zWPc+Pp5S+8aGnxxiUnQRO7dIkfYvM{-P?MDCQRWRMc?t{_TcQ%?Zi4zq6A`)JwI~&RMrF$`e?PU2X5}u(mtBCzC;@`E% zGov_2uCLW2ZJ5su%75eD3}pwIzm;eHWESTr`klDag|(#q-r6)~JjW>7NS3-8^{L5N0H;sf>7|MQfHWNQuF_&wU{oURe!VWV05eW_Wjp_S1I?e_|cagr0;BRG-WF1Db^(t-li*SNZr+Z;wQ#&go547C)%@w z+tlo?XBo~ua`aH!v|<4_sL)gV7{qom^-^23U>27s-8&LKU?AH_*GG-gibdR^W?%bZ z3fFkDpE=B2u2ZqU_dEu$os0wY5Y3puc}fg)AG))Sq(N$cpBcwd@(+%L7wO0nZc%B7 z=Qab`Nyee-nr6)80>y`UXQ3AxNH^U4pb3*WL9r2PkglvIKGOa9o{=0N=O{Hu8|HJJ za--d!{%j`081swvtRZ==Rr=b7pgiQWKql?yMte zl0M=`Mst`vlhpyOna4HCO;KYEW*eENiVw}0#d%Vu=~23~p2T!#=LaTojJz|<1KP8U zJ5-$M>Zha3}ic*{&QBEGn-44n(IA_UTh-GJo(}` zCUJ_w^W}@KtR%WX9Ql?p>?g-UJ;y)H<2vORsYM2}gA9xH98H2FxNoWc zr!nI>Ly=`>2Ax?+(sDC{MvUPQc~|(EhqkOI%}V*9Jxd6yywC9kgV;gV)p~>G%;q9x z*60m-vxzio%~^h7GN&lK&YYkND~PNY558vv2gtR-erU@AZct%kBz(qDc9M0I8A@~J za+xxl<%2$KC;b-p=QpNvj-p%LpRTMYvCUlITjp_>N4I;2VFp(ywZm*?Fgr=!>E8U# z49=6X%Qf_51F3hbdzvtw!{pmzF3^F++@QiX|c zeqj=)D0I+!1RYsU?2x$fEhE`a?!)Sg)~uq`5zi8C@$pgpLbhY(A}v_RH7Xnzdj_(T zj3?Zm-bVD;gxBF}9-M=!RM>5lu* zk=10m>vMW>nEdy=r_r6A>PO((c@FgR;NZHiU@IB+XNVPQ4(1ZotrB>Q#_=)Kx z(nZ6wv}Zl3(nrHX{LXxCQ8Pm{G-ojpzd5KjO_|JD%4dp(|1pW%)J=|t4y-0kX6y40 z3rU?N8eX6mo2isF8v2qtTQq#dOfFL=do=vQB4RnBp*~HS!8NMnjE3)+#w9A}iiXCF z=L+R=+Y=MGPTf4tz-mHX>(Pv5Bg11xHtZp9foS-e)if+1cR@4y-2q{m#Px&Qi9F*s+{6WuxI~{$Ux>a$-X>R+3y^{nD8& zWULSkk29Dnd|xpd4)SOvu^?2AhDYeg269(%c7}6~2dml_>&RX$8lGVc=crUY8opsY z&((;A5oD?v4WBZGTRc=N8rrdiA`e8vkIW-lTWsjb4$9SuhM$?rS!&mnLspUeU^KkV zcv9-g8~+jN>qSmd;vw;6JlA>nVSUIhayL-3bmbHc8%9GH4pa9Lb;@!wKN=11F_!3K z&O{&1Q1WqmXE9lxh=!*az;UWRsg{{T`lqbHZ1O*?PMFDU9(hJy*hT4Q)iWC@@m$pJ z3GvUV_PlemigYjNJ$i74DlbOEw=5*QB>sHKQ1+AaW#?oNN2vCSIl(M$(%{u-=*Bh* zy(Z5L<^m7A9u2M7Ozt<-5F@!py*H!bFIJHIt!Q|Op6sR2+xEn0V(*A2tyxX(cio?b zqg@&KY16#=Tg}R|XTPgLWS<5WaeI?I~XGSrP#Mg2|PtH*O z8|yHQ>(u#HJUKww@1mg*bBTPf_xO`F8oupROFB^iO6Y)5zS| zvxULjq~Xubz)8w9u^0NWj~u`FnsKE1)xPP&W$OPXCtT&BrgFzxN;Z>o_L1_txxoak z^1vTHV;hD4RO5``IN!qc341cRtI4f?Q$qV2?uQ5>aId%etha&)i{=8@b{Z0O7u zvUk!4^k6rI|CK9-afy1J#ewzY>Y^9>_JlaiBNbDbP>enZPBg_xH?UHn9QfkiS_* zVxXGkZ&r|QkbYzwSE)2uU$BTUMBUJXePkPImeZN7@JZ@5Nycxqz3Qth0 zjOG@#CPu>_EG5Gvv7;CJ$Tit~WDq;ZJ;nTC0Q)F1Rcsl}MXFBo9APDi>Egjaj#GJt zxU+~1GkqVc$uUd*=+9O%&2|sEu#4Pt#F4)2p~Qdcnog`B^;|QQf$ZY`dFq@AoS@YF zX!wLtoTk(Qv0^26d2nGg{K{I=EmD)TWhEIGdvBl>8_BUmJusW=)L3eUv78*s)Fs_H zPPygs$5ig}Rr0Ag%Yd$nln7HTAh<>jeFCb{ghlQwk##iXTHbPN8k$Pgk~( zYr7hu4+qJ=Lk-b`9hBawu9?AoyQ1MmhI5`rb~`tRcx;bZ&2Gx>_5Q?2&QWcj8N)iN z>=zT#9I!6qxkjyn<};%>MUg|+U=+v5f7re0&PK8vQCE!T1SOAJkCB`r<(RlIh+Pyt z?rTPHnxZFsKSMaf{U_BOqd85N9e}FHGhHh0cl_ec3|ZbDjkZ z<2=>QyPlQgzhDNli2E{T^G=4gD#KQM1;5rW`#li?qalbzZ z@*^|3!=tHUp(V@6n>rT$$2bmAEln&mV+pC!#=>KCU^P|K#X<`%>BJzF`VCsbAOUtS8rlYL7|WqCq|R=OD%F$3k;1^7KRU&prx091EW_ zh1=9`U@xpESHoENfN9*K)+4d-7e}f5Xeb83OPqFs7^RY z`Ilnh7iNBe_DASIq_1k?l44W)@jq7biw>mKtxwLMIMU>rHcoE#!JD z=FbqQTT;I*?+oPz_21F^Y@qDB^2UBr-g6%olKcHw_?S@~r~C&#V-{Da^P&B*n&gkf zhVfkI!H;8pmW+k1Wd9@^Vdq4pOX%nqWHjc=#9BvX(TzsvpJ@ z`z;n;U?5j1)zsb?z%KGNvkntDM!w(0gOThdiiNhE<*9$gmy0y$91Fj*h)9=Mc$^;W zCVSUdc#*DbBYQVJ#C)=K_dH@5MSJ+?Od?ZHdt@N@c(a$8O1|Fm#wyD7iG`n;MN;2b zc!G9pCvQKoW-527-(MfFno0xIFKGu_m*HHX^dLRPN{S3N@0m@;A!>jrq#YUy?=XhT z)F0;DtReSs=cEh!C_X|hFoGkL9BJOOf-Iw)kFFf0*yvbzkICGj;h0$H&2=h_bxvk* zo!aB{5erE@UX9U(6~rfqJ)PJ@@fq)OkggFDdq_Ks4`W&*iZ3k zYKh@or1Eri%Sv+05E~{CpXt}Eq0%h(VI^s1dk!*$)0CX!Jj~|~_5M?D?4;OS&t^t& zmXvwk37Nqy8qAkt_ETsfgpza29jsFN6%~iUylYE;zx9G-x%5T=&%p=Vf*V2Z?q~B_u(UJA! z-6lp1WG6+o+Z&U)MeQAC3+u_hQ%y3EbClg>CNZCiyVV$bDZ59ESVz{q&cwg$q4Yj6 zVH5fGdp>fGrw*7u+~T2wuIDJV51B7q;EBVr(2IlAJ>u7#rS4JhqiiG7G4;Vrl8>7o zOe6aVy~h|%QR}4rvWHTq^bc#vf7*Kl3&?y%oimRtXJg?z77;tAb{NWe8lD$#j#K@D z{$Ul#7u7D)xW^lp^cB&|&P6j861$>~`JJUCUo~Ut%Wm$!<{b2A8(FT41zp%r&KqJ( zZ;n#@raGiM>&SddebbM9+zGW4$dwP+-SV8&_56{z%<-{ZL z@G?U=#Z%FE7{e8+$Ks&{Gq}p5@p$-)(>$AqhnYl^;^75)a)|ryi-!;B!!Gitiifx8 z#TN3Vj)(W@%NYu#iHA4n%`OV0wLb1<=Sj&J z_w!sljN>BpGP$0qq)U#6@7Y31=6Lv;$%HKN@C=>UNvW*y(1`h@&1UZm=O~4<+dG4} zNZlOq@HgAZlQSM(<6pLtC6{X%#y(2tj)xw^{KW=O(S?m<&KnO;(3xFi&KD2Q(Th{m z${!CuGmd+_QXn2ClB!@lJV|ekQL9iq{K#02P@`}>{J?AyMdINhTC#@hMdRTe`f;9W z#l(gMgyQi~lP;X4Mu~W6!xqY>#6t)6kgsGse8_07@o*`7;S6<4$3tuOQuF?JXwEtc zmXR-}kiKj@e8)mEl@lxau$4mPQr_%R+C)C zx~w2xH6C7~8+$2KO>HuSi#$@@XY3_^jd=KsN!+7WP3L9>_tlbHMskiPA8>BYQ=_)) z*iPX(_RUbvP^+#uvWNT+#>2ae=NQ%N#X~dZleT_5)TcdL$p4VKXDKBg7Bhx%p7IUi z;SV;GyP>!-j>}Yd#G0%mRT8#a*jF=t=^r>XLIJTzu5S)Nc=4B z$wJaT9S@JwlI;|HCLZ2nB*#d3HXgoZGBX?4)AF=$_3@%f$kvK7&vsC)Qewe{|O8qGJ^yV-rKiMBMNouUO`H2-|`q{IBA>8BHCTgEF zzvvfcllxaS$z;NBaerPr9{yn~`I@?pS%hYKm!FxzE$aNPURX-1Kg==Ou#i-Lsx5l3 zo1)F-fjK0%P#d&k5%;M1mwhvxTRil)dBrxewsbvhSwiYoa!yAMQs5uY9r|&EvaR(s zi%7IFxA>d2WNWLo7{F<&w-aCHa)ava?Smzh=wQaMnGzkB~Nf4>U&@!v)F=@->q< zN%g^U$T{i^@hs;Em516dtH?7Va^{$(dABdyISE>d??JT&D$?oxAf zJTzeuX~x9E>x?Hh)|!kXG0u4z%rPDquO8V<-U;Rli^w=pd|5!&Not2d+@RiM-_Je% zoZ@>)nJUJN<1%HY#ly$UYivv5zHbZ~YlFbyFsb87QWope55BBibY&FbrD$h~h zY@pzOW)u@RNA=*oVIZP%}i{)fe zBGR5yQ~bjw@}5`o4CM?pF31J{k#x}bDn2zcqiim)oz+4Od)bherd`A61Vj$Jvcz|JMO_Wu2S=^xz9}Q zQRkj#AQQRE!yys=;sE&~iSRkoxyhr^L}<%4vcwYMJ*IJAJQ3bzG}ow?NQ9QGCTCK@ z&xeW7p5>&zFA*N23p>f1DiJ=DUvf0zGNr|$(bt=UZfkxc__DYbA?KI65(s6aDzH| z6X8!*lPg~$Jj1^nqE!AwXu^D=1>}WREFpctM0kR~n8Rgi7qS*B$ywMrnaV95EaFpQi;%Q+vK7A)c(4^@$Kwot5UB7DRwV%6l1J{%;ax_z^QYBdsJ4FC5R5I#av zrf`bFwG!bihH-?H2b`a&+@f;rL}<)n($sN&TCNPs}0uh?wvTn<)6GbFzmTkI6Z6i9c?y z4CD+|o{)3q5uS8EnzMxXQ)+;=Y$X5FiSP!4*iF`FVE7qR+9L{IcUp%8h+{?OyWG{ zJ~Km^$R#R#o(P{ZfNf;^A`zaY1FJ~&rCidU4P^PsJmDWUllgyohDlta*4O4A^SHy) z-{@Pmlk;1-VK}#F@SXc`g1X=PHCxEr$V_E4SE>1fdSwz9c;H8M&jjvJ`zN)>da^h6 zy^P@|FaE5bI6&bho-K^x8V~*=7i^)xuRdcWr>OQ@BK*W0?(uw6HO67eH#0w3M!Mgf ziQeoY_aADP9_%OO&qVlwQC#MM=4L8$h_#R(IpX1{u#u1YPPlxJ1E>n?XrY4ZS9YL*+9B>`kL0PAY*%9(}V3~?I7Q@XC+xW zIxoH1PVP?nkM10z*uUb&RIc%QXS1KsMQ!jKb4k@T5uT(S8_C@*;b%nig)@}yt}dCv zEgJMt<18mlPr2iFrgDbjz1)|%+~Bd^dX7_+>thBmf|Hc#>)FZ_Zc?kCeq|mR`g@n4 z58KEwAQ9eWJa=g@(7Oy9DKyAB%p`fR9_4p7l6{CcF^Ged9O^kse-4p%SR#DP7%o$J zxLRZ(krCoX4~|fGq?%zNkx|Y@J2sMSw74;m=oq!nXmXC#mrNwxIJ1SpT;!4QzMoAb zPw*N2*hk7lXJQGdCh1>#aFEiIy}vMpyHuYdCv2e9RQ1O!Zt?Im?*MG0^mH?aWyEKw zFM6_%5;Mht(VXD^S>Edy#bs*DR>z#6;+#Zi#&Yuf=YC8k%oP*eHS5lvr*K zGK#CzT%o48$D=FN5NpY|%4}mA=cuqc;qSTfHL2Ig6@RdSJZsGoCUS!()_GpBh1~1a z0fRV0xee~mVzO;?X4-cl-?N0< zynDdUAtVlZ4l|2z$V{XYTd8~4oZ>RiAJPBZ;>Dw8Dp`-|dsb2IxN|U^tJFW?9h$Y| zJgI(JK=hQ_q92jdYLHo^J0qq{;yU%u>M>T5>6{ociS+0FoWvZmUGUz;3JPEJUdT2| zT=KrgD6UfRviZ#%GG8&D=)`I=UUg>rvV#)W^e;Opd|kb;fOI#+fbJZj_)UGrOe)^; z{oLcd+vXr;?x;)VkoK9kQCamp1hHy@E!v>PNis4Xu)PG#*#u4 z7LhHU6y9PGmnok}3e8wauB4>!3IjMy+53{h-)y8%sw6)jCWUE))JdTs|FVO!X_7)y z7LhfreK3I2R8N-_zG5&3DVaVge9i=JQaM9X_=O3?GbV)>8Ok+Y&6E_nvzxrhN#P9! zbB%hL?Tu~Z%90e`Wem}*zK=d! 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE): + unlisted = size_list if online else 320000000 + + # Check online big list + if unlisted > size_list and online and not self.is_in_online_pwned_list(password): + unlisted = 320000000 + + self.listed = unlisted < 320000000 + return self.compare(unlisted, length, digits, lowers, uppers, others) + + def compare(self, unlisted, length, digits, lowers, uppers, others): + strength = 0 + + for i, config in enumerate(self.strength_lvl): + if unlisted < config[0] or length < config[1] \ + or digits < config[2] or lowers < config[3] \ + or uppers < config[4] or others < config[5]: + break + strength = i + 1 + return strength + + def is_in_online_pwned_list(self, password, silent=True): """ Check if a password is in the list of breached passwords from haveibeenpwned.com """ from hashlib import sha1 - from moulinette.utils.network import download_text + import requests hash = sha1(password).hexdigest() range = hash[:5] needle = (hash[5:].upper()) try: - hash_list = download_text('https://api.pwnedpasswords.com/range/' + - range) - except MoulinetteError as e: + hash_list =requests.get('https://api.pwnedpasswords.com/range/' + + range, timeout=30) + except e: if not silent: raise else: if hash_list.find(needle) != -1: - raise HintException('password_listed') + return True + return False - def _check_cracklib_list(self, password, old, pwd_dict): + def is_in_cracklib_list(self, password, pwd_dict): try: - cracklib.VeryFascistCheck(password, old, + cracklib.VeryFascistCheck(password, None, os.path.join(PWDDICT_PATH, pwd_dict)) except ValueError as e: # We only want the dictionnary check of cracklib, not the is_simple # test. if str(e) not in ["is too simple", "is a palindrome"]: - raise HintException('password_listed', pwd_list=pwd_dict) + return True - def _get_config(self): + +class ProfilePasswordValidator(PasswordValidator): + def __init__(self, profile): + self.profile = profile + import json + try: + settings = json.load(open('/etc/yunohost/settings.json', "r")) + self.validation_strength = int(settings["security.password." + profile + + '.strength']) + except Exception as e: + self.validation_strength = 2 if profile == 'admin' else 1 + return + +class LoggerPasswordValidator(ProfilePasswordValidator): + """ + PasswordValidator class validate password + """ + + def validate(self, password): """ - Build profile config from settings + Validate a password and raise error or display a warning """ + if self.validation_strength == -1: + return + import errno + import logging + from moulinette import m18n + from moulinette.core import MoulinetteError + from moulinette.utils.log import getActionLogger - def _set_param(name): - self.config[name] = self._get_setting(name) + logger = logging.getLogger('yunohost.utils.password') - if self.config[name] == 'error' and self.config['mode'] == 'warn_only': - self.config[name] = self.config['mode'] - elif self.config[name] in ['error', 'warn_only'] and \ - self.config['mode'] == 'disabled': - self.config[name] = 'disabled' + error = super(LoggerPasswordValidator, self).validate(password) + if error is not None: + raise MoulinetteError(1, m18n.n(error)) - if self.config is not None: - return self.config - self.config = {} - self.config['mode'] = self._get_setting('mode') - for validator in self.validators: - _set_param(validator) - for param in ['min_length.', 'cracklib_list.']: - self.config[param + 'error'] = self._get_setting(param + 'error') - self.config[param + 'warn'] = self._get_setting(param + 'warn') + if self.strength < 3: + logger.info(m18n.n('password_advice')) - return self.config +if __name__ == '__main__': + if len(sys.argv) < 2: + print("usage: password.py PASSWORD") + + result = ProfilePasswordValidator('user').validate(sys.argv[1]) + if result is not None: + sys.exit(result) - def _get_setting(self, setting): - return settings_get('security.password.' + self.profile + '.' + setting) - From 7f7fb80b3808f0819fe270c4c149cad1fed3a026 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 28 Aug 2018 16:49:20 +0200 Subject: [PATCH 011/250] Add ynh_delete_file_checksum --- data/helpers.d/filesystem | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index d4146ad8f..c07de2ece 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -299,6 +299,23 @@ ynh_backup_if_checksum_is_different () { fi } +# Delete a file checksum from the app settings +# +# $app should be defined when calling this helper +# +# usage: ynh_remove_file_checksum file +# | arg: -f, --file= - The file for which the checksum will be deleted +ynh_delete_file_checksum () { + # Declare an array to define the options of this helper. + declare -Ar args_array=( [f]=file= ) + local file + # Manage arguments with getopts + ynh_handle_getopts_args "$@" + + local checksum_setting_name=checksum_${file//[\/ ]/_} # Replace all '/' and ' ' by '_' + ynh_app_setting_delete $app $checksum_setting_name +} + # Remove a file or a directory securely # # usage: ynh_secure_remove path_to_remove From 536b46e5271d9f7457e25d9c558eab2e8e74a903 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 28 Aug 2018 21:34:33 +0200 Subject: [PATCH 012/250] [enh] Support advice with standalone password.py --- src/yunohost/utils/password.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 1e6bcf813..22dbb1810 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -53,14 +53,18 @@ class PasswordValidator(object): Validate a password and raise error or display a warning """ if self.validation_strength <= 0: - return + return ("success", "") self.strength = self.compute(password, ACTIVATE_ONLINE_PWNED_LIST) if self.strength < self.validation_strength: if self.listed: - return "password_listed_" + str(self.validation_strength) + return ("error", "password_listed_" + str(self.validation_strength)) else: - return "password_too_simple_" + str(self.validation_strength) + return ("error", "password_too_simple_" + str(self.validation_strength)) + + if self.strength < 3: + return ("warning", 'password_advice') + return ("success", "") def compute(self, password, online=False): # Indicators @@ -173,19 +177,21 @@ class LoggerPasswordValidator(ProfilePasswordValidator): logger = logging.getLogger('yunohost.utils.password') - error = super(LoggerPasswordValidator, self).validate(password) - if error is not None: - raise MoulinetteError(1, m18n.n(error)) - - if self.strength < 3: - logger.info(m18n.n('password_advice')) + status, msg = super(LoggerPasswordValidator, self).validate(password) + if status == "error": + raise MoulinetteError(1, m18n.n(msg)) + elif status == "warning": + logger.info(m18n.n(msg)) if __name__ == '__main__': if len(sys.argv) < 2: print("usage: password.py PASSWORD") - result = ProfilePasswordValidator('user').validate(sys.argv[1]) - if result is not None: - sys.exit(result) + status, msg = ProfilePasswordValidator('user').validate(sys.argv[1]) + if status == "error": + sys.exit(msg) + elif status == "warning": + print(msg) + sys.exit(0) From ca91a9cae5f866e9935ada9249b30dfe9692c746 Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 29 Aug 2018 01:09:13 +0200 Subject: [PATCH 013/250] [enh] Protect password --- src/yunohost/utils/password.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 22dbb1810..5029c71de 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -185,9 +185,12 @@ class LoggerPasswordValidator(ProfilePasswordValidator): if __name__ == '__main__': if len(sys.argv) < 2: - print("usage: password.py PASSWORD") - - status, msg = ProfilePasswordValidator('user').validate(sys.argv[1]) + import getpass + pwd = getpass.getpass("") + #print("usage: password.py PASSWORD") + else: + pwd = sys.argv[1] + status, msg = ProfilePasswordValidator('user').validate(pwd) if status == "error": sys.exit(msg) elif status == "warning": From 0a633e74d700848303459583c3f921741464e29c Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 29 Aug 2018 01:25:28 +0200 Subject: [PATCH 014/250] [enh] Change the way password.py interract with ssowat --- src/yunohost/utils/password.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 5029c71de..051e9682f 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -191,9 +191,6 @@ if __name__ == '__main__': else: pwd = sys.argv[1] status, msg = ProfilePasswordValidator('user').validate(pwd) - if status == "error": - sys.exit(msg) - elif status == "warning": print(msg) sys.exit(0) From 4a1363489a2c5dfa5f3dd6a528edbe09a551ff6d Mon Sep 17 00:00:00 2001 From: ljf Date: Wed, 29 Aug 2018 03:07:00 +0200 Subject: [PATCH 015/250] [fix] Bad indentation --- src/yunohost/utils/password.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 051e9682f..306be6103 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -191,7 +191,7 @@ if __name__ == '__main__': else: pwd = sys.argv[1] status, msg = ProfilePasswordValidator('user').validate(pwd) - print(msg) - sys.exit(0) + print(msg) + sys.exit(0) From 7a3ba81a2aa9a71d2d1f770a4a7ba423edb30652 Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 30 Aug 2018 01:16:41 +0200 Subject: [PATCH 016/250] [fix] Migrate bad password --- .../data_migrations/0006_migrate_pwd.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/yunohost/data_migrations/0006_migrate_pwd.py diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_migrate_pwd.py new file mode 100644 index 000000000..c64940d4a --- /dev/null +++ b/src/yunohost/data_migrations/0006_migrate_pwd.py @@ -0,0 +1,133 @@ +import spwd +import crypt +import random +import string +import subprocess + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +from moulinette.utils.process import run_commands +from moulinette.utils.filesystem import append_to_file +from moulinette.authenticators.ldap import Authenticator + +from yunohost.tools import Migration + +logger = getActionLogger('yunohost.migration') +SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", + "root", "test", "rpi"] + +class MyMigration(Migration): + "Migrate password" + + def migrate(self): + + if self._is_root_pwd_listed(SMALL_PWD_LIST): + new_hash = self._get_admin_hash() + self._replace_root_hash(new_hash) + + def backward(self): + + pass + + def _get_admin_hash(self): + """ + Ask for admin hash the ldap db + Note: to do that like we don't know the admin password we add a second + password + """ + logger.debug('Generate a random temporary password') + tmp_password = ''.join(random.choice(string.ascii_letters + + string.digits) for i in range(12)) + + # Generate a random temporary password (won't be valid after this + # script ends !) and hash it + logger.debug('Hash temporary password') + tmp_hash = subprocess.check_output(["slappasswd", "-h", "{SSHA}","-s", + tmp_password]) + + try: + logger.debug('Stop slapd and backup its conf') + run_commands([ + # Stop slapd service... + 'systemctl stop slapd', + + # Backup slapd.conf (to be restored at the end of script) + 'cp /etc/ldap/slapd.conf /root/slapd.conf.bkp' + ]) + + logger.debug('Add password to the conf') + # Append lines to slapd.conf to manually define root password hash + append_to_file("/etc/ldap/slapd.conf", 'rootdn "cn=admin,dc=yunohost,dc=org"') + append_to_file("/etc/ldap/slapd.conf", "\n") + append_to_file("/etc/ldap/slapd.conf", 'rootpw ' + tmp_hash) + + logger.debug('Start slapd with new password') + run_commands([ + # Test conf (might not be entirely necessary though :P) + 'slaptest -Q -u -f /etc/ldap/slapd.conf', + + # Regenerate slapd.d directory + 'rm -Rf /etc/ldap/slapd.d', + 'mkdir /etc/ldap/slapd.d', + 'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1', + + # Set permissions to slapd.d + 'chown -R openldap:openldap /etc/ldap/slapd.d/', + + # Restore slapd.conf + 'mv /root/slapd.conf.bkp /etc/ldap/slapd.conf', + + # Restart slapd service + 'service slapd start' + ]) + + logger.debug('Authenticate on ldap') + auth = Authenticator('default', 'ldap://localhost:389', + 'dc=yunohost,dc=org', 'cn=admin') + auth.authenticate( tmp_password) + logger.debug('Ask for the admin hash') + admin_hash = auth.search('cn=admin,dc=yunohost,dc=org', 'cn=admin', + ['userPassword'])[0]['userPassword'][0] + admin_hash = admin_hash.replace('{CRYPT}', '') + finally: + logger.debug('Remove tmp_password from ldap db') + # Remove tmp_password from ldap db + run_commands([ + + # Stop slapd service + 'service slapd stop || true', + + 'if [ -f /root/slapd.conf.bkp ]; then mv /root/slapd.conf.bkp /etc/ldap/slapd.conf; fi', + + # Regenerate slapd.d directory + 'rm -Rf /etc/ldap/slapd.d', + 'mkdir /etc/ldap/slapd.d', + 'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1', + + # Set permissions to slapd.d + 'chown -R openldap:openldap /etc/ldap/slapd.d/', + + # Restart slapd service + 'service slapd start' + ]) + return admin_hash + + + def _replace_root_hash(self, new_hash): + hash_root = spwd.getspnam("root").sp_pwd + + 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)) + + def _is_root_pwd_listed(self, pwd_list): + hash_root = spwd.getspnam("root").sp_pwd + + for password in pwd_list: + if hash_root == crypt.crypt(password, hash_root): + return True + return False From 6aef80cd2502aee71bcbf555d3c4b5669ffe0b5e Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 30 Aug 2018 01:18:26 +0200 Subject: [PATCH 017/250] [fix] Restrict to very common password --- src/yunohost/data_migrations/0006_migrate_pwd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_migrate_pwd.py index c64940d4a..93783d662 100644 --- a/src/yunohost/data_migrations/0006_migrate_pwd.py +++ b/src/yunohost/data_migrations/0006_migrate_pwd.py @@ -14,8 +14,7 @@ from moulinette.authenticators.ldap import Authenticator from yunohost.tools import Migration logger = getActionLogger('yunohost.migration') -SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", - "root", "test", "rpi"] +SMALL_PWD_LIST = ["yunohost", "olinux"] class MyMigration(Migration): "Migrate password" From 2d5077e2eadc5ec18273fe73d6b96ac94219a7fb Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 30 Aug 2018 01:34:12 +0200 Subject: [PATCH 018/250] [enh] Synchronize root and admin password --- data/actionsmap/yunohost.yml | 2 +- locales/en.json | 1 + src/yunohost/tools.py | 19 ++++++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8509bfb23..222921747 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1438,7 +1438,7 @@ tools: ### tools_adminpw() adminpw: - action_help: Change admin password + action_help: Change password of admin and root users api: PUT /adminpw configuration: authenticate: all diff --git a/locales/en.json b/locales/en.json index 074512311..ab5569cfc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -364,6 +364,7 @@ "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", + "root_password_desynchronized": "Password of the user admin has been changed, but Root password has not been synchronized with your new admin password !", "server_shutdown": "The server will shutdown", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_reboot": "The server will reboot", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f9ee14994..7221e6804 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -127,15 +127,28 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password + import spwd + new_hash = _hash_user_password(new_password) try: - auth.update("cn=admin", { - "userPassword": _hash_user_password(new_password), - }) + auth.update("cn=admin", { "userPassword": new_hash, }) except: logger.exception('unable to change admin password') raise MoulinetteError(errno.EPERM, m18n.n('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: + before = before_file.read() + + with open('/etc/shadow', 'w') as after_file: + after_file.write(before.replace("root:" + hash_root, + "root:" + new_hash)) + except IOError as e: + logger.warning(m18n.n('root_password_desynchronized')) + return logger.success(m18n.n('admin_password_changed')) From 26fe3d8c95e691f56f793fcfb9957565f0a844a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 2 Sep 2018 18:02:32 +0200 Subject: [PATCH 019/250] Indentation typo --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 94e26350c..48031d415 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -34,7 +34,7 @@ ynh_use_logrotate () { local su_directive="" if [[ -n $user_group ]]; then su_directive=" # Run logorotate as specific user - group - su ${user_group%/*} ${user_group#*/}" + su ${user_group%/*} ${user_group#*/}" fi cat > ./${app}-logrotate << EOF # Build a config file for logrotate From ff7942b2461378c5249f08338438b81243cac635 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Mon, 3 Sep 2018 13:15:31 +0200 Subject: [PATCH 020/250] [enh] Don't backp user home with .nobackup file --- data/hooks/backup/17-data_home | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/data/hooks/backup/17-data_home b/data/hooks/backup/17-data_home index 9e5aba5a5..ea783700a 100755 --- a/data/hooks/backup/17-data_home +++ b/data/hooks/backup/17-data_home @@ -12,6 +12,8 @@ backup_dir="${1}/data/home" # Backup user home for f in $(find /home/* -type d -prune | awk -F/ '{print $NF}'); do if [[ ! "$f" =~ ^yunohost|lost\+found ]]; then - ynh_backup "/home/$f" "${backup_dir}/$f" 1 + if [ ! -e "/home/\$f/.nobackup" ]; then + ynh_backup "/home/\$f" "${backup_dir}/\$f" 1 + fi fi done From 1fcd0b252b5b58cda542ed8aacbc6d567fae3699 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 4 Sep 2018 16:24:57 +0200 Subject: [PATCH 021/250] Remove old legacy code about letsencrypt --- locales/en.json | 1 - src/yunohost/certificate.py | 27 --------------------------- 2 files changed, 28 deletions(-) diff --git a/locales/en.json b/locales/en.json index 074512311..ebbe08943 100644 --- a/locales/en.json +++ b/locales/en.json @@ -130,7 +130,6 @@ "certmanager_hit_rate_limit": "Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details", "certmanager_http_check_timeout": "Timed out when server tried to contact itself through HTTP using public IP address (domain {domain:s} with ip {ip:s}). You may be experiencing hairpinning issue or the firewall/router ahead of your server is misconfigured.", "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", - "certmanager_old_letsencrypt_app_detected": "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation:\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B.: this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 1b80b6b49..a2886c266 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -97,11 +97,6 @@ def certificate_status(auth, domain_list, full=False): import yunohost.domain - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - # If no domains given, consider all yunohost domains if domain_list == []: domain_list = yunohost.domain.domain_list(auth)['domains'] @@ -144,11 +139,6 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si self-signed -- Instal self-signed certificates instead of Let's Encrypt """ - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - if self_signed: _certificate_install_selfsigned(domain_list, force) else: @@ -328,11 +318,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal import yunohost.domain - # Check if old letsencrypt_ynh is installed - # TODO / FIXME - Remove this in the future once the letsencrypt app is - # not used anymore - _check_old_letsencrypt_app() - # If no domains given, consider all yunohost domains with Let's Encrypt # certificates if domain_list == []: @@ -430,18 +415,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Back-end stuff # ############################################################################### -def _check_old_letsencrypt_app(): - import yunohost.domain - - installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] - - if "letsencrypt" not in installedAppIds: - return - - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_old_letsencrypt_app_detected')) - - def _install_cron(): cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" From 22cff38bbb700dedc904b771b3af8b481259d10a Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Wed, 5 Sep 2018 19:57:11 +0200 Subject: [PATCH 022/250] Revert Indentation typo It's part of the syntax --- data/helpers.d/backend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 48031d415..94e26350c 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -34,7 +34,7 @@ ynh_use_logrotate () { local su_directive="" if [[ -n $user_group ]]; then su_directive=" # Run logorotate as specific user - group - su ${user_group%/*} ${user_group#*/}" + su ${user_group%/*} ${user_group#*/}" fi cat > ./${app}-logrotate << EOF # Build a config file for logrotate From 95827438c5c0e85393b82cf2d9253380b5d49f88 Mon Sep 17 00:00:00 2001 From: irina11y <38069993+irina11y@users.noreply.github.com> Date: Thu, 6 Sep 2018 14:00:09 +0200 Subject: [PATCH 023/250] Add option '--need-lock' to 'yunohost service add' (#530) * [enh] Add option '--need-lock' * corection typo * correction merge --- data/actionsmap/yunohost.yml | 4 ++++ src/yunohost/service.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 6e2743015..140f8bc6a 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1136,6 +1136,10 @@ service: full: --runlevel help: Runlevel priority of the service type: int + -n: + full: --need_lock + help: Use this option to prevent deadlocks if the service does invoke yunohost commands. + action: store_true -d: full: --description help: Description of the service diff --git a/src/yunohost/service.py b/src/yunohost/service.py index dbdaa1cdf..5b7680a80 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -50,7 +50,7 @@ MOULINETTE_LOCK = "/var/run/moulinette_yunohost.lock" logger = log.getActionLogger('yunohost.service') -def service_add(name, status=None, log=None, runlevel=None, description=None): +def service_add(name, status=None, log=None, runlevel=None, need_lock=False, description=None): """ Add a custom service @@ -59,8 +59,8 @@ def service_add(name, status=None, log=None, runlevel=None, description=None): status -- Custom status command log -- Absolute path to log file to display runlevel -- Runlevel priority of the service + need_lock -- Use this option to prevent deadlocks if the service does invoke yunohost commands. description -- description of the service - """ services = _get_services() @@ -75,6 +75,9 @@ def service_add(name, status=None, log=None, runlevel=None, description=None): if runlevel is not None: services[name]['runlevel'] = runlevel + if need_lock: + services[name]['need_lock'] = True + if description is not None: services[name]['description'] = description From 89540c1b0b1f903709a27de5c8b51da4cd1762e1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Sep 2018 01:05:17 +0200 Subject: [PATCH 024/250] Add script to automatically generate helpers documentation (#538) * Add code to generate helper doc compatible with Simone's bootstrap * Disable markdown parsing + misc cosmetic stuff * Render categories in alphabetic order --- doc/generate_helper_doc.py | 171 +++++++++++++++++++++++++++++++++++ doc/helper_doc_template.html | 93 +++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 doc/generate_helper_doc.py create mode 100644 doc/helper_doc_template.html diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py new file mode 100644 index 000000000..20022e253 --- /dev/null +++ b/doc/generate_helper_doc.py @@ -0,0 +1,171 @@ +#!/usr/env/python2.7 + +import os +import glob +import datetime + +def render(data): + + from jinja2 import Template + from ansi2html import Ansi2HTMLConverter + from ansi2html.style import get_styles + + conv = Ansi2HTMLConverter() + shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme))) + + def shell_to_html(shell): + return conv.convert(shell, False) + + template = open("helper_doc_template.html", "r").read() + t = Template(template) + t.globals['now'] = datetime.datetime.utcnow + result = t.render(data=data, convert=shell_to_html, shell_css=shell_css) + open("helpers.html", "w").write(result) + +############################################################################## + +class Parser(): + + def __init__(self, filename): + + self.file = open(filename, "r").readlines() + self.blocks = None + + def parse_blocks(self): + + self.blocks = [] + + current_reading = "void" + current_block = { "name": None, + "line": -1, + "comments": [], + "code": [] } + + for i, line in enumerate(self.file): + + line = line.rstrip().replace("\t", " ") + + if current_reading == "void": + if is_global_comment(line): + # We start a new comment bloc + current_reading = "comments" + assert line.startswith("# ") or line == "#", malformed_error(i) + current_block["comments"].append(line[2:]) + else: + pass + #assert line == "", malformed_error(i) + continue + + elif current_reading == "comments": + if is_global_comment(line): + # We're still in a comment bloc + assert line.startswith("# ") or line == "#", malformed_error(i) + current_block["comments"].append(line[2:]) + else: + # We're getting out of a comment bloc, we should find + # the name of the function + assert len(line.split()) >= 1 + current_block["line"] = i + current_block["name"] = line.split()[0].strip("(){") + # Then we expect to read the function + current_reading = "code" + + continue + + elif current_reading == "code": + + if line == "}": + # We're getting out of the function + current_reading = "void" + + # Then we keep this bloc and start a new one + # (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": [] } + else: + current_block["code"].append(line) + pass + + continue + + def parse_block(self, b): + + b["brief"] = "" + b["details"] = "" + b["usage"] = "" + b["args"] = [] + b["ret"] = "" + b["example"] = "" + + subblocks = '\n'.join(b["comments"]).split("\n\n") + + for i, subblock in enumerate(subblocks): + subblock = subblock.strip() + + if i == 0: + b["brief"] = subblock + continue + + elif subblock.startswith("example"): + b["example"] = " ".join(subblock.split()[1:]) + continue + + elif subblock.startswith("usage"): + for line in subblock.split("\n"): + + if line.startswith("| arg"): + argname = line.split()[2] + argdescr = " ".join(line.split()[4:]) + b["args"].append((argname, argdescr)) + elif line.startswith("| ret"): + b["ret"] = " ".join(line.split()[2:]) + else: + if line.startswith("usage"): + line = " ".join(line.split()[1:]) + b["usage"] += line + "\n" + continue + + elif subblock.startswith("| arg"): + for line in subblock.split("\n"): + if line.startswith("| arg"): + argname = line.split()[2] + argdescr = line.split()[4:] + b["args"].append((argname, argdescr)) + continue + + else: + b["details"] += subblock + "\n\n" + + b["usage"] = b["usage"].strip() + + +def is_global_comment(line): + return line.startswith('#') + +def malformed_error(line_number): + import pdb; pdb.set_trace() + return "Malformed file line {} ?".format(line_number) + +def main(): + + helper_files = sorted(glob.glob("../data/helpers.d/*")) + helpers = [] + + for helper_file in helper_files: + category_name = os.path.basename(helper_file) + print "Parsing %s ..." % category_name + p = Parser(helper_file) + p.parse_blocks() + for b in p.blocks: + p.parse_block(b) + + helpers.append((category_name, p.blocks)) + + render(helpers) + +main() + diff --git a/doc/helper_doc_template.html b/doc/helper_doc_template.html new file mode 100644 index 000000000..1fa1f68ad --- /dev/null +++ b/doc/helper_doc_template.html @@ -0,0 +1,93 @@ + + +

    App helpers

    + +{% for category, helpers in data %} + +

    {{ category }}

    + +{% for h in helpers %} + +
    + +{% endfor %} +{% endfor %} + + + From 7efe7ed6abc3dece6b38c912856c8f549928673e Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 17 Jun 2018 16:35:13 +0000 Subject: [PATCH 025/250] [i18n] Translated using Weblate (Arabic) Currently translated at 91.8% (371 of 404 strings) --- locales/ar.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index cda9c2c8b..f300ec864 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -296,8 +296,8 @@ "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers:s}]", - "server_reboot": "The server will reboot", - "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", + "server_reboot": "سيعاد تشغيل الخادوم", + "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers:s}]", "service_add_failed": "تعذرت إضافة خدمة '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", From b52297b8e12b66f29ef349e9e8132168429ca5a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Sep 2018 16:47:32 +0200 Subject: [PATCH 026/250] [i18n] Translated using Weblate (Arabic) (#539) Currently translated at 91.8% (371 of 404 strings) --- locales/ar.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index cda9c2c8b..f300ec864 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -296,8 +296,8 @@ "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "server_shutdown": "سوف ينطفئ الخادوم", "server_shutdown_confirm": "سوف ينطفئ الخادوم حالا. متأكد ؟ [{answers:s}]", - "server_reboot": "The server will reboot", - "server_reboot_confirm": "The server will reboot immediatly, are you sure? [{answers:s}]", + "server_reboot": "سيعاد تشغيل الخادوم", + "server_reboot_confirm": "سيعاد تشغيل الخادوم في الحين. هل أنت متأكد ؟ [{answers:s}]", "service_add_failed": "تعذرت إضافة خدمة '{service:s}'", "service_added": "The service '{service:s}' has been added", "service_already_started": "Service '{service:s}' has already been started", From 9550bf3eb15906b4864ce4b22ccc96e6fa9a61c8 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 7 Sep 2018 16:50:17 +0200 Subject: [PATCH 027/250] Add many print helpers (#523) * Add many print helpers * Typo fix --- data/helpers.d/print | 103 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/print b/data/helpers.d/print index d35c3e929..93d402e64 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -8,12 +8,11 @@ ynh_die() { # Display a message in the 'INFO' logging category # # usage: ynh_info "Some message" -ynh_info() -{ - echo "$1" >>"$YNH_STDINFO" +ynh_print_info() { + echo "$1" >> "$YNH_STDINFO" } -# Ignore the yunohost-cli log to prevent errors with conditionals commands +# Ignore the yunohost-cli log to prevent errors with conditional commands # # [internal] # @@ -29,3 +28,99 @@ ynh_no_log() { sudo mv ${ynh_cli_log}-move ${ynh_cli_log} return $? } + +# Main printer, just in case in the future we have to change anything about that. +# +# [internal] +# +ynh_print_log () { + echo -e "${1}" +} + +# Print a warning on stderr +# +# usage: ynh_print_warn "Text to print" +# | arg: text - The text to print +ynh_print_warn () { + ynh_print_log "\e[93m\e[1m[WARN]\e[0m ${1}" >&2 +} + +# Print an error on stderr +# +# usage: ynh_print_err "Text to print" +# | arg: text - The text to print +ynh_print_err () { + ynh_print_log "\e[91m\e[1m[ERR]\e[0m ${1}" >&2 +} + +# Execute a command and print the result as an error +# +# usage: ynh_exec_err command to execute +# usage: ynh_exec_err "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_err () { + ynh_print_err "$(eval $@)" +} + +# Execute a command and print the result as a warning +# +# usage: ynh_exec_warn command to execute +# usage: ynh_exec_warn "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_warn () { + ynh_print_warn "$(eval $@)" +} + +# Execute a command and force the result to be printed on stdout +# +# usage: ynh_exec_warn_less command to execute +# usage: ynh_exec_warn_less "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_warn_less () { + eval $@ 2>&1 +} + +# Execute a command and redirect stdout in /dev/null +# +# usage: ynh_exec_quiet command to execute +# usage: ynh_exec_quiet "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_quiet () { + eval $@ > /dev/null +} + +# Execute a command and redirect stdout and stderr in /dev/null +# +# usage: ynh_exec_fully_quiet command to execute +# usage: ynh_exec_fully_quiet "command to execute | following command" +# In case of use of pipes, you have to use double quotes. Otherwise, this helper will be executed with the first command, then be sent to the next pipe. +# +# | arg: command - command to execute +ynh_exec_fully_quiet () { + eval $@ > /dev/null 2>&1 +} + +# Remove any logs for all the following commands. +# +# usage: ynh_print_OFF +# WARNING: You should be careful with this helper, and never forget to use ynh_print_ON as soon as possible to restore the logging. +ynh_print_OFF () { + set +x +} + +# Restore the logging after ynh_print_OFF +# +# usage: ynh_print_ON +ynh_print_ON () { + set -x + # Print an echo only for the log, to be able to know that ynh_print_ON has been called. + echo ynh_print_ON > /dev/null +} From 8edc5c1ae35bec49f87a6cef0e612c920abf722c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 5 Sep 2018 23:20:31 +0200 Subject: [PATCH 028/250] [mod] uniformise returned info --- src/yunohost/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 62fe9129c..da6e6d767 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1544,6 +1544,8 @@ def app_config_show_panel(app_id): if not os.path.exists(config_panel) or not os.path.exists(config_script): return { + "app_id": app_id, + "app_name": app_info_dict["name"], "config_panel": [], } From b111c57aa591f9b60b19a260f70467bdcf8a9fd1 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 8 Sep 2018 02:45:18 +0200 Subject: [PATCH 029/250] [fix] app_id is id in app list, not in yunohost --- data/actionsmap/yunohost.yml | 24 +++++++++--------- src/yunohost/app.py | 49 ++++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 140f8bc6a..ec4a6bf08 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -780,18 +780,18 @@ app: ### app_action_list() list: action_help: List app actions - api: GET /apps//actions + api: GET /apps//actions arguments: - app_id: - help: app id + app: + help: App name ### app_action_run() run: action_help: Run app action - api: PUT /apps//actions/ + api: PUT /apps//actions/ arguments: - app_id: - help: app id + app: + help: App name action: help: action id -a: @@ -805,18 +805,18 @@ app: ### app_config_show_panel() show-panel: action_help: show config panel for the application - api: GET /apps//config-panel + api: GET /apps//config-panel arguments: - app_id: - help: App ID + app: + help: App name ### app_config_apply() apply: action_help: apply the new configuration - api: POST /apps//config + api: POST /apps//config arguments: - app_id: - help: App ID + app: + help: App name -a: full: --args help: Serialized arguments for new configuration (i.e. "domain=domain.tld&path=/path") diff --git a/src/yunohost/app.py b/src/yunohost/app.py index da6e6d767..3b5cecd7b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1461,33 +1461,33 @@ def app_change_label(auth, app, new_label): # actions todo list: # * docstring -def app_action_list(app_id): +def app_action_list(app): logger.warning(m18n.n('experimental_feature')) # this will take care of checking if the app is installed - app_info_dict = app_info(app_id) + app_info_dict = app_info(app) - actions = os.path.join(APPS_SETTING_PATH, app_id, 'actions.json') + actions = os.path.join(APPS_SETTING_PATH, app, 'actions.json') return { - "app_id": app_id, + "app": app, "app_name": app_info_dict["name"], "actions": read_json(actions) if os.path.exists(actions) else [], } -def app_action_run(app_id, action, args=None): +def app_action_run(app, action, args=None): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec import tempfile # will raise if action doesn't exist - actions = app_action_list(app_id)["actions"] + actions = app_action_list(app)["actions"] actions = {x["id"]: x for x in actions} if action not in actions: - raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app_id, ", ".join(actions.keys()))) + raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) action_declaration = actions[action] @@ -1508,9 +1508,9 @@ def app_action_run(app_id, action, args=None): os.chmod(path, 700) if action_declaration.get("cwd"): - cwd = action_declaration["cwd"].replace("$app_id", app_id) + cwd = action_declaration["cwd"].replace("$app", app_id) else: - cwd = "/etc/yunohost/apps/" + app_id + cwd = "/etc/yunohost/apps/" + app retcode = hook_exec( path, @@ -1521,7 +1521,7 @@ def app_action_run(app_id, action, args=None): ) if retcode not in action_declaration.get("accepted_return_codes", [0]): - raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app_id, retcode)) + raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode)) os.remove(path) @@ -1531,27 +1531,31 @@ def app_action_run(app_id, action, args=None): # Config panel todo list: # * docstrings # * merge translations on the json once the workflow is in place -def app_config_show_panel(app_id): +def app_config_show_panel(app): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec # this will take care of checking if the app is installed - app_info_dict = app_info(app_id) + app_info_dict = app_info(app) + + config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') - config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') - config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') if not os.path.exists(config_panel) or not os.path.exists(config_script): return { "app_id": app_id, + "app": app, "app_name": app_info_dict["name"], "config_panel": [], } config_panel = read_json(config_panel) - env = {"YNH_APP_ID": app_id} + env = { + "YNH_APP_ID": app, + } parsed_values = {} # I need to parse stdout to communicate between scripts because I can't @@ -1608,23 +1612,24 @@ def app_config_show_panel(app_id): return { "app_id": app_id, + "app": app, "app_name": app_info_dict["name"], "config_panel": config_panel, } -def app_config_apply(app_id, args): +def app_config_apply(app, args): logger.warning(m18n.n('experimental_feature')) from yunohost.hook import hook_exec - installed = _is_installed(app_id) + installed = _is_installed(app) if not installed: raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app_id)) + m18n.n('app_not_installed', app=app)) - config_panel = os.path.join(APPS_SETTING_PATH, app_id, 'config_panel.json') - config_script = os.path.join(APPS_SETTING_PATH, app_id, 'scripts', 'config') + config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') + config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') if not os.path.exists(config_panel) or not os.path.exists(config_script): # XXX real exception @@ -1632,7 +1637,9 @@ def app_config_apply(app_id, args): config_panel = read_json(config_panel) - env = {"YNH_APP_ID": app_id} + env = { + "YNH_APP_ID": app, + } args = dict(urlparse.parse_qsl(args, keep_blank_values=True)) if args else {} for tab in config_panel.get("panel", []): From a95fb3905ef67f67e8e8993f2dead03181edda55 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sat, 8 Sep 2018 02:45:33 +0200 Subject: [PATCH 030/250] [enh] add more local variables during config execution --- src/yunohost/app.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3b5cecd7b..8c35d5e39 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1496,8 +1496,12 @@ def app_action_run(app, action, args=None): args_odict = _parse_args_for_action(actions[action], args=args_dict) args_list = args_odict.values() + app_id, app_instance_nb = _parse_app_instance_name(app) + env_dict = _make_environment_dict(args_odict, prefix="ACTION_") env_dict["YNH_APP_ID"] = app_id + env_dict["YNH_APP_INSTANCE_NAME"] = app + env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_ACTION"] = action _, path = tempfile.mkstemp() @@ -1542,6 +1546,7 @@ def app_config_show_panel(app): config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') + app_id, app_instance_nb = _parse_app_instance_name(app) if not os.path.exists(config_panel) or not os.path.exists(config_script): return { @@ -1554,7 +1559,9 @@ def app_config_show_panel(app): config_panel = read_json(config_panel) env = { - "YNH_APP_ID": app, + "YNH_APP_ID": app_id, + "YNH_APP_INSTANCE_NAME": app, + "YNH_APP_INSTANCE_NUMBER": str(app_instance_nb), } parsed_values = {} @@ -1637,8 +1644,11 @@ def app_config_apply(app, args): config_panel = read_json(config_panel) + app_id, app_instance_nb = _parse_app_instance_name(app) env = { - "YNH_APP_ID": app, + "YNH_APP_ID": app_id, + "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 {} From 3d60e2d34bb05f15f7efb69dc830ae41b337a833 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Sep 2018 14:35:54 +0000 Subject: [PATCH 031/250] Update changelog --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index a24f5c054..8b57aeee0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (3.2.0) stable; urgency=low + + * Add many print and exec helpers (#523) + * Add su directive as option for logrotate helper (#511) + * Add equivs, fake-hwclock and jq as base dependencies (#515, #514, #532) + * Allow to add a service description on "yunohost service add" (#529) + * Add option '--need-lock' to 'yunohost service add' (#530) + * Don't backup user home with .nobackup file (#536) + * Add a script to automatically generate helpers documentation (#538) + * [i18n] Improve Arabic translation + + Thanks to all contributors (Bram, Maniack, irina11y, Josue, BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Tue, 11 Sep 2018 16:30:00 +0000 + yunohost (3.2.0~testing1) testing; urgency=low * Add logging system of every unit operation (#165) From 8e574d2b5f19173762f01d7be564df2522a6cc86 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 13 Sep 2018 15:36:18 +0200 Subject: [PATCH 032/250] [fix] Error due to unwanted backslash ! (#541) --- data/hooks/backup/17-data_home | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/hooks/backup/17-data_home b/data/hooks/backup/17-data_home index ea783700a..f7a797b6b 100755 --- a/data/hooks/backup/17-data_home +++ b/data/hooks/backup/17-data_home @@ -12,8 +12,8 @@ backup_dir="${1}/data/home" # Backup user home for f in $(find /home/* -type d -prune | awk -F/ '{print $NF}'); do if [[ ! "$f" =~ ^yunohost|lost\+found ]]; then - if [ ! -e "/home/\$f/.nobackup" ]; then - ynh_backup "/home/\$f" "${backup_dir}/\$f" 1 + if [ ! -e "/home/$f/.nobackup" ]; then + ynh_backup "/home/$f" "${backup_dir}/$f" 1 fi fi done From d420bb240f8a97ce63521e5104d336ecb60a3aef Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Thu, 13 Sep 2018 17:00:26 +0200 Subject: [PATCH 033/250] [enh] Don't send email if no certificate need to be renewed (#540) --- 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 a2886c266..049eeb0f4 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -340,7 +340,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal domain_list.append(domain) - if len(domain_list) == 0: + if len(domain_list) == 0 and not email: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given From 9e87da8d2656355428a11fb482852995f46bd1a3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Sep 2018 15:03:36 +0200 Subject: [PATCH 034/250] [yolo] Add head -n1, because on some setups like stupid OVH, 'VERSION_ID' is duplicated :| --- src/yunohost/data_migrations/0003_migrate_to_stretch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 7347f0e66..26f91ae0b 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -94,7 +94,7 @@ 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 | 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_installed_version("yunohost").split('.')[0]) From 53388e4e28fed3b1a207b07c2dccc4fe55497f7a Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sat, 15 Sep 2018 16:07:09 +0200 Subject: [PATCH 035/250] [enh] Speak the user about post-install on browser --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index de05dd6fa..2705dbcdc 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -56,7 +56,7 @@ else echo "$LOGO_AND_FINGERPRINTS" echo -e "\e[m Post-installation \e[0m" echo "Congratulations! YunoHost has been successfully installed.\nTwo more steps are required to activate the services of your server." - read -p "Proceed to post-installation? (y/n) " -n 1 + read -p "Proceed to post-installation? (y/n)\nAlternatively, you can proceed the post-installation on https://${ip}" -n 1 RESULT=1 while [ $RESULT -gt 0 ]; do if [[ $REPLY =~ ^[Nn]$ ]]; then From abecd90c38b4f20a6a3c8357a64e9ddf17c629bb Mon Sep 17 00:00:00 2001 From: Xaloc Date: Sun, 16 Sep 2018 13:42:55 +0000 Subject: [PATCH 036/250] Added translation using Weblate (Catalan) --- locales/ca.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/ca.json diff --git a/locales/ca.json b/locales/ca.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/ca.json @@ -0,0 +1 @@ +{} From f3373e81d6f0736cd21058b657c79145bc2919a0 Mon Sep 17 00:00:00 2001 From: Xaloc Date: Sun, 16 Sep 2018 13:44:54 +0000 Subject: [PATCH 037/250] [i18n] Translated using Weblate (Catalan) Currently translated at 2.4% (11 of 453 strings) --- locales/ca.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 0967ef424..029f685a6 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -1 +1,13 @@ -{} +{ + "action_invalid": "Acció '{action:s}' invàlida", + "admin_password": "Contrasenya d'administració", + "admin_password_change_failed": "No s'ha pogut canviar la contrasenya", + "admin_password_changed": "S'ha canviat la contrasenya d'administració", + "app_already_installed": "{app:s} ja està instal·lada", + "app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a \"app changeurl\" si està disponible.", + "app_already_up_to_date": "{app:s} ja està actualitzada", + "app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}", + "app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}", + "app_argument_required": "Es necessita l'argument '{name:s}'", + "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar" +} From 4a273da5950220a283f0314b50e02b99c690c6eb Mon Sep 17 00:00:00 2001 From: Xaloc Date: Sun, 16 Sep 2018 13:52:19 +0000 Subject: [PATCH 038/250] [i18n] Translated using Weblate (Catalan) Currently translated at 5.0% (23 of 453 strings) --- locales/ca.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 029f685a6..6c06d55b3 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -9,5 +9,17 @@ "app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}", "app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}", "app_argument_required": "Es necessita l'argument '{name:s}'", - "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar" + "app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar.", + "app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar nginx. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}", + "app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain:s}{path:s}'), no hi ha res per fer.", + "app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar l'aplicació.", + "app_change_url_success": "La URL de {app:s} s'ha canviat correctament a {domain:s}{path:s}", + "app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació", + "app_id_invalid": "Id de l'aplicació incorrecte", + "app_incompatible": "L'aplicació {app} no és compatible amb la teva versió de YunoHost", + "app_install_files_invalid": "Fitxers d'instal·lació invàlids", + "app_location_already_used": "L'aplicació '{app}' ja està instal·lada en aquest camí ({path})", + "app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini {domain} ja que ja és utilitzat per una altra aplicació '{other_app}'", + "app_location_install_failed": "No s'ha pogut instal·lar l'aplicació en aquest camí ja que entra en conflicte amb l'aplicació '{other_app}' ja instal·lada a '{other_path}'", + "app_location_unavailable": "Aquesta url no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}" } From 0b52b9224c8db4fc3cff948750e879584ad95a85 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 17 Sep 2018 16:07:11 +0000 Subject: [PATCH 039/250] Update changelog for 3.2.1 --- debian/changelog | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/debian/changelog b/debian/changelog index 8b57aeee0..5b3ae1055 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,15 @@ +yunohost (3.2.1) stable; urgency=low + + * Don't send an email if no certificate needs to be renewed (#540) + * Fix an issue with home backups (#541) + * Fix an issue with installs on OVH VPS + * Tell the user about post-install available in browser in bootprompt (#544) + * Improve Arabic translation + + Thanks to all contributors (BoF, ljf, Aleks) ! <3 + + -- Alexandre Aubin Mon, 17 Sep 2018 18:06:00 +0000 + yunohost (3.2.0) stable; urgency=low * Add many print and exec helpers (#523) From 10770aebd9283aa7a10f03a5a9681697c144f9d3 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Wed, 19 Sep 2018 16:35:23 +0200 Subject: [PATCH 040/250] [fix] Silent output/warning but display errors --- 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 595da3c2d..07b8a6d96 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -173,7 +173,7 @@ ynh_setup_source () { then # Use the local source file if it is present cp $local_src $src_filename else # If not, download the source - wget -nv -O $src_filename $src_url + local out=`wget -nv -O $src_filename $src_url 2>&1` || ynh_print_err $out fi # Check the control sum From da61546012f3bbce93704cffa0377d738e19e30b Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Fri, 28 Sep 2018 21:37:41 +0200 Subject: [PATCH 041/250] mod_auth_ldap: reflect SASL API changes in latest Metronome. The session state should always be passed in the profile as util.sasl functioning now relies on it --- lib/metronome/modules/mod_auth_ldap2.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/metronome/modules/mod_auth_ldap2.lua b/lib/metronome/modules/mod_auth_ldap2.lua index bb62ca546..f961885da 100644 --- a/lib/metronome/modules/mod_auth_ldap2.lua +++ b/lib/metronome/modules/mod_auth_ldap2.lua @@ -56,8 +56,9 @@ function new_default_provider(host) return nil, "Account creation/modification not available with LDAP."; end - function provider.get_sasl_handler() + function provider.get_sasl_handler(session) local testpass_authentication_profile = { + session = session, plain_test = function(sasl, username, password, realm) return provider.test_password(username, password), true; end, From 0596bd0f963a9220c0be14e7fbe6773ea5d68ce1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 28 Sep 2018 21:04:21 +0000 Subject: [PATCH 042/250] Update changelog --- debian/changelog | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/debian/changelog b/debian/changelog index 5b3ae1055..1562c4d2f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,12 @@ +yunohost (3.2.2) stable; urgency=low + + * [hotfix] mod_auth_ldap: reflect SASL API changes in latest Metronome (#546) + * [enh] Add the internal helper ynh_handle_getopts_args (#520) + + Thanks to all contributors (Maranda, Maniack) ! <3 + + -- Alexandre Aubin Fri, 28 Sep 2018 23:04:00 +0000 + yunohost (3.2.1) stable; urgency=low * Don't send an email if no certificate needs to be renewed (#540) From 89e58092b7bb637f85b80a455b182e955c31e0d8 Mon Sep 17 00:00:00 2001 From: flahemade Date: Thu, 4 Oct 2018 18:08:08 +0200 Subject: [PATCH 043/250] Fix potential key error when retrieving install_time --- 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 8c35d5e39..de15c1116 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -346,7 +346,8 @@ def app_info(app, show_status=False, raw=False): ret['settings'] = _get_app_settings(app) # Determine upgradability - local_update_time = ret['settings'].get('update_time', ret['settings']['install_time']) + # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded + local_update_time = ret['settings'].get('update_time', ret['settings'].get('install_time', 0)) if 'lastUpdate' not in ret or 'git' not in ret: upgradable = "url_required" From b7c8ab541f42eeaff0f15e46f872d79e5fcc3b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20PIERR=C3=89?= Date: Fri, 5 Oct 2018 12:54:33 +0200 Subject: [PATCH 044/250] [#901] Human readable date --- data/actionsmap/yunohost.yml | 5 +++++ src/yunohost/app.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ec4a6bf08..fd1d98946 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -469,6 +469,11 @@ app: listlists: action_help: List registered application lists api: GET /appslists + arguments: + -H: + full: --human-readable + help: Return the lastUpdate with a human readable date + action: store_true ### app_removelist() removelist: diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 8c35d5e39..b2116d966 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -36,6 +36,7 @@ import glob import pwd import grp from collections import OrderedDict +import datetime from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError @@ -66,10 +67,13 @@ re_app_instance_name = re.compile( ) -def app_listlists(): +def app_listlists(human_readable=False): """ List fetched lists + Keyword argument: + human_readable -- Show human readable dates + """ # Migrate appslist system if needed @@ -80,6 +84,14 @@ def app_listlists(): # Get the list appslist_list = _read_appslist_list() + # Human readable date + if human_readable: + for app in appslist_list: + now_for_humans = datetime.datetime.fromtimestamp( + appslist_list[app].get("lastUpdate")) + appslist_list[app]["lastUpdate"] = now_for_humans.strftime( + '%Y-%m-%d %H:%M:%S') + return appslist_list From 8670f0a1de0b8f156e671be086109fb4a90ac384 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sat, 13 Oct 2018 20:34:56 +0200 Subject: [PATCH 045/250] Update data/templates/metronome/metronome.cfg.lua Conform configuration file to latest v3.11 release file, Metronome v3.11.x is best package or Metronome might throw a few (non-fatal) errors on startup --- data/templates/metronome/metronome.cfg.lua | 149 ++++++++------------- 1 file changed, 55 insertions(+), 94 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 012f427ef..c00fcc398 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -9,16 +9,63 @@ -- A table is a list of values, except each value has a name. An -- example would be: -- --- ssl = { key = "keyfile.key", certificate = "certificate.crt" } +-- ssl = { key = "keyfile.key", certificate = "certificate.cert" } -- -- Tip: You can check that the syntax of this file is correct when you have finished -- by running: luac -p metronome.cfg.lua -- If there are any errors, it will let you know what and where they are, otherwise it -- will keep quiet. ----------- Server-wide settings ---------- --- Settings in this section apply to the whole server and are the default settings --- for any virtual hosts +-- Global settings go in this section + +-- This is the list of modules Metronome will load on startup. +-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. + +modules_enabled = { + -- Generally required + "roster"; -- Allow users to have a roster. Recommended. + "saslauth"; -- Authentication for clients. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + "disco"; -- Service discovery + + -- Not essential, but recommended + "private"; -- Private XML storage (for room bookmarks, etc.) + "vcard"; -- Allow users to set vCards + "pep"; -- Allows setting of mood, tune, etc. + "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. + "bidi"; -- Enables Bidirectional Server-to-Server Streams. + + -- Nice to have + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "register"; -- Allow users to register on this server using a client and change passwords + "stream_management"; -- Allows clients and servers to use Stream Management + "stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT + "message_carbons"; -- Allows clients to enable carbon copies of messages + "mam"; -- Enable server-side message archives using Message Archive Management + "push"; -- Enable Push Notifications via PubSub using XEP-0357 + "lastactivity"; -- Enables clients to know the last presence status of an user + "adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc + "admin_adhoc"; -- administration adhoc commands + "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage + "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. + + -- Other specific functionality + --"admin_telnet"; -- administration console, telnet to port 5582 + --"admin_web"; -- administration web interface + "bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP" + --"compression"; -- Allow clients to enable Stream Compression + --"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages + --"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features + --"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands) + --"server_presence"; -- Enables Server Buddies extension support + --"service_directory"; -- Enables Service Directories extension support + --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features + --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification + "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" +}; -- Server PID pidfile = "/var/run/metronome/metronome.pid" @@ -33,65 +80,6 @@ http_interfaces = { "127.0.0.1", "::1" } -- Enable IPv6 use_ipv6 = true --- This is the list of modules Metronome will load on startup. --- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. -modules_enabled = { - - -- Generally required - "roster"; -- Allow users to have a roster. Recommended ;) - "saslauth"; -- Authentication for clients and servers. Recommended if you want to log in. - "tls"; -- Add support for secure TLS on c2s/s2s connections - "dialback"; -- s2s dialback support - "disco"; -- Service discovery - --"discoitems"; -- Service discovery items - --"extdisco"; -- External Service Discovery - - -- Not essential, but recommended - "private"; -- Private XML storage (for room bookmarks, etc.) - "vcard"; -- Allow users to set vCards - "privacy"; -- Support privacy lists - - -- These are commented by default as they have a performance impact - --"compression"; -- Stream compression (Debian: requires lua-zlib module to work) - - -- Nice to have - "version"; -- Replies to server version requests - "uptime"; -- Report how long server has been running - "time"; -- Let others know the time here on this server - "ping"; -- Replies to XMPP pings with pongs - "pep"; -- Enables users to publish their mood, activity, playing music and more - "message_carbons"; -- Allow clients to keep in sync with messages send on other resources - "register"; -- Allow users to register on this server using a client and change passwords - "adhoc"; -- Support for "ad-hoc commands" that can be executed with an XMPP client - - -- Admin interfaces - "admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands - "admin_telnet"; -- Opens telnet console interface on localhost port 5582 - - -- HTTP modules - "bosh"; -- Enable BOSH clients, aka "Jabber over HTTP" - --"websockets"; -- Enable WebSocket clients - --"http_files"; -- Serve static files from a directory over HTTP - - -- Other specific functionality --- "bidi"; -- Bidirectional Streams for S2S connections --- "stream_management"; -- Stream Management support - --"groups"; -- Shared roster support - --"announce"; -- Send announcement to all online users - --"welcome"; -- Welcome users who register accounts - --"watchregistrations"; -- Alert admins of registrations - --"motd"; -- Send a message to users when they log in - "mam"; -- Nice archive management - --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. - "offline"; -- Store offline messages - "c2s"; -- Handle client connections - "s2s"; -- Handle server-to-server connections - - -- Debian: do not remove this module, or you lose syslog - -- support - "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. -}; - -- Discovery items disco_items = { { "muc.{{ main_domain }}" }, @@ -100,42 +88,16 @@ disco_items = { }; -- BOSH configuration (mod_bosh) -bosh_max_inactivity = 30 consider_bosh_secure = true cross_domain_bosh = true +-- WebSocket configuration (mod_websocket) +consider_websocket_secure = true +cross_domain_websocket = true + -- Disable account creation by default, for security allow_registration = false --- SSL/TLS configuration -ssl = { - options = { - "no_sslv2", - "no_sslv3", - "no_ticket", - "no_compression", - "cipher_server_preference" - }; -} - --- Force clients to use encrypted connections? This option will --- prevent clients from authenticating unless they are using encryption. -c2s_require_encryption = true - --- Force servers to use encrypted connections? This option will --- prevent servers from connecting unless they are using encryption. -s2s_require_encryption = true - --- Allow servers to use an unauthenticated encryption channel -s2s_allow_encryption = true - -allow_unencrypted_plain_auth = false; - -s2s_secure = true -s2s_secure_auth = false - ---anonymous_login = false - -- Use LDAP storage backend for all stores storage = "ldap" @@ -147,7 +109,6 @@ log = { -- "*console"; -- Log to the console, useful for debugging with daemonize=false } - ------ Components ------ -- You can specify components to add hosts that provide special services, -- like multi-user conferences, and transports. From a19d914485bf6800e5b1ebdde5a6843a55b82d58 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sat, 13 Oct 2018 20:38:10 +0200 Subject: [PATCH 046/250] Update data/templates/metronome/metronome.cfg.lua Finish updating --- data/templates/metronome/metronome.cfg.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index c00fcc398..6652e995c 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -124,17 +124,13 @@ Component "muc.{{ main_domain }}" "muc" modules_enabled = { "muc_limits"; "muc_log"; + "muc_log_mam"; "muc_log_http"; } muc_event_rate = 0.5 muc_burst_factor = 10 - muc_log_http_config = { - url_base = "logs"; - theme = "metronome"; - } - ---Set up a PubSub server Component "pubsub.{{ main_domain }}" "pubsub" name = "{{ main_domain }} Publish/Subscribe" From 4cff7614817e67f6d0a53bdf92d52d8c77ad2850 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sat, 13 Oct 2018 20:44:37 +0200 Subject: [PATCH 047/250] Update data/templates/metronome/metronome.cfg.lua Add MUC avatars support --- data/templates/metronome/metronome.cfg.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 6652e995c..961adb1b6 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -126,6 +126,7 @@ Component "muc.{{ main_domain }}" "muc" "muc_log"; "muc_log_mam"; "muc_log_http"; + "muc_vcard"; } muc_event_rate = 0.5 From 19120cfa60c02303eb7a0632ffb10f0ad5c48b37 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Sat, 13 Oct 2018 20:45:44 +0200 Subject: [PATCH 048/250] Update data/templates/metronome/metronome.cfg.lua Fix whitespaces --- data/templates/metronome/metronome.cfg.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 961adb1b6..81f07023d 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -122,10 +122,10 @@ Component "muc.{{ main_domain }}" "muc" name = "{{ main_domain }} Chatrooms" modules_enabled = { - "muc_limits"; - "muc_log"; + "muc_limits"; + "muc_log"; "muc_log_mam"; - "muc_log_http"; + "muc_log_http"; "muc_vcard"; } From 802c71b0e9b076d01646c4839791681eeeee323f Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 17 Oct 2018 20:47:35 +0200 Subject: [PATCH 049/250] Update data/templates/metronome/metronome.cfg.lua Add HTTP Upload service (moul's request), and fix indenting --- data/templates/metronome/metronome.cfg.lua | 120 +++++++++++---------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 81f07023d..7b86f6050 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -22,49 +22,49 @@ -- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too. modules_enabled = { - -- Generally required - "roster"; -- Allow users to have a roster. Recommended. - "saslauth"; -- Authentication for clients. Recommended if you want to log in. - "tls"; -- Add support for secure TLS on c2s/s2s connections - "disco"; -- Service discovery + -- Generally required + "roster"; -- Allow users to have a roster. Recommended. + "saslauth"; -- Authentication for clients. Recommended if you want to log in. + "tls"; -- Add support for secure TLS on c2s/s2s connections + "disco"; -- Service discovery - -- Not essential, but recommended - "private"; -- Private XML storage (for room bookmarks, etc.) - "vcard"; -- Allow users to set vCards - "pep"; -- Allows setting of mood, tune, etc. - "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. - "bidi"; -- Enables Bidirectional Server-to-Server Streams. + -- Not essential, but recommended + "private"; -- Private XML storage (for room bookmarks, etc.) + "vcard"; -- Allow users to set vCards + "pep"; -- Allows setting of mood, tune, etc. + "posix"; -- POSIX functionality, sends server to background, enables syslog, etc. + "bidi"; -- Enables Bidirectional Server-to-Server Streams. - -- Nice to have - "version"; -- Replies to server version requests - "uptime"; -- Report how long server has been running - "time"; -- Let others know the time here on this server - "ping"; -- Replies to XMPP pings with pongs - "register"; -- Allow users to register on this server using a client and change passwords - "stream_management"; -- Allows clients and servers to use Stream Management - "stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT - "message_carbons"; -- Allows clients to enable carbon copies of messages - "mam"; -- Enable server-side message archives using Message Archive Management - "push"; -- Enable Push Notifications via PubSub using XEP-0357 - "lastactivity"; -- Enables clients to know the last presence status of an user - "adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc - "admin_adhoc"; -- administration adhoc commands - "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage - "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. + -- Nice to have + "version"; -- Replies to server version requests + "uptime"; -- Report how long server has been running + "time"; -- Let others know the time here on this server + "ping"; -- Replies to XMPP pings with pongs + "register"; -- Allow users to register on this server using a client and change passwords + "stream_management"; -- Allows clients and servers to use Stream Management + "stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT + "message_carbons"; -- Allows clients to enable carbon copies of messages + "mam"; -- Enable server-side message archives using Message Archive Management + "push"; -- Enable Push Notifications via PubSub using XEP-0357 + "lastactivity"; -- Enables clients to know the last presence status of an user + "adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc + "admin_adhoc"; -- administration adhoc commands + "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage + "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. - -- Other specific functionality - --"admin_telnet"; -- administration console, telnet to port 5582 - --"admin_web"; -- administration web interface - "bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP" - --"compression"; -- Allow clients to enable Stream Compression - --"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages - --"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features - --"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands) - --"server_presence"; -- Enables Server Buddies extension support - --"service_directory"; -- Enables Service Directories extension support - --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features - --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification - "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" + -- Other specific functionality + --"admin_telnet"; -- administration console, telnet to port 5582 + --"admin_web"; -- administration web interface + "bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP" + --"compression"; -- Allow clients to enable Stream Compression + --"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages + --"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features + --"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands) + --"server_presence"; -- Enables Server Buddies extension support + --"service_directory"; -- Enables Service Directories extension support + --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features + --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification + "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" }; -- Server PID @@ -82,9 +82,10 @@ use_ipv6 = true -- Discovery items disco_items = { - { "muc.{{ main_domain }}" }, - { "pubsub.{{ main_domain }}" }, - { "vjud.{{ main_domain }}" } + { "muc.{{ main_domain }}" }, + { "pubsub.{{ main_domain }}" }, + { "upload.{{ main_domain }}" }, + { "vjud.{{ main_domain }}" } }; -- BOSH configuration (mod_bosh) @@ -103,10 +104,10 @@ storage = "ldap" -- Logging configuration log = { - info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging - error = "/var/log/metronome/metronome.err"; - -- "*syslog"; -- Uncomment this for logging to syslog - -- "*console"; -- Log to the console, useful for debugging with daemonize=false + info = "/var/log/metronome/metronome.log"; -- Change 'info' to 'debug' for verbose logging + error = "/var/log/metronome/metronome.err"; + -- "*syslog"; -- Uncomment this for logging to syslog + -- "*console"; -- Log to the console, useful for debugging with daemonize=false } ------ Components ------ @@ -115,13 +116,13 @@ log = { ---Set up a local BOSH service Component "localhost" "http" - modules_enabled = { "bosh" } + modules_enabled = { "bosh" } ---Set up a MUC (multi-user chat) room server Component "muc.{{ main_domain }}" "muc" - name = "{{ main_domain }} Chatrooms" + name = "{{ main_domain }} Chatrooms" - modules_enabled = { + modules_enabled = { "muc_limits"; "muc_log"; "muc_log_mam"; @@ -129,18 +130,26 @@ Component "muc.{{ main_domain }}" "muc" "muc_vcard"; } - muc_event_rate = 0.5 - muc_burst_factor = 10 + muc_event_rate = 0.5 + muc_burst_factor = 10 ---Set up a PubSub server Component "pubsub.{{ main_domain }}" "pubsub" - name = "{{ main_domain }} Publish/Subscribe" + name = "{{ main_domain }} Publish/Subscribe" + + unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server) + +---Set up a HTTP Upload service +Component "upload.{{ main_domain }}" "http_upload" + name = "{{ main_domain }} Sharing Service" + + http_file_size_limit = 6*1024*1024 + http_file_quota = 60*1024*1024 - unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server) ---Set up a VJUD service Component "vjud.{{ main_domain }}" "vjud" - ud_disco_name = "{{ main_domain }} User Directory" + ud_disco_name = "{{ main_domain }} User Directory" ----------- Virtual hosts ----------- @@ -148,4 +157,3 @@ Component "vjud.{{ main_domain }}" "vjud" -- Settings under each VirtualHost entry apply *only* to that host. Include "conf.d/*.cfg.lua" - From aa670058bdeda7067608c45a76e34aa1a73327d0 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 17 Oct 2018 20:59:56 +0200 Subject: [PATCH 050/250] Update data/templates/metronome/metronome.cfg.lua Add privacy lists and simple blocking command between loaded modules --- data/templates/metronome/metronome.cfg.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 7b86f6050..0640ef9d5 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -51,6 +51,7 @@ modules_enabled = { "admin_adhoc"; -- administration adhoc commands "bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage "sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs. + "privacy"; -- Add privacy lists and simple blocking command support -- Other specific functionality --"admin_telnet"; -- administration console, telnet to port 5582 From 3471eb728d001d255f3d5d725cd069b7a6eb6642 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Wed, 17 Oct 2018 22:24:21 +0200 Subject: [PATCH 051/250] Update data/templates/metronome/metronome.cfg.lua Comment websocket as lua-bitop is not installed by default --- data/templates/metronome/metronome.cfg.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index 0640ef9d5..ca6930f9f 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -65,7 +65,7 @@ modules_enabled = { --"service_directory"; -- Enables Service Directories extension support --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification - "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" + --"websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" }; -- Server PID @@ -94,8 +94,8 @@ consider_bosh_secure = true cross_domain_bosh = true -- WebSocket configuration (mod_websocket) -consider_websocket_secure = true -cross_domain_websocket = true +--consider_websocket_secure = true +--cross_domain_websocket = true -- Disable account creation by default, for security allow_registration = false From 837d0ccd83f518e33985565d45805bc000ce2480 Mon Sep 17 00:00:00 2001 From: Marco Cirillo Date: Thu, 18 Oct 2018 00:14:01 +0200 Subject: [PATCH 052/250] Revert "Update data/templates/metronome/metronome.cfg.lua" This reverts commit 3471eb728d001d255f3d5d725cd069b7a6eb6642. --- data/templates/metronome/metronome.cfg.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/templates/metronome/metronome.cfg.lua b/data/templates/metronome/metronome.cfg.lua index ca6930f9f..0640ef9d5 100644 --- a/data/templates/metronome/metronome.cfg.lua +++ b/data/templates/metronome/metronome.cfg.lua @@ -65,7 +65,7 @@ modules_enabled = { --"service_directory"; -- Enables Service Directories extension support --"public_service"; -- Enables Server vCard support for public services in directories and advertises in features --"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification - --"websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" + "websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets" }; -- Server PID @@ -94,8 +94,8 @@ consider_bosh_secure = true cross_domain_bosh = true -- WebSocket configuration (mod_websocket) ---consider_websocket_secure = true ---cross_domain_websocket = true +consider_websocket_secure = true +cross_domain_websocket = true -- Disable account creation by default, for security allow_registration = false From 428a29936f63089dfa827de471101f899e76ac29 Mon Sep 17 00:00:00 2001 From: Josue-T Date: Fri, 19 Oct 2018 16:08:28 +0200 Subject: [PATCH 053/250] Fix http2 with curl (#547) --- data/helpers.d/utils | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index 07b8a6d96..eef9f2a8e 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -253,6 +253,9 @@ ynh_local_curl () { # Add --data arg and remove the last character, which is an unecessary '&' POST_data="--data ${POST_data::-1}" fi + + # Wait untils nginx has fully reloaded (avoid curl fail with http2) + sleep 2 # Curl the URL curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url" From 5566c404ff7282a9ca62ce0907e52d4a2f3061da Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 20 Oct 2018 12:16:38 +0200 Subject: [PATCH 054/250] [fix] Allow - between two name --- data/actionsmap/yunohost.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ec4a6bf08..bb51c6f7f 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -105,7 +105,7 @@ user: ask: ask_lastname required: True pattern: &pattern_lastname - - !!str ^([^\W\d_]{2,30}[ ,.']{0,3})+$ + - !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$ - "pattern_lastname" -m: full: --mail From 6883784fdb99b7122bfe9d60ba620408d1d0018d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Oct 2018 15:45:41 +0000 Subject: [PATCH 055/250] Using dyndns host for dns resolution instead of hardcoded ip --- 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 e9542ed95..bb6a626fc 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -227,8 +227,8 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, ] - old_ipv4 = check_output("dig @91.224.148.92 +short %s" % domain).strip() - old_ipv6 = check_output("dig @91.224.148.92 +short aaaa %s" % domain).strip() + old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() + old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() # Get current IPv4 and IPv6 ipv4_ = get_public_ip() From d42ac39c11bd8045ff693ba4d69d92afe5f60522 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Oct 2018 15:55:20 +0000 Subject: [PATCH 056/250] Fallback to 'None' if no ip was resolved --- 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 bb6a626fc..5edf77ccd 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -227,8 +227,8 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, ] - old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() - old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() + old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() or None + old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() or None # Get current IPv4 and IPv6 ipv4_ = get_public_ip() From d1d660b2cf373440937bda5919a89b5092ee6f51 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Oct 2018 18:08:56 +0200 Subject: [PATCH 057/250] Those files don't exist anymoar --- src/yunohost/dyndns.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 5edf77ccd..a1e6cd60a 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -292,8 +292,6 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None, command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] subprocess.check_call(command) except subprocess.CalledProcessError: - rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent) - rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent) raise MoulinetteError(errno.EPERM, m18n.n('dyndns_ip_update_failed')) From e150c41da23d21ba72c95786db2b06d7b4dec5d7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 24 Oct 2018 18:27:09 +0000 Subject: [PATCH 058/250] Simplify method to fetch admin password hash --- .../data_migrations/0006_migrate_pwd.py | 90 ++----------------- 1 file changed, 9 insertions(+), 81 deletions(-) diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_migrate_pwd.py index 93783d662..4aad37b2e 100644 --- a/src/yunohost/data_migrations/0006_migrate_pwd.py +++ b/src/yunohost/data_migrations/0006_migrate_pwd.py @@ -7,10 +7,9 @@ import subprocess from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from moulinette.utils.process import run_commands +from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import append_to_file from moulinette.authenticators.ldap import Authenticator - from yunohost.tools import Migration logger = getActionLogger('yunohost.migration') @@ -31,88 +30,17 @@ class MyMigration(Migration): def _get_admin_hash(self): """ - Ask for admin hash the ldap db - Note: to do that like we don't know the admin password we add a second - password + Fetch the admin hash from the LDAP db using slapcat """ - logger.debug('Generate a random temporary password') - tmp_password = ''.join(random.choice(string.ascii_letters + - string.digits) for i in range(12)) - - # Generate a random temporary password (won't be valid after this - # script ends !) and hash it - logger.debug('Hash temporary password') - tmp_hash = subprocess.check_output(["slappasswd", "-h", "{SSHA}","-s", - tmp_password]) - - try: - logger.debug('Stop slapd and backup its conf') - run_commands([ - # Stop slapd service... - 'systemctl stop slapd', - - # Backup slapd.conf (to be restored at the end of script) - 'cp /etc/ldap/slapd.conf /root/slapd.conf.bkp' - ]) - - logger.debug('Add password to the conf') - # Append lines to slapd.conf to manually define root password hash - append_to_file("/etc/ldap/slapd.conf", 'rootdn "cn=admin,dc=yunohost,dc=org"') - append_to_file("/etc/ldap/slapd.conf", "\n") - append_to_file("/etc/ldap/slapd.conf", 'rootpw ' + tmp_hash) - - logger.debug('Start slapd with new password') - run_commands([ - # Test conf (might not be entirely necessary though :P) - 'slaptest -Q -u -f /etc/ldap/slapd.conf', - - # Regenerate slapd.d directory - 'rm -Rf /etc/ldap/slapd.d', - 'mkdir /etc/ldap/slapd.d', - 'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1', - - # Set permissions to slapd.d - 'chown -R openldap:openldap /etc/ldap/slapd.d/', - - # Restore slapd.conf - 'mv /root/slapd.conf.bkp /etc/ldap/slapd.conf', - - # Restart slapd service - 'service slapd start' - ]) - - logger.debug('Authenticate on ldap') - auth = Authenticator('default', 'ldap://localhost:389', - 'dc=yunohost,dc=org', 'cn=admin') - auth.authenticate( tmp_password) - logger.debug('Ask for the admin hash') - admin_hash = auth.search('cn=admin,dc=yunohost,dc=org', 'cn=admin', - ['userPassword'])[0]['userPassword'][0] - admin_hash = admin_hash.replace('{CRYPT}', '') - finally: - logger.debug('Remove tmp_password from ldap db') - # Remove tmp_password from ldap db - run_commands([ - - # Stop slapd service - 'service slapd stop || true', - - 'if [ -f /root/slapd.conf.bkp ]; then mv /root/slapd.conf.bkp /etc/ldap/slapd.conf; fi', - - # Regenerate slapd.d directory - 'rm -Rf /etc/ldap/slapd.d', - 'mkdir /etc/ldap/slapd.d', - 'slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1', - - # Set permissions to slapd.d - 'chown -R openldap:openldap /etc/ldap/slapd.d/', - - # Restart slapd service - 'service slapd start' - ]) + admin_hash = check_output("slapcat \ + | grep 'dn: cn=admin,dc=yunohost,dc=org' -A20 \ + | grep userPassword -A2 \ + | tr -d '\n ' \ + | tr ':' ' ' \ + | awk '{print $2}' \ + | base64 -d") return admin_hash - def _replace_root_hash(self, new_hash): hash_root = spwd.getspnam("root").sp_pwd From 556f33f15fec1e86efba8457c4f92700a3c71cfc Mon Sep 17 00:00:00 2001 From: frju365 Date: Wed, 24 Oct 2018 21:04:01 +0200 Subject: [PATCH 059/250] [fix] Add a proper conf for LDAP (Issue 1218) (#554) * Update ldap.conf * [fix] Port --- data/templates/slapd/ldap.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/templates/slapd/ldap.conf b/data/templates/slapd/ldap.conf index 406b5c175..09aeb8b4f 100644 --- a/data/templates/slapd/ldap.conf +++ b/data/templates/slapd/ldap.conf @@ -5,8 +5,8 @@ # See ldap.conf(5) for details # This file should be world readable but not world writable. -#BASE dc=example,dc=com -#URI ldap://ldap.example.com ldap://ldap-master.example.com:666 +BASE dc=yunohost,dc=org +URI ldap://localhost:389 #SIZELIMIT 12 #TIMELIMIT 15 From 1c5b93f5327db9617b76881dd4a86eea715a0d08 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Wed, 24 Oct 2018 21:15:25 +0200 Subject: [PATCH 060/250] [fix] Set random serial number for CA (#557) [fix] Use random serial number for CA --- data/templates/ssl/openssl.cnf | 2 +- src/yunohost/tools.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/data/templates/ssl/openssl.cnf b/data/templates/ssl/openssl.cnf index fa5d19fa3..ac8c422e3 100644 --- a/data/templates/ssl/openssl.cnf +++ b/data/templates/ssl/openssl.cnf @@ -43,7 +43,7 @@ unique_subject = no # Set to 'no' to allow creation of new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/ca/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number +#serial = $dir/serial # The current serial number #crlnumber = $dir/crlnumber # the current crl number # must be commented out to leave a V1 CRL crl = $dir/crl.pem # The current CRL diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f9ee14994..ccf489a92 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -358,7 +358,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False): service_regen_conf(['ssl'], force=True) ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' commands = [ - 'echo "01" > %s/serial' % ssl_dir, 'rm %s/index.txt' % ssl_dir, 'touch %s/index.txt' % ssl_dir, 'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir), From 8691017b46b9d79d204c182e412522d486486dc0 Mon Sep 17 00:00:00 2001 From: Gabriel Corona Date: Sat, 13 Oct 2018 13:39:50 +0200 Subject: [PATCH 061/250] Pass Host header to YunoHost API This is useful to validate Origin/Referer headers in order to prevent CSRF. --- data/templates/nginx/plain/yunohost_api.conf.inc | 1 + 1 file changed, 1 insertion(+) diff --git a/data/templates/nginx/plain/yunohost_api.conf.inc b/data/templates/nginx/plain/yunohost_api.conf.inc index 35cd0090c..4d7887cc6 100644 --- a/data/templates/nginx/plain/yunohost_api.conf.inc +++ b/data/templates/nginx/plain/yunohost_api.conf.inc @@ -4,6 +4,7 @@ location /yunohost/api/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; # Custom 502 error page error_page 502 /yunohost/api/error/502; From a16b6f08f503363eda666c5906dd7fda5388f064 Mon Sep 17 00:00:00 2001 From: Julien Osman <33510663+jershon@users.noreply.github.com> Date: Wed, 24 Oct 2018 21:57:56 +0200 Subject: [PATCH 062/250] Add a sorting lambda function when sorting the backups list (#562) * Add a sorting lambda function when sorting the backups list * Update backup.py --- 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 0749312d3..6ef9a0554 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2187,7 +2187,7 @@ def backup_list(with_info=False, human_readable=False): except ValueError: continue result.append(name) - result.sort() + result.sort(key=lambda x: os.path.getctime(os.path.join(ARCHIVES_PATH, x+".tar.gz"))) if result and with_info: d = OrderedDict() From 7975d73cadbc60c320333fba78aca1e5e31dd23b Mon Sep 17 00:00:00 2001 From: ljf Date: Thu, 25 Oct 2018 14:56:12 +0200 Subject: [PATCH 063/250] [enh] Manual migration if not weak password --- locales/en.json | 1 + .../data_migrations/0006_migrate_pwd.py | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/locales/en.json b/locales/en.json index ab5569cfc..cb9d53560 100644 --- a/locales/en.json +++ b/locales/en.json @@ -287,6 +287,7 @@ "migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", + "migration_0006_root_admin_sync_warning": "Yunohost now expect admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by your root password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_migrate_pwd.py index 93783d662..dec795ae3 100644 --- a/src/yunohost/data_migrations/0006_migrate_pwd.py +++ b/src/yunohost/data_migrations/0006_migrate_pwd.py @@ -21,14 +21,26 @@ class MyMigration(Migration): def migrate(self): - if self._is_root_pwd_listed(SMALL_PWD_LIST): - new_hash = self._get_admin_hash() - self._replace_root_hash(new_hash) + new_hash = self._get_admin_hash() + self._replace_root_hash(new_hash) def backward(self): - pass + @property + def mode(self): + if self._is_root_pwd_listed(SMALL_PWD_LIST): + return "auto" + + return "manual" + + @property + def disclaimer(self): + if self._is_root_pwd_listed(SMALL_PWD_LIST): + return None + + return m18n.n("migration_0006_root_admin_sync_warning") + def _get_admin_hash(self): """ Ask for admin hash the ldap db From 5e91b5c780424434d8ecd99d05206c9421fb9cc9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 14:23:40 +0000 Subject: [PATCH 064/250] Smol fix to remove {CRYPT} in front of hash --- src/yunohost/data_migrations/0006_migrate_pwd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_migrate_pwd.py index c80304985..6413086ae 100644 --- a/src/yunohost/data_migrations/0006_migrate_pwd.py +++ b/src/yunohost/data_migrations/0006_migrate_pwd.py @@ -50,7 +50,8 @@ class MyMigration(Migration): | tr -d '\n ' \ | tr ':' ' ' \ | awk '{print $2}' \ - | base64 -d") + | base64 -d \ + | sed 's/{CRYPT}//g'") return admin_hash def _replace_root_hash(self, new_hash): From ae3bf28481ab9d9edbef483da071a6c223521fd2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 14:38:49 +0000 Subject: [PATCH 065/250] Misc naming, description and comments improvements --- locales/en.json | 3 ++- ...wd.py => 0006_sync_admin_and_root_passwords.py} | 14 +++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) rename src/yunohost/data_migrations/{0006_migrate_pwd.py => 0006_sync_admin_and_root_passwords.py} (80%) diff --git a/locales/en.json b/locales/en.json index cb9d53560..77d40c17a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -271,6 +271,7 @@ "migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0", "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", + "migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", @@ -287,7 +288,7 @@ "migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", - "migration_0006_root_admin_sync_warning": "Yunohost now expect admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by your root password.", + "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0006_migrate_pwd.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py similarity index 80% rename from src/yunohost/data_migrations/0006_migrate_pwd.py rename to src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 6413086ae..9b74952cc 100644 --- a/src/yunohost/data_migrations/0006_migrate_pwd.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -16,7 +16,7 @@ logger = getActionLogger('yunohost.migration') SMALL_PWD_LIST = ["yunohost", "olinux"] class MyMigration(Migration): - "Migrate password" + "Synchronize admin and root passwords" def migrate(self): @@ -28,17 +28,21 @@ class MyMigration(Migration): @property def mode(self): - if self._is_root_pwd_listed(SMALL_PWD_LIST): - return "auto" - return "manual" + # If the root password is still a "default" value, + # then this is an emergency and migration shall + # be applied automatically + # + # Otherwise, as playing with root password is touchy, + # we set this as a manual migration. + return "auto" if self._is_root_pwd_listed(SMALL_PWD_LIST) else "manual" @property def disclaimer(self): if self._is_root_pwd_listed(SMALL_PWD_LIST): return None - return m18n.n("migration_0006_root_admin_sync_warning") + return m18n.n("migration_0006_disclaimer") def _get_admin_hash(self): """ From feb45578db235e62985ab7dc4eecdd5dfaf06890 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 15:13:05 +0000 Subject: [PATCH 066/250] Improve wording --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 77d40c17a..ef762fa41 100644 --- a/locales/en.json +++ b/locales/en.json @@ -366,7 +366,7 @@ "restore_running_app_script": "Running restore script of app '{app:s}'...", "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", - "root_password_desynchronized": "Password of the user admin has been changed, but Root password has not been synchronized with your new admin password !", + "root_password_desynchronized": "The admin password has been changed, but YunoHost was unable to propagate this on the root password !", "server_shutdown": "The server will shutdown", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_reboot": "The server will reboot", From 848a6b6088a6163767711eb844182eca72456e7c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 15:18:44 +0000 Subject: [PATCH 067/250] Gotta remove that '{CRYPT}' thing here too --- 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 7221e6804..9bd4c9e88 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -145,7 +145,7 @@ def tools_adminpw(auth, new_password): with open('/etc/shadow', 'w') as after_file: after_file.write(before.replace("root:" + hash_root, - "root:" + new_hash)) + "root:" + new_hash.replace('{CRYPT}', ''))) except IOError as e: logger.warning(m18n.n('root_password_desynchronized')) return From 3f3069d0809af148e3b9b4757d6755f3b6c49bf0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 15:35:46 +0000 Subject: [PATCH 068/250] Add info message after the migration is complete --- locales/en.json | 1 + .../data_migrations/0006_sync_admin_and_root_passwords.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/locales/en.json b/locales/en.json index ef762fa41..703db0b7e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -289,6 +289,7 @@ "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", + "migration_0006_done": "Your root password have been replaced by your admin password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 9b74952cc..0bcb3cd3a 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -23,6 +23,8 @@ class MyMigration(Migration): new_hash = self._get_admin_hash() self._replace_root_hash(new_hash) + logger.info(m18n.n("migration_0006_done")) + def backward(self): pass From a94e69f760f37aa80b2d52c44d444321d7b5ae8e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 18:09:11 +0200 Subject: [PATCH 069/250] Make the SMALL_PWD_LIST consistent with cracklib's PR --- .../data_migrations/0006_sync_admin_and_root_passwords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 0bcb3cd3a..366363f22 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -13,7 +13,7 @@ from moulinette.authenticators.ldap import Authenticator from yunohost.tools import Migration logger = getActionLogger('yunohost.migration') -SMALL_PWD_LIST = ["yunohost", "olinux"] +SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] class MyMigration(Migration): "Synchronize admin and root passwords" From aac9b78c0342e1b566a5d21211d7bffef4d94b51 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 16:51:40 +0000 Subject: [PATCH 070/250] We aint using that online thing :| --- src/yunohost/utils/password.py | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 306be6103..e3e99075d 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -30,7 +30,6 @@ PWDDICT_PATH = '/usr/local/share/dict/cracklib/' SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] PWD_LIST_FILE = '100000-most-used' -ACTIVATE_ONLINE_PWNED_LIST = False class PasswordValidator(object): """ @@ -55,7 +54,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") - self.strength = self.compute(password, ACTIVATE_ONLINE_PWNED_LIST) + self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: return ("error", "password_listed_" + str(self.validation_strength)) @@ -66,7 +65,7 @@ class PasswordValidator(object): return ("warning", 'password_advice') return ("success", "") - def compute(self, password, online=False): + def compute(self, password): # Indicators length = len(password) digits = 0 @@ -92,10 +91,6 @@ class PasswordValidator(object): # Check big list size_list = 100000 if unlisted > 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE): - unlisted = size_list if online else 320000000 - - # Check online big list - if unlisted > size_list and online and not self.is_in_online_pwned_list(password): unlisted = 320000000 self.listed = unlisted < 320000000 @@ -112,29 +107,6 @@ class PasswordValidator(object): strength = i + 1 return strength - def is_in_online_pwned_list(self, password, silent=True): - """ - Check if a password is in the list of breached passwords from - haveibeenpwned.com - """ - - from hashlib import sha1 - import requests - hash = sha1(password).hexdigest() - range = hash[:5] - needle = (hash[5:].upper()) - - try: - hash_list =requests.get('https://api.pwnedpasswords.com/range/' + - range, timeout=30) - except e: - if not silent: - raise - else: - if hash_list.find(needle) != -1: - return True - return False - def is_in_cracklib_list(self, password, pwd_dict): try: cracklib.VeryFascistCheck(password, None, From 08e1d929c13a5582b76b84b141c32917104004b3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:10:54 +0000 Subject: [PATCH 071/250] Simplify the 'listed' check --- src/yunohost/utils/password.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e3e99075d..6d3ee91b5 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -38,10 +38,10 @@ class PasswordValidator(object): # Unlisted, length, digits, lowers, uppers, others strength_lvl = [ - [100000, 6, 0, 0, 0, 0], - [100000, 8, 1, 1, 1, 0], - [320000000, 8, 1, 1, 1, 1], - [320000000, 12, 1, 1, 1, 1], + [6, 0, 0, 0, 0], + [8, 1, 1, 1, 0], + [8, 1, 1, 1, 1], + [12, 1, 1, 1, 1], ] def __init__(self, validation_strength): @@ -54,6 +54,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") + self.listed = password in SMALL_PWD_LIST or self.is_in_cractklib_list(password, PWD_LIST_FILE) self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: @@ -83,26 +84,15 @@ class PasswordValidator(object): else: others = others + 1 - # Check small list - unlisted = 0 - if password not in SMALL_PWD_LIST: - unlisted = len(SMALL_PWD_LIST) + return self.compare(length, digits, lowers, uppers, others) - # Check big list - size_list = 100000 - if unlisted > 0 and not self.is_in_cracklib_list(password, PWD_LIST_FILE): - unlisted = 320000000 - - self.listed = unlisted < 320000000 - return self.compare(unlisted, length, digits, lowers, uppers, others) - - def compare(self, unlisted, length, digits, lowers, uppers, others): + def compare(self, length, digits, lowers, uppers, others): strength = 0 for i, config in enumerate(self.strength_lvl): - if unlisted < config[0] or length < config[1] \ - or digits < config[2] or lowers < config[3] \ - or uppers < config[4] or others < config[5]: + if length < config[0] or digits < config[1] \ + or lowers < config[3] or uppers < config[4] \ + or others < config[5]: break strength = i + 1 return strength From 85d3c7df340b8791ad55b9feb1a753be02e13749 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:23:58 +0000 Subject: [PATCH 072/250] Moar cleaning --- src/yunohost/utils/password.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 6d3ee91b5..3ab6147e6 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -26,17 +26,18 @@ import cracklib import string ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz" -PWDDICT_PATH = '/usr/local/share/dict/cracklib/' SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] -PWD_LIST_FILE = '100000-most-used' + +PWDDICT_FOLDER = '/usr/local/share/dict/cracklib/' +PWDDICT_LIST = '100000-most-used' class PasswordValidator(object): """ PasswordValidator class validate password """ - # Unlisted, length, digits, lowers, uppers, others + # Length, digits, lowers, uppers, others strength_lvl = [ [6, 0, 0, 0, 0], [8, 1, 1, 1, 0], @@ -54,7 +55,7 @@ class PasswordValidator(object): if self.validation_strength <= 0: return ("success", "") - self.listed = password in SMALL_PWD_LIST or self.is_in_cractklib_list(password, PWD_LIST_FILE) + self.listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) self.strength = self.compute(password) if self.strength < self.validation_strength: if self.listed: @@ -97,10 +98,10 @@ class PasswordValidator(object): strength = i + 1 return strength - def is_in_cracklib_list(self, password, pwd_dict): + def is_in_cracklib_list(self, password): try: cracklib.VeryFascistCheck(password, None, - os.path.join(PWDDICT_PATH, pwd_dict)) + os.path.join(PWDDICT_FOLDER, PWDDICT_LIST)) except ValueError as e: # We only want the dictionnary check of cracklib, not the is_simple # test. From 2b00e072d8edb1a8857c400fd0eb5eb2dc9ea675 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 17:30:55 +0000 Subject: [PATCH 073/250] Merge Profile validator into regular validator --- src/yunohost/utils/password.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 3ab6147e6..97f2a6deb 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -45,8 +45,15 @@ class PasswordValidator(object): [12, 1, 1, 1, 1], ] - def __init__(self, validation_strength): - self.validation_strength = validation_strength + def __init__(self, profile): + self.profile = profile + import json + try: + settings = json.load(open('/etc/yunohost/settings.json', "r")) + setting_key = "security.password." + profile + ".strength" + self.validation_strength = int(settings[setting_key]) + except Exception as e: + self.validation_strength = 2 if profile == 'admin' else 1 def validate(self, password): """ @@ -109,19 +116,7 @@ class PasswordValidator(object): return True -class ProfilePasswordValidator(PasswordValidator): - def __init__(self, profile): - self.profile = profile - import json - try: - settings = json.load(open('/etc/yunohost/settings.json', "r")) - self.validation_strength = int(settings["security.password." + profile + - '.strength']) - except Exception as e: - self.validation_strength = 2 if profile == 'admin' else 1 - return - -class LoggerPasswordValidator(ProfilePasswordValidator): +class LoggerPasswordValidator(PasswordValidator): """ PasswordValidator class validate password """ @@ -153,7 +148,7 @@ if __name__ == '__main__': #print("usage: password.py PASSWORD") else: pwd = sys.argv[1] - status, msg = ProfilePasswordValidator('user').validate(pwd) + status, msg = PasswordValidator('user').validate(pwd) print(msg) sys.exit(0) From 3c5ce491c50fd321d5568fe9de69b8f3679f79d6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 18:25:53 +0000 Subject: [PATCH 074/250] Various changes to try to improve the semantic of everything @.@ --- src/yunohost/utils/password.py | 117 +++++++++++++++++++++------------ 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 97f2a6deb..840cb0847 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -21,60 +21,80 @@ import sys import os +import json import cracklib - import string -ASCII_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" -ASCII_LOWERCASE = "abcdefghijklmnopqrstuvwxyz" + SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] -PWDDICT_FOLDER = '/usr/local/share/dict/cracklib/' -PWDDICT_LIST = '100000-most-used' +MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used' + +# Length, digits, lowers, uppers, others +STRENGTH_LEVELS = [ + (6, 0, 0, 0, 0), + (8, 1, 1, 1, 0), + (8, 1, 1, 1, 1), + (12, 1, 1, 1, 1), +] + class PasswordValidator(object): - """ - PasswordValidator class validate password - """ - - # Length, digits, lowers, uppers, others - strength_lvl = [ - [6, 0, 0, 0, 0], - [8, 1, 1, 1, 0], - [8, 1, 1, 1, 1], - [12, 1, 1, 1, 1], - ] def __init__(self, profile): + """ + Initialize a password validator. + + The profile shall be either "user" or "admin" + and will correspond to a validation strength + defined via the setting "security.password..strength" + """ + self.profile = profile - import json try: + # We do this "manually" instead of using settings_get() + # 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")) setting_key = "security.password." + profile + ".strength" self.validation_strength = int(settings[setting_key]) except Exception as e: + # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 2 if profile == 'admin' else 1 - def validate(self, password): + def validation_summary(self, password): """ - Validate a password and raise error or display a warning + Check if a password is listed in the list of most used password + and if the overall strength is good enough compared to the + validation_strength defined in the constructor. + + Produces a summary-tuple comprised of a level (success, error, warning) + and a message key describing the issues found. """ if self.validation_strength <= 0: return ("success", "") - self.listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) - self.strength = self.compute(password) - if self.strength < self.validation_strength: - if self.listed: + listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) + strength_level = self.strength_level(password) + if strength_level < self.validation_strength: + if listed: return ("error", "password_listed_" + str(self.validation_strength)) else: return ("error", "password_too_simple_" + str(self.validation_strength)) - if self.strength < 3: + if strength_level < 3: return ("warning", 'password_advice') return ("success", "") - def compute(self, password): + def strength(self, password): + """ + Returns the strength of a password, defined as a tuple + containing the length of the password, the number of digits, + lowercase letters, uppercase letters, and other characters. + + For instance, "PikachuDu67" is (11, 2, 7, 2, 0) + """ # Indicators length = len(password) digits = 0 @@ -85,41 +105,52 @@ class PasswordValidator(object): for character in password: if character in string.digits: digits = digits + 1 - elif character in ASCII_UPPERCASE: + elif character in string.ascii_uppercase: uppers = uppers + 1 - elif character in ASCII_LOWERCASE: + elif character in string.ascii_lowercase: lowers = lowers + 1 else: others = others + 1 - return self.compare(length, digits, lowers, uppers, others) + return (length, digits, lowers, uppers, others) - def compare(self, length, digits, lowers, uppers, others): - strength = 0 + def strength_level(self, password): + """ + Computes the strength of a password and compares + it to the STRENGTH_LEVELS. - for i, config in enumerate(self.strength_lvl): - if length < config[0] or digits < config[1] \ - or lowers < config[3] or uppers < config[4] \ - or others < config[5]: + Returns an int corresponding to the highest STRENGTH_LEVEL + satisfied by the password. + """ + + strength = self.strength(password) + + strength_level = 0 + # Iterate over each level and its criterias + for level, level_criterias in enumerate(STRENGTH_LEVELS): + # Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0]) + # and the strength of the password (e.g. [11, 2, 7, 2, 0]) + # and compare the values 1-by-1. + # If one False is found, the password does not satisfy the level + if False in [s>=c for s, c in zip(strength, level_criterias)]: break - strength = i + 1 - return strength + # Otherwise, the strength of the password is at least of the current level. + strength_level = level + 1 + + return strength_level def is_in_cracklib_list(self, password): try: - cracklib.VeryFascistCheck(password, None, - os.path.join(PWDDICT_FOLDER, PWDDICT_LIST)) + cracklib.VeryFascistCheck(password, None, MOST_USED_PASSWORDS) except ValueError as e: # We only want the dictionnary check of cracklib, not the is_simple # test. if str(e) not in ["is too simple", "is a palindrome"]: return True + return False class LoggerPasswordValidator(PasswordValidator): - """ - PasswordValidator class validate password - """ def validate(self, password): """ @@ -135,7 +166,7 @@ class LoggerPasswordValidator(PasswordValidator): logger = logging.getLogger('yunohost.utils.password') - status, msg = super(LoggerPasswordValidator, self).validate(password) + status, msg = super(LoggerPasswordValidator, self).validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) elif status == "warning": @@ -148,7 +179,7 @@ if __name__ == '__main__': #print("usage: password.py PASSWORD") else: pwd = sys.argv[1] - status, msg = PasswordValidator('user').validate(pwd) + status, msg = PasswordValidator('user').validation_summary(pwd) print(msg) sys.exit(0) From 55256c1973e0baebc3d9e6679970f0ea64ff5d6c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 18:36:55 +0000 Subject: [PATCH 075/250] Merge LoggerPasswordValidator with PasswordValidator --- src/yunohost/utils/password.py | 59 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 840cb0847..9527c7ff6 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -63,6 +63,36 @@ class PasswordValidator(object): # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 2 if profile == 'admin' else 1 + def validate(self, password): + """ + Check the validation_summary and trigger the corresponding + exception or info message + + This method is meant to be used from inside YunoHost's code + (compared to validation_summary which is meant to be called + by ssowat) + """ + if self.validation_strength == -1: + return + + # Note that those imports are made here and can't be put + # 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. + import errno + import logging + from moulinette import m18n + from moulinette.core import MoulinetteError + from moulinette.utils.log import getActionLogger + + logger = logging.getLogger('yunohost.utils.password') + + status, msg = validation_summary(password) + if status == "error": + raise MoulinetteError(1, m18n.n(msg)) + elif status == "warning": + logger.info(m18n.n(msg)) + def validation_summary(self, password): """ Check if a password is listed in the list of most used password @@ -95,7 +125,7 @@ class PasswordValidator(object): For instance, "PikachuDu67" is (11, 2, 7, 2, 0) """ - # Indicators + length = len(password) digits = 0 uppers = 0 @@ -150,28 +180,9 @@ class PasswordValidator(object): return False -class LoggerPasswordValidator(PasswordValidator): - - def validate(self, password): - """ - Validate a password and raise error or display a warning - """ - if self.validation_strength == -1: - return - import errno - import logging - from moulinette import m18n - from moulinette.core import MoulinetteError - from moulinette.utils.log import getActionLogger - - logger = logging.getLogger('yunohost.utils.password') - - status, msg = super(LoggerPasswordValidator, self).validation_summary(password) - if status == "error": - raise MoulinetteError(1, m18n.n(msg)) - elif status == "warning": - logger.info(m18n.n(msg)) - +# 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 len(sys.argv) < 2: import getpass @@ -182,5 +193,3 @@ if __name__ == '__main__': status, msg = PasswordValidator('user').validation_summary(pwd) print(msg) sys.exit(0) - - From 167df05f56833e2e336a460121da9ac49c303a67 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:03:41 +0000 Subject: [PATCH 076/250] Not sure to understand the whole logic behind this :/ To me this should as simple as this ? --- locales/en.json | 5 +---- src/yunohost/utils/password.py | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6275250ae..d4d52c957 100644 --- a/locales/en.json +++ b/locales/en.json @@ -329,14 +329,11 @@ "packages_no_upgrade": "There is no package to upgrade", "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", + "password_listed": "This password is among the most used password in the world. Please choose something a bit more unique.", "password_too_simple_1": "Password needs to be at least 6 characters long", "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", - "password_listed_1": "This password is in a well known list. Please make it unique. Password needs to be at least 6 characters long", - "password_listed_2": "This password is in a well known list. Please make it unique. Password needs to be at least 8 characters long and contains digit, upper and lower characters", - "password_listed_3": "This password is in a well known list. Please make it unique. Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", - "password_listed_4": "This password is in a well known list. Please make it unique. Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", "password_advice": "Advice: a good password is at least 8 characters and contains digit, upper, lower and special characters", "path_removal_failed": "Unable to remove path {:s}", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 9527c7ff6..75fbf2d74 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -107,11 +107,10 @@ class PasswordValidator(object): listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) strength_level = self.strength_level(password) + if listed: + return ("error", "password_listed") if strength_level < self.validation_strength: - if listed: - return ("error", "password_listed_" + str(self.validation_strength)) - else: - return ("error", "password_too_simple_" + str(self.validation_strength)) + return ("error", "password_too_simple_%s" % self.validation_strength) if strength_level < 3: return ("warning", 'password_advice') From 914088954de2ea7791297f4110adfca1eb5ce357 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:21:15 +0000 Subject: [PATCH 077/250] Propagate interface changes everywhere the assertion is used --- src/yunohost/app.py | 4 ++-- src/yunohost/tools.py | 15 +++++++++------ src/yunohost/user.py | 8 ++++---- src/yunohost/utils/password.py | 2 ++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index db0bb1bee..3231c58ed 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2252,8 +2252,8 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): m18n.n('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')) elif arg_type == 'password': - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(arg_value) + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough('user', arg_value) args_dict[arg_name] = arg_value # END loop over action_args... diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f18bdf8c9..f58a7880f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -129,9 +129,10 @@ def tools_adminpw(auth, new_password): """ from yunohost.user import _hash_user_password - from yunohost.utils.password import LoggerPasswordValidator + from yunohost.utils.password import assert_password_is_strong_enough + + assert_password_is_strong_enough("admin", new_password) - LoggerPasswordValidator('admin').validate(new_password) try: auth.update("cn=admin", { "userPassword": _hash_user_password(new_password), @@ -152,8 +153,9 @@ def tools_validatepw(password): password """ - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(password) + + from yunohost.utils.password import assert_password_is_strong_enough + assert_password_is_strong_enough("user", password) @is_unit_operation() @@ -279,6 +281,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, password -- YunoHost admin password """ + from yunohost.utils.password import assert_password_is_strong_enough + dyndns_provider = "dyndns.yunohost.org" # Do some checks at first @@ -288,8 +292,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Check password if not force_password: - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('admin').validate(password) + assert_password_is_strong_enough("admin", password) if not ignore_dyndns: # Check if yunohost dyndns can handle the given domain diff --git a/src/yunohost/user.py b/src/yunohost/user.py index d08e39e8c..083030bd4 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -117,10 +117,10 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas from yunohost.domain import domain_list, _get_maindomain from yunohost.hook import hook_callback from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough # Ensure sufficiently complex password - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(password) + assert_password_is_strong_enough("user", password) # Validate uniqueness of username and mail in LDAP auth.validate_uniqueness({ @@ -284,6 +284,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, """ from yunohost.domain import domain_list from yunohost.app import app_ssowatconf + from yunohost.utils.password import assert_password_is_strong_enough attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop'] new_attr_dict = {} @@ -309,8 +310,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if change_password: # Ensure sufficiently complex password - from yunohost.utils.password import LoggerPasswordValidator - LoggerPasswordValidator('user').validate(change_password) + assert_password_is_strong_enough("user", password) new_attr_dict['userPassword'] = _hash_user_password(change_password) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 75fbf2d74..b1aa9050d 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -38,6 +38,8 @@ STRENGTH_LEVELS = [ (12, 1, 1, 1, 1), ] +def assert_password_is_strong_enough(profile, password): + PasswordValidator(profile).validate(password) class PasswordValidator(object): From 8a0c4509fde77775b5a4bf88c72e9944f6cdff26 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:30:18 +0000 Subject: [PATCH 078/250] Those arent used ? --- src/yunohost/settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 3178b268d..cff82cb68 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -29,12 +29,6 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" # * string # * enum (in form a python list) -# we don't store the value in default options -PWD_MODE = ["disabled", "weak", "strong"] -PWD_CHOICES = ["error", "warn_only", "disabled"] -PWD_DEFAULT_ERROR = {"type": "enum", "default": "error", - "choices": PWD_CHOICES} - DEFAULTS = OrderedDict([ ("example.bool", {"type": "bool", "default": True}), ("example.int", {"type": "int", "default": 42}), From c313084dc3f07c444d4d0cab24021e24b04dde80 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:30:34 +0000 Subject: [PATCH 079/250] Consistency with comment in settings.py --- 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 b1aa9050d..243e48a30 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -104,7 +104,7 @@ class PasswordValidator(object): Produces a summary-tuple comprised of a level (success, error, warning) and a message key describing the issues found. """ - if self.validation_strength <= 0: + if self.validation_strength < 0: return ("success", "") listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) From 2209f75985868064a2f759b327cbde7e3c09a272 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:33:36 +0000 Subject: [PATCH 080/250] Raise the level 1 length from 6 to 8 to reduce the gap with level 2 --- 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 243e48a30..6870c7043 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -32,7 +32,7 @@ MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used' # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ - (6, 0, 0, 0, 0), + (8, 0, 0, 0, 0), (8, 1, 1, 1, 0), (8, 1, 1, 1, 1), (12, 1, 1, 1, 1), From 354cd8106edc379621a6787338c28ac5c4bb5628 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:35:36 +0000 Subject: [PATCH 081/250] Misc cleaning --- src/yunohost/tools.py | 1 - src/yunohost/utils/password.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f58a7880f..c54355c36 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -54,7 +54,6 @@ from yunohost.monitor import monitor_disk, monitor_system from yunohost.utils.packages import ynh_packages_version from yunohost.utils.network import get_public_ip from yunohost.log import is_unit_operation, OperationLogger -from yunohost.settings import settings_get # FIXME this is a duplicate from apps.py APPS_SETTING_PATH = '/etc/yunohost/apps/' diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 6870c7043..e3e82207b 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -2,7 +2,7 @@ """ License - Copyright (C) 2013 YunoHost + Copyright (C) 2018 YunoHost This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published From 319602537d5bf9cbf62acdb475827e7c34e07a6a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 19:39:08 +0000 Subject: [PATCH 082/250] To me this doesnt make sense :| Either the password is accepted or it is not, but we shall not give advice to the user *after* validating the password... --- locales/en.json | 1 - src/yunohost/utils/password.py | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index d4d52c957..a25dab7da 100644 --- a/locales/en.json +++ b/locales/en.json @@ -334,7 +334,6 @@ "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", - "password_advice": "Advice: a good password is at least 8 characters and contains digit, upper, lower and special characters", "path_removal_failed": "Unable to remove path {:s}", "pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only", "pattern_domain": "Must be a valid domain name (e.g. my-domain.org)", diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index e3e82207b..1b9bde6f9 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -67,8 +67,8 @@ class PasswordValidator(object): def validate(self, password): """ - Check the validation_summary and trigger the corresponding - exception or info message + Check the validation_summary and trigger an exception + if the password does not pass tests. This method is meant to be used from inside YunoHost's code (compared to validation_summary which is meant to be called @@ -92,8 +92,6 @@ class PasswordValidator(object): status, msg = validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) - elif status == "warning": - logger.info(m18n.n(msg)) def validation_summary(self, password): """ @@ -101,7 +99,7 @@ class PasswordValidator(object): and if the overall strength is good enough compared to the validation_strength defined in the constructor. - Produces a summary-tuple comprised of a level (success, error, warning) + Produces a summary-tuple comprised of a level (succes or error) and a message key describing the issues found. """ if self.validation_strength < 0: @@ -114,8 +112,6 @@ class PasswordValidator(object): if strength_level < self.validation_strength: return ("error", "password_too_simple_%s" % self.validation_strength) - if strength_level < 3: - return ("warning", 'password_advice') return ("success", "") def strength(self, password): From 5ed1b6d36c12a476cb2f55edb61115102ad81986 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 26 Oct 2018 01:46:56 +0200 Subject: [PATCH 083/250] [fix] Number of char --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index a25dab7da..1781ac388 100644 --- a/locales/en.json +++ b/locales/en.json @@ -330,7 +330,7 @@ "packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later", "packages_upgrade_failed": "Unable to upgrade all of the packages", "password_listed": "This password is among the most used password in the world. Please choose something a bit more unique.", - "password_too_simple_1": "Password needs to be at least 6 characters long", + "password_too_simple_1": "Password needs to be at least 8 characters long", "password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters", "password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters", "password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters", From a780ebd9007c4cc3251ee71cad9c81df9177bbfb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 01:47:52 +0200 Subject: [PATCH 084/250] Number of char --- src/yunohost/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index cff82cb68..357e94d77 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -36,7 +36,7 @@ DEFAULTS = OrderedDict([ ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}), # Password Validation - # -1 disabled, 0 alert if listed, 1 6-letter, 2 normal, 3 strong, 4 strongest + # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest ("security.password.admin.strength", {"type": "int", "default": 2}), ("security.password.user.strength", {"type": "int", "default": 1}), ]) From 4268c0d04d1831c2f98d65afd4eb28d8f603b38e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 14:30:25 +0000 Subject: [PATCH 085/250] Forgot 'self' --- 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 1b9bde6f9..b064d909b 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -89,7 +89,7 @@ class PasswordValidator(object): logger = logging.getLogger('yunohost.utils.password') - status, msg = validation_summary(password) + status, msg = self.validation_summary(password) if status == "error": raise MoulinetteError(1, m18n.n(msg)) From ded9b5857527a3aef239e7712145ffc70c6a241d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 14:31:16 +0000 Subject: [PATCH 086/250] Use level 1 as default for everybody --- src/yunohost/settings.py | 2 +- src/yunohost/utils/password.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 357e94d77..d2526316e 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -37,7 +37,7 @@ 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": 2}), + ("security.password.admin.strength", {"type": "int", "default": 1}), ("security.password.user.strength", {"type": "int", "default": 1}), ]) diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index b064d909b..6b31285e7 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -63,7 +63,7 @@ class PasswordValidator(object): self.validation_strength = int(settings[setting_key]) except Exception as e: # Fallback to default value if we can't fetch settings for some reason - self.validation_strength = 2 if profile == 'admin' else 1 + self.validation_strength = 1 def validate(self, password): """ From 98c0745056c749e5c2381fe1ec7dffd5a6fa2205 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 26 Oct 2018 18:45:53 +0000 Subject: [PATCH 087/250] Add comment about good pratice for password --- data/actionsmap/yunohost.yml | 3 +++ locales/en.json | 2 ++ src/yunohost/app.py | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 6108da07b..102be300d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -125,6 +125,7 @@ user: pattern: &pattern_password - !!str ^.{3,}$ - "pattern_password" + comment: good_practices_about_user_password -q: full: --mailbox-quota help: Mailbox size quota @@ -1449,6 +1450,7 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password ### tools_validatepw() validatepw: @@ -1498,6 +1500,7 @@ tools: password: ask_new_admin_password pattern: *pattern_password required: True + comment: good_practices_about_admin_password --ignore-dyndns: help: Do not subscribe domain to a DynDNS service action: store_true diff --git a/locales/en.json b/locales/en.json index 1781ac388..8b0908523 100644 --- a/locales/en.json +++ b/locales/en.json @@ -197,6 +197,8 @@ "global_settings_setting_example_string": "Example string option", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", + "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", + "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", "hook_exec_failed": "Script execution failed: {path:s}", "hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}", "hook_list_by_invalid": "Invalid property to list hook by", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3231c58ed..13660a127 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -2189,11 +2189,15 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): for domain in domain_list(auth)['domains']: msignals.display("- {}".format(domain)) - if arg_type == 'user': + elif arg_type == 'user': msignals.display(m18n.n('users_available')) for user in user_list(auth)['users'].keys(): msignals.display("- {}".format(user)) + elif arg_type == 'password': + msignals.display(m18n.n('good_practices_about_user_password')) + + try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: From fa66a7b8c7c1b59c9c01b715497ec41d62471f86 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 27 Oct 2018 16:38:25 +0200 Subject: [PATCH 088/250] OCSP Stapling (#533) * [enh] Jinja templating * [enh] try to enable OCSP * typo * [mod] use jq (json parsing) * typo * well escaping "" :) * [fix] if 2d part condition * We need to include this for ynh_render_template to be available >.> ... * Simplify code * Gotta export domain too... * Remove quotes in variable itself.. * Replace previous code blocks by new code --- data/hooks/conf_regen/15-nginx | 16 +++++++++------- data/templates/nginx/server.tpl.conf | 9 +++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index 1aafcbfa2..461c10c0c 100755 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -2,6 +2,8 @@ set -e +. /usr/share/yunohost/helpers.d/utils + do_init_regen() { if [[ $EUID -ne 0 ]]; then echo "You must be root to run this script" 1>&2 @@ -42,18 +44,18 @@ do_pre_regen() { mkdir -p "$mail_autoconfig_dir" # NGINX server configuration - cat server.tpl.conf \ - | sed "s/{{ domain }}/${domain}/g" \ - > "${nginx_conf_dir}/${domain}.conf" - - cat autoconfig.tpl.xml \ - | sed "s/{{ domain }}/${domain}/g" \ - > "${mail_autoconfig_dir}/config-v1.1.xml" + export domain + export domain_cert_ca=$(yunohost domain cert-status $domain --json \ + | jq ".certificates.\"$domain\".CA_type" \ + | tr -d '"') + ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf" + ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml" [[ $main_domain != $domain ]] \ && touch "${domain_conf_dir}/yunohost_local.conf" \ || cp yunohost_local.conf "${domain_conf_dir}/yunohost_local.conf" + done # remove old domain conf files diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 78909e3f6..bf2f36cb7 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -68,6 +68,15 @@ server { add_header X-Permitted-Cross-Domain-Policies none; add_header X-Frame-Options "SAMEORIGIN"; + {% if domain_cert_ca == "Let's Encrypt" %} + # OCSP settings + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; + resolver 127.0.0.1 127.0.1.1 valid=300s; + resolver_timeout 5s; + {% endif %} + access_by_lua_file /usr/share/ssowat/access.lua; include conf.d/{{ domain }}.d/*.conf; From c43a3687ab1104c582a63bca29c890aa7f753eab Mon Sep 17 00:00:00 2001 From: irina11y <38069993+irina11y@users.noreply.github.com> Date: Sat, 27 Oct 2018 16:39:19 +0200 Subject: [PATCH 089/250] [enh] integrate CAA DNS entry into "yunohost domain dns-conf" (#528) * [enh] integrate CAA DNS entry into * Move CAA record to a new 'extra' category * Display 'extra' records in dns-conf * Ignore the 'extra' category during dyndns update * Update docstring --- src/yunohost/domain.py | 14 ++++++++++++++ src/yunohost/dyndns.py | 1 + 2 files changed, 15 insertions(+) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 560a6fda5..685e60517 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -209,6 +209,11 @@ def domain_dns_conf(domain, ttl=None): for record in dns_conf["mail"]: result += "\n{name} {ttl} IN {type} {value}".format(**record) + result += "\n\n" + result += "; Extra" + for record in dns_conf["extra"]: + result += "\n{name} {ttl} IN {type} {value}".format(**record) + is_cli = True if msettings.get('interface') == 'cli' else False if is_cli: logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation")) @@ -334,6 +339,9 @@ def _build_dns_conf(domain, ttl=3600): {"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600}, {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} ], + "extra": [ + {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org", "ttl": 3600}, + ], } """ @@ -387,10 +395,16 @@ def _build_dns_conf(domain, ttl=3600): ["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'], ] + # Extra + extra = [ + ["@", ttl, "CAA", "128 issue 'letsencrypt.org'"] + ] + return { "basic": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in basic], "xmpp": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in xmpp], "mail": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in mail], + "extra": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in extra], } diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 88547b4db..a1427ba29 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -263,6 +263,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, ] dns_conf = _build_dns_conf(domain) + del dns_conf["extra"] # Ignore records from the 'extra' category # Delete the old records for all domain/subdomains From d77b157bcc87541965e7bdee27d7bc6d3be0108d Mon Sep 17 00:00:00 2001 From: frju365 Date: Sat, 27 Oct 2018 17:47:12 +0200 Subject: [PATCH 090/250] [enh] Set Path as full-path (#563) * [fix] Set Path as full-path * same * Update yunohost_admin.conf --- data/templates/nginx/plain/yunohost_admin.conf | 4 ++-- data/templates/nginx/server.tpl.conf | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/data/templates/nginx/plain/yunohost_admin.conf b/data/templates/nginx/plain/yunohost_admin.conf index 41065d2bc..3de66e3e6 100644 --- a/data/templates/nginx/plain/yunohost_admin.conf +++ b/data/templates/nginx/plain/yunohost_admin.conf @@ -73,6 +73,6 @@ server { access_by_lua_file /usr/share/ssowat/access.lua; } - include conf.d/yunohost_admin.conf.inc; - include conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; } diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index bf2f36cb7..9acc641ae 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -5,7 +5,7 @@ server { access_by_lua_file /usr/share/ssowat/access.lua; - include conf.d/{{ domain }}.d/*.conf; + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; location /yunohost/admin { return 301 https://$http_host$request_uri; @@ -79,10 +79,10 @@ server { access_by_lua_file /usr/share/ssowat/access.lua; - include conf.d/{{ domain }}.d/*.conf; + include /etc/nginx/conf.d/{{ domain }}.d/*.conf; - include conf.d/yunohost_admin.conf.inc; - include conf.d/yunohost_api.conf.inc; + include /etc/nginx/conf.d/yunohost_admin.conf.inc; + include /etc/nginx/conf.d/yunohost_api.conf.inc; access_log /var/log/nginx/{{ domain }}-access.log; error_log /var/log/nginx/{{ domain }}-error.log; From 1ce20259cd3c7317da4589846e7bcba1ee1b2d88 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 31 Oct 2018 00:17:09 +0000 Subject: [PATCH 091/250] Cracklib is too nazi, use a simple txt list + grep to search for password --- data/other/password/100000-most-used.hwm | Bin 1024 -> 0 bytes data/other/password/100000-most-used.pwd | Bin 405035 -> 0 bytes data/other/password/100000-most-used.pwi | Bin 24132 -> 0 bytes data/other/password/100000-most-used.txt.gz | Bin 0 -> 375311 bytes debian/control | 2 +- debian/install | 2 +- src/yunohost/utils/password.py | 28 +++++++++++--------- 7 files changed, 18 insertions(+), 14 deletions(-) delete mode 100644 data/other/password/100000-most-used.hwm delete mode 100644 data/other/password/100000-most-used.pwd delete mode 100644 data/other/password/100000-most-used.pwi create mode 100644 data/other/password/100000-most-used.txt.gz diff --git a/data/other/password/100000-most-used.hwm b/data/other/password/100000-most-used.hwm deleted file mode 100644 index e78de4a46cae924d3c0bd341a0be942e6133a032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1024 zcmeIwzY75Y0LAg|lSNT7*>5s18f3OJUCF30806$1xLXxcerzU#zu?LwshgCtFk-w# z8Kl_x1E_oTS>E`1@ZY%@a4o~YfnpC@eYg$bG=k_DoC!RqzhPsFZ)OXqxsdfBd4Sjv z%rlgHSl_{ZKs6+m5#c6;Z3sUlGHGGu#5^y&g4k6=y#DSQV$~F8TZApK?TCFh5Nz=I Fffq=>4Kn}$ diff --git a/data/other/password/100000-most-used.pwd b/data/other/password/100000-most-used.pwd deleted file mode 100644 index c794b055035f3aa4d1ab8e6257d3e97a7cc842fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 405035 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ð9}G$?vMz!g!r1N6X!NI(KH2 zut2SEi%kOLnM*okpA1@^8Uv03410JLW*unuzDNPMjSF1dX@!>#Da_r`5&8^ME6pmb z8_=re$M%KDhyQk=S^9(+$2sfiH@^K%+S>cb)3aH;{+@g$N)cf54aqC2*48Ukzv_Q^P zPeVHH&kI5#ytah&$!z)W>4cl^WCeW5(a6>UjjekK2V+K;~ zj_`ntK=3>}f9MTDWMaxPi2|7uWKry%OG}7P9@&sSKwWW!Gy5bsRSN6AMVThWk&l2# zkK8(b)iCnX2nEC$T>x(~j;ejlI#pf7H|YS0&iN>j_}4Ifdi zMb$*qkCYH7Tg?ngf;`fZ$dz@R&lzDeCHP2B)G`pRHMQ`ZV$?0oPM+?rdl)+fowm>* z%4$?5*@N_SXk?KK7A&>7XcwkCD4q~k4_pHDG*HR1)op)2nXLc@7pO|2%w1(Clyn@5 zr^Oy6*<9YPpD&(@TNX=QKb(nM;S<=ChAYSpdk)uPz_u`TU;9^vwn4gi$4B@X5>1yMiCKD2F+VH%1ABCuIUK zKn(&rW-kQ~^|S<}v-4F zzhE@%D{cxqv`dr-YNlWmunnOU%>Ati2;*rQH|HU#&v_en(U|G_x56LGsz>5+v=UKXVd;Yo6tEW3CF*2 z;-Qx!cs`=6;)D}8%9j^*Qy1s#y5$4~z4{6z?6Cy~OG03pM=))hQyLMGa258lg^4pZ zV33n-BNpXLqX+H1ty@$lAp?RK)hJ;a67tu=*I5g1NyKA`(GkT8Y#s`QXBP-9EKHb; zvVTZOy9;AAq8W&5u)-w73jVB?nmN&|NuwKPM?1czHZ0KLlS~l{N^x+=jMOJT2dZ)K zE}VyRzcikENX~jKQ;rlzr;iImgq1O(n|C}%ob!K)nX7QO>qJR6cC5M%XY zq-K@#?zE;|=ZRh=WfrQvDRQC*&~%PlHDr&o`TzoBF`=+Bq@y+xjeM~~X|kUtLcoYw zE--+ZtVPp3oou&zXQYAUv$_(>VyG`;hioQ+V~8Rnb&GlARoW%N^C-ALun^cu2TsmpqY-sLR&aM(t8#mLA5W zfskU>@1?1}(uFyNGM%>jBl)*`oZ!ahF^wn&h$gy5$btm`1>*P{7BH;sR*)vuFa{4G zD+g050Porys%i0Mt=51>T){G#%000t;$`?u?btWeg9@#steva??*sDLk)4I!=+ZnGkU z9ov@}1m)O45ew0Qw9Sk)X%sEQrdT>)biu_a0?pzfBsUUr5E2Wq5j+>R5^KdUf_I3~ zwGAfFud7G6=;l!45KlNL;7~WSG_57(oyQx%EM%ZARC84JlOGrNKnV-Vxw&)7d?ZF8 zR1OZJgUD+Nob$#h7yvGO3GF0rGYxn_$WaEU{B(6p41!?rnHhlCD1{#aI63Vqd9woc zId(BCqC%OF22&8^`?1=A=}?XPVkZ{ULLG=4B&mZ00Rw#8rgiY*IS6J+=!va%1ayHa z1_2$b)-W~c1U|xx04F10{(?-6@?XIrRoa$iz~S_dEzakF5fR;HE+ChpZP^lP#{FV$ zS_>QjI%hFbg(eb=DJTR)fWG5Pt^PDadIZNQKL~+T5wZeDE_Y{Y%8OIr0UrWHk;pnN zbH%+;I4r7I7xcn+mJj(1Yvn6x2uTy}snG$F3lw0ukUsM_6(p4}*b7Xbq!qwm&0L2G z!B(my8i(YGJe$OYcpKUnz@WDSoDPXZ#5An@hX~*}16s=~sG^9?Ao&sRXGq5^mydoV zN8ENEX9Gci+5rJ-iz&PBm)?1W;Bs}*U1*!N=P@YPu@CKND&w1Ss6?3ftDdgp;=@i= z&h3{HWbB?2OquhMjqHV;bv80Udb7-OjIE_| z#tLEDdW;AodckSqEel4RsI?I-UW$!g^>Lt%688@$dccIt0MteKD?2i9O0oRuX=+bX z;o)B%L&Cz`fgrbTD!XXs%QpKG&?d7o>lW=3obU%QD@92CYeV&s#2rm2Je(X=GG<<{ znxZCVP;Af;5o+t@6Afa&XdAax$&aSMmr+2<>2wSFg9M$TK-IyJ6mopRC1l3VfFKI9 zGYy2QMx{_bj9IuIbtub(s4c4tVHor0Fhd2kV{ve*<0+Te-xEP!ZGSxCKn*Ah(M4kg zW$f6%+-4Ol2SJi1HYZC4SosV#hweK_00@(?q|`7Z76*122wUb}LEyoP(TSYqOe1}y zEC4Xvi4&(BfKenIu406R1NVrl4F8)5jlj&r zV@#l`k!T(lNPAGbazt66goYkGf`j!c4w5+2e*zq|t?;s8_7FoD$eSrt$ZAhv5ID|! z#jNokQ=|oyNcI`T@tV1z$3}DzbwJxg={z8Ejro*7S_Qio z!(x*Wmevu3eL=(-6%zYxbyZ00kg?$0j1rke09A&0;p8ftjEqz)5!kJWcN2qP-b!^2 z%;I#@UO0(fz34$@1j?={{NW(8@SiC@lIF2*g(3MmXq*?95-8`ULDC$`g$o{90|xd0 zGH5&a1UW@ZLCw)^Ac=z5UgHN*3Gbuu!fDzr_Rk(sj(rW}0OqF0jrZO&&Fd#C*SQb6 z-z%QRYYwal$n*7RxOdn1H2e+DVqjA_A#Oif5BD>JG@h4#f`>4mo8i8k5IXJ+h8}{~ z?NM#R_DI^aDZFWK_x3!AC$ZU@Yi{*m8t`Fnz;CAlGN>&vH2^Q1l(_bQk^%VJ9}Alc z!4eflcj#R55mt|M%5+&^WK{{s89+cSDrW;M2;eg8`_wi;H*y^yV}AxqfHJguAq`#) zbY81(kK_PdEDXD}m!2{~PfBJ+0>L4a4<)>TwNtQm@fr%?aJL&LvF0G}2no315drB( za66OM)f90oVMf@m13S7a0MATQ5NPqGPY)tn77QF*&hv^oMq@;e;q;6L%w1@vmHH0p zPyx<;ZS+A5Ijo@=3u$FPJPzoLy!E&}-Q|anKrR}Gfq$Lbxvi;r+eC3}m~&CpA1=M; z1>xtAl)`=H>3|Re*)1Z;Xhu6uRiuv{FX;Kc+~IU1RZC?S`%Hi-8UrAAha@Z`7fo|H zRN~|>Q>c7&df_vvia@Wlk0sqigu$|egSv)<<`aN!*#?+`Oz9d?#w&Lc3d}wt1S(0Z z2wLK+u^UmeLFr8(q+Y<3pnBBNFASZ9$0^Y0f@oW%)q%I6Ra1{dYYCHMwBs?xl5rUk zAzWClvw|_jRy$2e9&eUb{Rw0KI)x5`9(vcl;uC&HWGD~-`R?Ir8BqhPJ9d{<% z1qPFrG6rf*;5{K`N*UDQLt=Hm4WPPd3NCi3usT4bnr)dceG;x%}Tj3?N^Cwg^*%_d(5+ z!Q5z!oTDvkVyTuA@W3|G!$a(BaMB957F1Sp4lA5lPIT;92Fg>o*A zFQ6EMP~nOoUUTgMN|{{=6Fhv+0002+IB+xWp;Fr#P^G)KY*D;rH3q=Ihb6&<6fBullx8m(?+>l(2r z55P)bFa($?v|RTczpe<8fQ2Z4peb)OO*keLdzn_sg1|lml<^91=4m9dL+6Ffe0J~g zVNV9}sA@GUCt1XqSMBK*XOn_WU?pupsA7{*cmVZf^@TE)2?eRQIR`5aze4`uqqSFv ze~`e#FU!@Im~GUPFfS$?TfULkhB8u;14c}X%HHN{KGva)Vm5~6lc=^G6R3c!5luAIeLt zYWUgsbg$%v<6;X0sUy~7%O`Ch=s239`79MtzYyUjEOAi`1h~7Not2wQ~sO8@c=6;L~y_u?ii%>8R^*U^|5GMK&2sh9&!i_SxU906?y`2vQW}3& zjvAtdEGmm-ToS38DE~02lFADsw8xU53}j(vsxmZ9^6rE$t({+LlqH8>bS5&(5AjKR zB(JFsZetR@w=cI0AH;4R><17)%ZH;oYV_{-%|v3-zS9$@Wk&}E870YzMX=tKBQi)pB@WC<8TjMbi}_%m87}?o#^D)h2IlWy0M^_gah2QC7O$d z@UR1=*3*%4yvX>8qw$$GJ_c#2xaEHO)A07f^vBHegNM+s8PYQ`L|tB&8ErS5CHPd9 zzBrATpBOu&71$o1hn)jy=E#V zi$=|yL<+Q1wauHS$|9L(lW7gz61ry^i*+@1PSeE6SXfp*t<{@<3?`k*TW9O0q??1M zi;L;zyP%)cr%ThEm53WCiO2I`e%K+9)8+Ip0s~z4P?VIa_r<4SGT3~|(O|_PxlEBb zQrUix(G+HEw}TlOb2%&w;;_Th0z+UKFKe@g3c5nQFtNVO>+Y)@h$H_Iy@~nDPG7@t zvalEg%Q5YV{Hrowu-3)hTq&Cm%j}00<<>TJm|@m-0FsBFk(b@kF@7$*P`>cO7ES#P@Fpf?9}eIf|mLE zRB7rzU^kH6wEw9?`pJw>kuOXrLEYh7775M8EwMLj z+gNg`U68SA*yYY_!IV2X%vKN*A7BT4)n6|hwF47Wu;_;{S&RqZm`%RB@KN1CvNgeS zmbCPDb$7wo9Vyo2NM3uKf(1-X>wey(q&+%xAGtAAvRtY1UpGdY)Z%5A z6u{%|lB@qvx47JW*0?s(UZtGy%X7+e#LJlZ`eJnlPZ>g(Ajt22mFK&@{L!(1&A8j+ zrfArtTk+(;g-B$&?Z@nxYhkM-a}uhR^(91ax*gCl{t9SNyl<( zb;Qxop^c3Pj~X*oQ1cNz4$%bdBTD!?VdTDK;1WxU@X*fSW;s@L5uQlKbZ@7FuwUIM z0dHXg=o0J(53?OK)Ft+&vm99kEAd7e{b%j}LrOLWLf;`(BbeH#Zsp-(xc@_(Z%6i_Jtp^ygH>`e4^b3F}+lm z}ArUQOTS43tnY zwTT#isC3;zdw81W*`%PL8Bh(Cj%XW|VyOf$6{5)7)6P7JL2D=1u8PgXO2Qb&0B7HC z@_Yy`7l}H~Q}#;!ev<`DRJ47d1R7y&ig*PcV@~WMkb(E!Oc6&~_@uns@R^kc^DucV zCl*Xtnq1MVCH3_h2o7*B9e}}^gP(1+0RjRj?t!r6QXTr$h7ofMyqs$td(xtpo@<<@ zh5mII0UG&)p`Wfr@nFmK4bau~4_3!tJ8SqYUnGA@#Q9A%GeX9W3qY_on1lz1I%gs9 zepWcoP8K#V;EY@_n$^+MGLQs0~CLvhJrh4;cQEgTv? z<>L>7>88GVJ#8JgjH*52Y*;ESrG}1?ztr61J8M{2WkLX;RbQZVCXSq+Jt|V}7LW5O zNA2L`?209)dy=SE&7EGh1Hfrdym|e&(3`#Lxb3utyK1O{0vdwt8sO1?TBn1Z^kACU z@&xC2t-23kp?2i!_y2jYv0d@m{xN=@kK)Q156 zEa%SRakcx9=E>nf41GfXVWUm*9WXlD?;K`v3j$J1J<=AQ!j{`TLqMi`r%H8#sl$`` zdD!|W@4Eyjyn#r|$l$vtZw}4-;gu42%#oJ^UnwOf=3m&XwXu#~v=y#ILA!b~eS9c( z5)xJ&3j*C)Hvk8CLpu84EJJXhQY%q$zp#$v0nUn=PJ&u%)0WJeq1KEE!Hb$ruz>n`jK z38A&GyzAPirK#JMyG`&tMC~j5<+C?yosu;lic)pS7Z_X{;lx6IIfK5tEemx0+f;v@ zUVS^aqMOalnM6>R|2Q8O(mvBJwEgX^TS}Ht#hGI+*-D&N>=1k!Z0VN%Xsvd68WkGx&apbpQKP-uMiYZi;Ihy#iq18q z(TkUKp+m@kS1O8`BLnZeMY}Om2qX3#cIuUFy_X7ryDMX^1Oty^roCx}X<++fmCj&* zt6Na>cyekl%omAf&R3?2b54eXBy;i1sMw@Ft{K-fKx(Q6cqV?7c$ z+GcO{U*v&LkFn)6;L{MGHvlw`j!pJzkwxj$tsWs_ym9{b@3Vb75lvUDkxuSCJt^$3 z8+L-zm`v~rw#!yUDavSNThA}`;w)v#s9QJ{k$nM~*k+Xn#uuC{112t57@h)mUOA<( z%5Er*c?$@#9Nb4DX(AX(Wpvag>dw5+iEdLcTct%Ne8!Y_fd?a0LU)*(KH|lqcMY*e zI%|Ns!%!w0fN*ds1+aPjm{5ZZK!1GCC_mUmoA)>B@*7u?d*3ZI7AxkE%Tc9y)vF0n zCaSMZ4Ruw>L6IPiqH5GER5lLKTc!P*0fx7T8vB`*meZ4qQY(9UVBLPDwOEojFeO@f zcsQXO9-Io31309&gi!&jw*nKgyNa&|%+?Wq@1cpl7Q)hvF1Y=GQ*&RN!oQ>%k#<^} zNPjBwZqjwQ;%UMAI(ysY^r5Eysjntl~whu8Vd?```~ zyLp(#9EpwIvtZ`rNNCVsKhKOLTRZ4p;$yJ*+pBi@^yye+pqyXlw0h3yF%M5=b0r3d z-Ss|RAkD$f@x;Q-A}$_#@FyudhEJOI@7)jv^bUt0UsdV{Ki=MrA+p*%C;V3z->|Ni}tE2UA^+Y@_y|9o_Bj53CeR0aq#WVB*U#S?&4Fk1*%@b0ZGX`4a2xdgei_; z6x~3#fqX{dH{M3yhT1vS%&5{Ml$Hq+GEkniOkb0bwh#Ms(AAwo)nRq$no@?kbZ{d+ zZ;^@p!hio78a>T!CFe3%csD4s@3@k_WYBjUXbOP9*nO3x65gq3x?I%v(Onb6eY z(->p-1teLArSe_p0RgO^GmT#Zte2sz-!{?N!N4PDNAntwAFrNn#w^4dnylRI4 z8kwWd{LpYbin0`;cjJnR^%9+${|ISoh;ddxCM{$$QjCWT5h{hYUT9Q=iXxatbD(<0Gc)L+zX<;a#al`B1jA;xRw%t5C!-LTf?ygdJhbqR6u)fI7_e{ax#B%I1S( zd434gU$(`dNA5wtgBy0?9T(&tmNo$GwrxVOEDq<>t}sYXrN*w+OIWrmMJ(mmFj*GX zUpAW$OfUR*Qf0sUU3=(-322Q_$Kg}R1Ksj{<+zl!13ZKA!b~-v2)Dzvcfs_B3vw1R zSyUD30kw?@X7Ug6w>%s5IQqffA9LcHI}Y0R#32&TN0s0Me7;0;PLCWjR)kJY@jZ^> z3d_DP*f7W1e7SkOvg9XxfQBDBSW%h2>miuc_4&pMsNu1=GRi`l3e15%9cjH8i@P+f zS27to+s+LG;ET7p2tjFZK8TOO7sltmiIHUzj~bQuDp~6~N_}N(P-FCVh@*#_%`J0m z)aZAENtn~YmK&PW4vI~3P@cD+xcZ{3O!#UJ(2F{4Xw~JSx%+0$>??wmMG? zTPfY6n^7e%n?-fGoboR_2AUJpF zcn?xrNbc9}%gw-Dmz9WW>8xT{tktFAPK|>ap3yRWekI|^_K!j!Ot6D6C)hJZ^si57 z8lGth_iBSy(}Jv zv|nqqtFJ3vm{p7mk!DtqC){bar9dp(Gfa$Xa>S>PDA2?J=7QB$;B&oTmK^O(%YS`Dz(?UW46>kCwA|?VwVk>&!W?SmQdK71$L5UHUNPYHLMTdlT&dq9(Ff2Iv8kz zS1Jx)36+P74z_$V6LgR66NbW1%Q0O>*7K64^(v%IuR|HI)h%u@mecC{C4t*co;`Ux zFQ&bfrDcLxR|NuA?$OLV6^zuqo?MhgFK#k6gj{qt?&MCodmW`U#) z6$c&emWNUYV?&LkUiN!WP{rx}#dbM?HOSKl2*VcKm(ypI!_kL_NezhPKt$9Z!LaxS zZtOj}YWQ<*crCT^uN02ilOX_G2zF`LQ|j7uT-%cAR01?-tEbCNqOi=Clwz?KZ=%_nls(QPF%&RPu9?Mn$tK(r`o*P^3vouhe*4YjVOM35ygpC&W8Vy&w8NDQJ%V_hw#O@TXGFYn`{*CPiPK*yoa9wn z(-Ab#O;zBHqjJpb2MA``*j~SRU5Ky{&$P=l&%N0WFr&rsGsGkBol;Fb8v0y^`5|BU zJQ2_`To&fiq-X-*g|Uu1Jp|R7a<=du+{3SCO$SM|ELY>9^WS!STtw^bR6T}x>%fN_ zGlsq9@Aih2k2B=tp&DZ`dDY@ruNlV^h)5M}cDdAkcH%HmohTV{L_toEKYbB;Yl`+ghrsk$~47@KUfTUorPijQx8Ai5ruJX!toXtGTCs=5=U`; zk>jfQi_EXm5UARNwK&r74rGAhVuBj%?$1+C7rGt%pOVph14m_MOgPumV{>iV@ODIv z80?w*pz?lp6b}3QS76#K@nz>uR6My1;h@|+JzGU3V~?0lj@EplFC6$rEX#2)H}`-X zXP}00AnT}E-~+EL#I^@8NW1iVcU3UBbL1sS!A&6J}4RMm8u$z;yz1%n!g=2nYx^QpOcb?=6@6k5cb^*m+HF(IU!S5WR{Qf<}S$U(k&zn!b?&m01N#51P zV_CX~=QrPZ^V}uJkXv;?d-JT?m=bhV%}~b8IpvLmyYH9J=WcrE!Pz)+uFcw`5K}HA zH>os5JkWn>eiQ#qL$sw_ptkpiWILU^j^PohxoR4#yRCT0zJLaNb4C3!dp>s~DiEwW z(C9|D><@u24^KC4n_E3@HY8f0ha|SYoUoVgYPsE<{loS!$2S}PrG-h+Bj`9em?>OC(JZT0$&W_<72`)6mrj;BdJ=g*`%+vYD7oZlW_{(Vb#Jvsh(`sITo@Kvs) z`{V8Lo42PY+poLNpH3-E|J*%Y{e2IG|i~?|*{x z{m-X2o3Cf5*XggWIiZi3gPL>sS5jD}_OWE+CA1>WJOogs-ASzi59QI56B&-WO5sB_ zOV}jbiz!AE^X<@53i}^!Y?z5k*WFLLNj=;XFqlZc=zK|Ae^KTX#P(h`Z*tnEZz|@L z#oxK)dY58GX`Olo@jf6X6mX-%`-~=@*>SS2t4?pL;L2p+VT2a7jk;m(AZYuUd#7qP zSZkLa5XVf_d~e3e>K=V8hv5B((;JAetDet7-9uk#aKa;$R@dA5e3wIa^Yxsw-uZ(Z zQQakYi@46zt{81-HYd106xoI-5z)#^PUT^Ti3TWUOkg(I*0}hAu!KX#?&>>%+Xfhd zUX(bT3*6I;M(N29IF?tbk)je0tG3zgI$w3=+U>rzCWUWPo#y7;iAkJGg1445N7njS zM8pp1<5Dya8Kc^UK9no~)mwk9UW8Uyf)5%y4Nvqm%1T-F~`P%LLgR|rGwwRFO;IV3_CG3fd@+Cn$S;3XmHE5Z11!6 z3sF!j!;b0(1*=-N#rNL)m-0Mmi4Pl+`LW_|B;lOd*Askkzlv8ClCk;cYNEDidu}Cw zZiGLqmhf4>G*5ff#SB=`#F{wWT8?C{cH>hj&QjD6q8n2(pV7#-Oc*z0zazMkr*QW- zu-L!b-^AW$UN7))vNX3EZga7<(??rJ!;r<3kUN>Z3o_*NvR_%NI~3;k5`c>*0g5q5 z^0Xk`!6M$usE`7f;O67BhTx7{5Dt^%I-waE2LA--!ubkL0rR#_XKfM0DIn z`mg7Qq-HV31YIWWfVva-4;&PnkP&mJG>Qbc>FTa!*jeFu{HPSA_7y0bv~7#l3$!uO zJ#}|~bDG7aOOuhe0rlcJkil=Z!i@w6bnnpKo4RbnJKKOu7RxrSy@Gi=dk4{t4%A5A z`9yF`yn|7FY<&-VwSj5@rz?$_kscufiD{49+}8VY^K|px2&R$Q8~=$3mFEa_;Z$GI zA-yhU8I<;PXV+S9X95dE|LDM3f+ve1xWuy~QqBoeuahaKTr6}!0?So%bu?$rfH&IC zeh!r>KEg0_yHz3CsBgZ!Lp}8#F3J~Ka>7;(^L4|l|nBJM@5J+Wl<{r(S zKuRIu^w6H}F?C0!uC;`tnbN~98^zX{v|B0l1UgHHgJH&}w0vdi`(33$oPpO83I9xA z=_Z@K{=;{T*SQ^v0cSl9 zRnAH5IsbZ_;451_fM|Q}ZkfN@#R6xG(*pL=CIKLVt?fO^6Cc}(X9hYC6(Ca&_>^B# z_ONE84FbX@7JvRbD(~%BBgw&|Pn$*jP!2FGS*%o?agtCm^0_2Z0k?pn z32Z&M>a6%$(Zc+^GiB>(9WD%tkfv5TX%8ko9(~}O8)gHIaH#M%8)h!snFA|nTZg%O zedRE|9hQVLLqw^=#0S5$)#ZoJCto6R$Qp?OLQ3VFb5AX;?5sqLJ9sP*r@#^lY$1&_ zT25rFhKJ0EGyTpJJUhW*)kD(u+m!!r_Y7*`o`Z0EH!4j&l-s5K9(5+2*npcpUDVjk zcnikn!116bv_NdJn~tGu2$SU9$(v!{9i7$cHYFjq4w!T4N6VJDXIPB|jTWRxpMpJE zNw3U82Y9vl!r~-|TTSxUmCZL9 zEx^RJVo;A+lU{sKmhV?9$lz!+%r8DS6Ncx!A6YUU*C;Aoe$a_j$*US7KVzk3ac&-V zNpDUJ|ExuSwg9H5hD1oG1JiG{Bg^PYljf@TWpJ$~>>@-2cr0ZZ^|dRP@5E1O{}t5o zI@)4r8oNhWQO<=o|1x2Nq9FQqCEza`QhSHtCRJe0vK&uXcJ1^Zfi+h%e}cbw$XMQPF6Nre%U{ovzeq-)m?lkHg1ugM4pCn@hdZo>%8W!#&n|hrcp-!!n21fzd#B4Xff~d4e=jNf<>t zUUtb|p-pWBgO^}l2|9P7#DLexqLU=2U1x-nESyi{Pdc4^3^@C3oqm4 z^y@{MVZ#xS?b62oX3IHAZT{Ms_t-Kbb<82(`qSJBcU4&chmvr1Fk?N4cdTu3tk<`Mq=h}G-g@)-%LMe8yCpj)RqHIJlIU3vM_3?ZQ&Hf ze)bUr-<}=6|MUQ@zRp!NbJxoKR1H~e&NjRk*WBQaxYsw2&pa5H>M~jVi=93DaqV8D z)m4{rke5pVvU}Pq_4Q+H{v3`FDyO4`y3fW})X@V0SM&GpQ;RBCfIr;sIu5veoG_Qo z8Q^`d7~12@=8SuSujQ?j!7u0BEal&_u4!;l0AzrUlY)LEZw*;q_DhgqGyT-%h*c9RF@K~-=!?9 ze|-PNCkF!Yu+<~i4m|k5qXY`E%zfpGALlrQYtc-KU*+)kukW7A3-tTu`{u{_RtdJh zznpye%EJ2ib@SN{aBIKWs38UeR$3w)VX!ScT#MG&YXP13U{jH8h|o>$Gu!b zkmhYk<$~m>y4ZQK87`b6%ES<5q<`CTLOnRaKe2(C#nt9OZ3%0PRZrN?AlhaJEzu)q z>>{7vVdCc0fwSp#8EQNQK^0%^seOAtA1ZUA%L%yB{|PYY?XSIzu+CC2F55 zN}YHwjNcdMg9zHdmk!!w1$0#!Ksa2+aQyPP8R7MfeL@+$l^1JG6YEM{rE!ov`s344 zG1PVhRhd-l2W%=pQQx`Rj`j5LcwKDd^W!B_UHv*&CS^TkR8}ulaFD>wWca+k*L`DVUx7utA3Yb;ao+)7?H=PxSOVOOuKU;p+M z&T(;G^c+-q_xycxk5#?<_4s^ortRk9@_hUKmwS0k?FUvf^(TIw$+6i4vh(}PClPmU zd3gLlwN+N~;e;Gicn0Ua^|Jc==GylV~gZ)VIKe)^i z0PSj{#iV1KpabIAUm6EAMAW6==fHXjhwK90BJ-cqp7og^>m4$ej`;i zyVwm=K=G4|g$q$}E=0F<*o(JT2xJ)*_n&h=6Pv>D6t_BXilYbP~wik z9yi)DE#6s;vC+$~F{dn~N!tXm5!MulE)WTRqc(^SM$741BbS|dPA9oMX3=+st>JRy zZes*kbCoRS#DDy_wRr{$BAGZaf-e6j>9=3_I#|kcVp|=e!HRTzJb?{ESpw`9Gbyea zK?6AqqQ=DuvV9EO>?_+bfn*=kU@bvl&y3+hoEh;05_f@@wEuC8k8tYzFxQ6JnZL=_gIN z4Fu#??1yLjkj{=x8)}caP;h>2kntBcQa#vN*xvookXCn^c`YuwcM~ALoL<6NJL(Xe zP_S3aWK2af`?oQE{&p6DFP=*UvtH ztZqj^Fzu8`TE3MVExxfQ;iL$sYm+}Z%sI!#j% z!nQKo**4=x7)rbWJ!kk+R!pEl$$_ejv$PZ@Ml!>aKvP@306-piEqcvS38j=7XF#f9 zjoC7cPXOAI)zwg*k94I11D;ak z5C7)BbaWtGX(nBJ2S;6UVvpT9Bd%39zq?+J6{YIE?b^H<``QQ%?Pose&H=bsut|2`Y0OCSqGw;hSYCJEV~R6~XVM)!yoUxWx?%s#f6|y!b|SNv z(DmV5ix(kaa1#|UZ3+4|A>1WTOY*`v*hw9X%X`^F*U`@}ROmK#kaF3L!$NE=^wHrZ zDbFu(SL1Pei%uF0jT-gs!HM&6SDRhnXWvsJlt79$>u0@Nb1Fbp(*kQXsiWeG`wZ6x z54vC$LqN~SNM*de!i_jyBo*v+LS0CtZ~q`9WS*s-d=?Y|gmv}3wu<_% z*bd8%rp;|GuWomi9)<3K3|f{uPLv6dUMmZipBy4>We;^nAl|mX{mp5dFiUQ-_ApoS?+#&{@jH6>MTP}1Gc^@8_4?qCPZNjOz zKim3eEAr9&3+i^q&J48MXdv#UE;bgwMptKajs~JMA0SC&bMpvR^Y_tOd3 zPIm?C{Ac&(zxQwc&*;^A{WEYHjs-TAx~mUX*J<=4=hrWeAaLqJt5*-d3weGjjn?Gx zBXP_vKb=K%G0n=p!GpdV28>h}9=zBlfCgSbQ;#FawNo>9XRSs=g!mW%?C)@zBijy9 zut_hN><_dQ6Ra_$Kus};!SJUmMj$jshZ$yC9aJj>fWy)~m-$z5D!&iXB3Cnz38V;! zR7mMOEoNQ8vmieliwNd`T6iGJMWZkHP^OBqrDIgPsyzE+^Udj=mpXxxn+*_n3yg`z zi4E$cIdyn_$(6&52L~DO+s}%U`l-oc=L&&>POjKmd(5p!auDF@#4Q?Ajli(w0 z&h@gH2{t;ZY7Q1F#CpN1aoXx;2~J-G_3J)_%$5vM;nkA8vfgE%K!oBS%*#EN35PNU z^#RUqcp=amUXE-(p;pmM7Sl4M*n4{?lA`L3Xc}s&9A{hODwReHvdB0X-7kfd>Y!Y$ zNHw9^tboJV^8Eew_!>_VlU>GixI3|v`YfgD-Lau=y>W&*iccH_jLDVw*s93F=X}pF z%muOg;4}1-Xl#l{>YTTjN!i$Z-z=4F&u^}`n@1O;Z67ykb(GouvbouI2SomoOTJCs z5fr}q@qK&!IDXWBU?wjLf?@Z+Qv&<(*9-5*!O!RzUl%#I`F_voTCONhNZ!}gFK%4N znrie4O}pRZ82Q%|a`asxwU57V?oN*lyv<+vr?~F=>YnrGumQ%2L($TqVCHphkt5lxr94WTSNfQ(^1SA7|8xqxtamocz;NT>r8_O%;MgN3~gaDb~HH zkX?VOe-rZBOfn2d(qL(GGh1~Gu@dVSDU{wIw-m@D>KG+k(J$5x#`To5;;=v-;H@Yc z{GX7W=Q&?&BUoF;4&+vQvpXu`2&XBGy%sHkJ3Vx!U*u$9*z1Ed8QdXy>G9SSy?4l< zlVrqPv6WG8>3%ZP;eg$&o$wD+5+f~SVIdjr$Xj#%Q_Nsrf|c&<8R|@6V_d2fF zPV$%r`UqL9B>v-{Ux5O-&}DcTEVDW3t(dlT0^YN88mh)=&3{(Mhru!z)0g=-h0@{W!1M8L4(HZO0o441S_g^r9n>#^( zLL4rjo}#kPGSpVHCx0f4bfdp`6Is-m=b98H#~h@_hPyn`EI3*EHEm~f1l`fc&@Fzj<{`1p z<2MHxw{TNn5IUHtFXdC1)=k|?=Gb)a%#6AgiV8mUSX~Y%HJDvs5}y#PF776UAqF$! z9F3!JG3>0Y&=`W1?Lb5y`ERMTz_2vkzN?C2*O?5R;{p|+Gg7y#nm`YX-kyt&B(-d` z<~qF-Jx?lrTSvyQGe;VY2XQ@RgR3x*7-x3`g4fv{p45RVQhw^qen#~J(HRS}o>0ul zwb&1(-oJ&M?Z4wdjC+kq*^Z67#0n?_EqQym8_fbuU8e(%d3#}68-%O}8!oZMn_^7Yn1J8FmqTO-yLhF!Dn{e-)7pe3n(!jzY*NczX;So4 z5qva~XGNkx)#QaV-nr|n5iM}3gp_w~^2N?2vajbhvrZ#~;x;u94uHv8-F(BTdVZZY z*}ff`#BSp);S(e0mBhOYrF$CwpR+@4l|(^MIBIHnmURwRGVJ1KF!uZshb90BkZryu zUMHpdYRA!Za4k}=Fa4!yhC4*Sq6Tcex6(kl45w){U;0{nYESn6Ut}uTa%mpz-v>;%+YAHf76I z40h{wG7F%`(6|PNx)}seL3wBcK4S~^r(FsZ?q_)2MT$gQh}X3<`^x+*0)Cl%z-<&4 zk|A>dKuT_s+v__^wa_mgKfwaSI9D~}oLm3Ad36&H8RaSVF9obPo*8>C0u4(ZzZII# z9`kZZXmUN7H}2|-YU()upcE0EhPq`f112gM<&cYslV`EtNV2{M$8@052=Dri93 zZ>qTyD9cKK5!-LfgXPF=40ju31V|c2Zi?B%0oKdK@LjHI!RYG;T1o*jFs=)f#J%cgxjWnD=xHFOa2AB+nekomGLBILsyDHO z;almeL(xDtE76YmmrZCHHWHCm@?ReuCwzN+TI@0P z^0R6OE9OL9tEC%ZP?n}uxIdaB%7P`Sn3)G{c>=e;78i+(1riQ~YYypKbD2CxQxQf8 zp0vIc2s~!O&-J2kCcE1FXcU7f`$NZ}LA`wNtR_IJ6Wr z>e)pLFp{l6o&MRrMY7W)4o?&d<>>KUS!*Gl!>(Z=-olhzQ8(XH1`r zkL6}FbQ1ercGuoQL@9wrotrhgm8Y5Oe@>AhK8V1xWum!7;vJMS2LH^t`M_c;kgKQJ z2t<3x_+~|yU0OjjTw*3YMvyVr4VlfM=RsTDFh6KCfVPPLbthm3lV8mXW-W;@IGToN ztL((MF)W8vj0;fJog%=`1WQblKGH%*Y(`#&EvI9~YZ$st#-}ZL?Nuk>k<*qcR0P?I z%fl;N8rOrtI6X5sT`J_EXc_N`h#R2rAqZENQ5(UMJQuIftg-d|+C_z7OmJ`9IV&wqzM2k;4lSTxK-vkyh58(d~lnTkl( zN4JNx5yWuv4@ZGB2-3`#Z7~3HO{h~0kOi5B>%>nOzn~Va`4`5-92M+_C9#K5Lm*%G z+YV{x0f@DgVU%|7#hmmr{-!httH8*(NQzjp1F7C#RYC?Bt5$iK@Zjv?IuOS(S&iM`vjI&xqSRdk&pO$-ceci zGuRI;4N45MlO}L;&}^swg_aI)%;LupBQkj9>g~uzAE@D{t?!+{S}m4 zbnJ`!@h{!Fwb+D6i?ksS_N3$?7@UW|n&R-Fm9y>N0|i+|q@e{H@1bDujk;?7LJ6z3 z2rZ2yCk9LQ*Y_#+_QLDPdKpTly04P#I3x;bwqeBT6Q$TNIa-umQ*No84V{|LalB*0?O_4zlQWVLF@_bTQ;$@H_6t_F&At zVJnSFXqlz;`7KFhg!a$G=U$>VEHrzn8dz|^UHutThCP7VT*MXfHMK`ulioTQs-fM7 zeP%=l;L&jzmkjgdxp)H)@^9w8H9e+{!*BXoUCEHA4QvliA)rbmHgms|7-&%1912acYdoIkY{Jqg!Zzv{8fih_fObYXW0{>iw+lQfO{?!c3nl5@Xd;H zSsy`ea)D^u&PM4VQnm@mp-wfuuUn!srBqC3sud=Kh5S zAW<%HD<<2R1Z8T@9~Qm@>Mi9A1+Wt(&dy;sU_P9m-T(c93a<+^QDzUTRcB*+@2;HH zV-_ur?GO!$$_aRl=3RgP#p(annN(4^TMpR0|5lj+j@#Ac_Kae7^|OVW>HRtB+coyw zst(#M>&g)+Xmu?6_zyvaRjG8-`EbF$KOV25l7D`vd|(e;)+k!{ND*&Q=+3NPzU!?z z2v6rZaUXtu{r=%(iC^aM%h`L-4d;4$`xGq}v3B*1TR0S(OLD;z9sng^;V~O$#_bL2 z!!G_Ptf*=dK!x&n>rp@U#lgeWrbr|5d@j~aZl1iXc7WXTntHYVF}E-HEC2T(SZz_3$twQnz1O-$AZn z?eYj)NI-sPzAB*B(!o0FBaG8|6xxRRuHk6_Op<0ce(5x{efRKAIYG}Q&G*7sk<0)W)kAuLNyLG-)MB$A&6NNeH=-#fqf~#|+rTCJZ=N2P`#)TlnnyU-cI(@VT#i^$ zy>6@(1=)%}R%zK~ZtmLBzh1s}=Q7<*eB<+n(+`w@)!AUv=GlqE<(}}yD1&1MzO~qn zXU?9>DtTmb01@V0sfX0O%Y??abEnST->`X>!xIm9`Ss}|7W8{}e)-{oQ*xs$iWU<8 zJNxJIAXjd;b1|5RsJFj9e0Hd^+WgvxVEPmJ|JN_Q7Uq(qNjk0M1(^A--`j_;yPyA5 zWaXw3Q>#cbXyiRjQoLg&?T?MQw?OJnK(4b9NvGozXT&1W$4hH+G}^vwT<1D_!loRT z0fexBT_m|MmKDLFd$-X(tIumzSVJY@$?@{cJNC1Hh|m&d2watZE%5CFaY z{&D&9&D*XzI^c0#?QpH~F;{mFf8V^^Y=5%9HgDc+-)|n@cVcIVC!Pd@Ixe56kxy!#*jY@yuS z(+}Ivw)yY(7os=ce!TsBXTyHpU~+FieSW&R{AIts?9N=XwqnMx9KOE)@cyJ5n~&dr zd^mrzb(nP$?iy{KD|qdj*cY<*&DDnwT}e;@pCf@TlZq$ClrfCAl%2(va6-3Tc%!q_ zCq1V1;R6KfaO?!~kgN`IhN!yu%)KRh9GM=T)NK2b$~B=NXv#9i3l1VcHu^dkg5H&CoDH<-)hiB)T4MbpLRhI4sdT~de;N{9W%(vb7ISKz*;f3hx|`?}OGq)VSaP>?X^#`H_AE89Kt# ziilctRF42?bOf^g?JHVotsQ;J;xIp{OIH;Li6I}n$8*}i1Ec%*`ORG01j2rE{(JO( ztQbaZU*%XZ==y>;iMnO-c3X{k>Vj!y5}`?lzGm>h^YBq?{E{29JhPeC<9K+7m0uUh z159ZR#4>Il_qNk+#{;8T5xn1;^oS^o1=?Vf>HVg}`hZgqRhFexp7f~Y=?}}#W_Fpfp4|`daA@zo8@!3m2wy<7Vc)1BlqHRrg0vA z&P5{0LVL9-qhaq-YPpBG_Udjt&Q`#cH>-)vztuSSAeGF&KJ6PFOCOAbo%^>o8pj)d5X!fVlL_&f7Ej&$yONn07xeQcm1=ogT1v>Wzcnr8G<5E~88{c3ibm#~ zb)ap5;nl!IuyL#WuIx^78?ZISm)Ma*4fg)PQq?3oD2=Sxu6?|K)aqc1AKIC&8{J9k z1!Wkz9HjRxMU}_L>{9T)@3toO<7V&p@5n6eEyZnN3X8_kg??a8nVf{J!>(VeFYS2l z=eV0LY@k?Wy8J%L2xaXMy5fjg@044+h<|VfOySJ(2z+ajItvkZ+V2UG1zw?UVok6k z;{!$yUAbd)o%u(j2q`)P81AEo390l+EV-S)8+cIqbdR>o{IAby6Ug{GxE$&%SA)F##)a4obM^hyU%jYL&s3ciZPZB>C}Fr{A)I?f zJM4$>;zW%n6Je*Z*ymk0U{evgH*Idh7J@KS&1eX;2RC;)+bobQ8VVT&B)JARj8W$L zmbPXfd@*J^V)}oX92eh_X!6fyfH^E-?9*&y-M`Gx{4{fb@jn4cd-f=DO)t}^Z2H^;H|+H{e}A=Lodp%GssrT|H3s)Vs}^51Lc__TU07-*NDVP(QKRkn-EshLlvN z$}2q4&;EJuB5L1df2@9`CBjF0+3qhjy%T6c(Oig-Wtmj<8NKH1q-;&;ipqW6NbZG& zrihn4mW-CaulX`ql53EKUi zQm4EEtE#-`Y9vgNr76s|EqYtR@;|lRp7pj$$VCxo}-fMS`p=Zi#}877J5R&dKnKZ^mL)BA_fm9+itz1NNSqcAIIfGw%aN3l z*j&gMTjJ4h6hAYS;I!h0nXh_t2bxX3X zyPayu{^Um)3Voa+S@31_Xt^rT9!Fr%Z+C!R6vdR(th0P-F6bT4>k`NFmyudCpOK)u z9k7hzHHI{Yza35|kUjhAU@2HFQDM>mv~2(t#B6t4 z*LdBA!;`M-zzYD^>2K=_@vb+$*nEE=050C~YE?KU%4Rgag@d1*B5Y7yq=9R#Cm@6H{NLb=fQU3B@?eO@|qd_NWrrhEV8B!mGs6uG**LszSB z?^$~5O}-tBcYLMZU2~>$MRqyK{nplRD`JnkOhK3LAod)iE6Q*6gAPt`K^t0vrbRlj zGx_XWx}hR85RKTJK2Zp^bbO`6!P=IMi)~z}yNjO$n7eQmp>$s$648;LKQzHg;s0=Z zhMhiK!-C+5vAsPik!KFWGI)|2fRNZj2#1mB(x2^{U?_*ix-g>&q&;~dOY)=1VFelJEI8|Cj*N_puP-fA>Qxr_y)=f9g0e$zHu$}Ws-)~GOQeE@O zcM&B-h||wCMhV{%Maln6u}RqjH3fXIkSKR6)p{z?$K2*{QO5buva?8tIi-F8*VYzo zr^TXHy>h#LzxgmMPcmM`im^Mez|JkM9KfFJF|M}$f za|PFsJs&;luN>j2l8$`u;2zb;wuP@$MEnCRcKLOXUfTKPS9Cx@8nm#(2qxa*xekRE zWha*WSMIqlpL_G>ufN~?^|7j{f0ZF9bev9%r#EHIXgPt|o897RS*%clJ9_XVR%QL4 zHH+{;V;FTi_v(?+xYl8O^3>ABA;XpS5j-cNNEfS;8Wq zc%~HR$diw@I><%&M!a^ifiV~GURcd2{&G7*ded-&O8#l)Cj zb?ru=(D|aUe%bCG9H`z6Mw!@n_>x444@*5arOU6mmt}Me=%A~uwhtiWseB3dd}p&; zlDc*SkF*a{FZ$oKtcBTpz@h*bxyLb51C4IOoo0x;eK=soSd8X@`Z%YH@0#bFS5-gi zV4}sTw~l+wVDTcQo1?0NT3%fwojr|uQz>U0H_XA-uli z&xS}>QqES)A7hX9bPd>u!a z*y6~=QN%6dncWUxy={K^qND9#y?;52ee-`#hq+B})5o|8yQab$5zI>M z|NX|n@2K_%y`cp~SSls?Z*{S#{O5q>qne-cx&MCHS?A?|t>?AhDx z`PKD5SKl*pE}fs7+nthAY!G3H=XYQ@*GiQ}xq9B7_YcdJcO?9tDR)-x&ss*~=Tkb) zzbTRH-`k}))YmV#$m&Y$(#Q92wilFfTjZ?U@k9k_{Pykcqu1{@&t*ai%3oato7Ll0 zV;-5%+i!Atl+`kxJ0bF`9VHQ=S{EqsYN)VHRTIcAl?k3S1NxuDg;vG_AC>xcFz^3Lt zvxV*94ZeCVe*?O$ru6>S$=8jq@w#4BnT5-Xq;{k!ZD!b_f&2rg6Ydi#Mhc>q7^Z;m z1)7d~APs4?oPmh6Vbr;U@gyZWrE*qGX6kRLUjw7j{U`lXif1 z3oV)WS@&gZze%<+NgCN9nllgTX#m2m@&5^C1ahlMuBFvdz{ZWJrH#I#B2qrEmo0xQ z{vF#|?m6gWi8GD-hFydx2r5322IL?vwYue()n@A@?ZOxQ%z4=8)~()5+T{hc+o0Ej z@Co5PkhhMD*{Zy^;|B`6Bc!XV8O^R}FF-+1DJP}_5^5%I+_-=Tcj(V_sKXPQ>?dE) zs(hpV%T{q`}~Z?M>^{OM;0wWzAOtRfC9) zTjvD5J&g)q1&%JLw_m=PPg?0=3ge=YWE>DB}wDZzjnq03iwj(FZBYOx%v4osg zC)UIxkacU@Jf;#&DP1;+mp~}TsXnMWUEV(E8L7;s_We{o4PB@Q<9&^KaYfxhJjfC9 z_2YgYufJXgEwaD7{bpf8_~m|q^En3$wsr7OX-AiOJbUUBwGV1SosXW}Cy%2@ysdDYfWlLLie;sV zT2DGxn;`5u&Q<+U8~v1Bu%3H4r#Q6E+P;ZJ0#7@bR6|n}gU+|J)hX^jEHD!cnhHYE zM-@LNrJsm0B(|(ChzypD34(}%hjJ%kE~KXHLWq2TS6^?>o)kt+IkebN(VHA4;c1M( zN=OEwm5YlCy`YphZwkgA@f6oHQ{(`?W8jQ;Nb|Yd5g^sg;Z|Jzf|SFmF^uE7^*l66 zV)*qX$|w1M|9-{Y&_E2Vf|{HjUlY}pRT5DmypXyBAj(hZ;PILIvQ=x6E=3tp(;=;A zU3AN8d|KK;)NwzX>FRuP;zc{)Hna0>0N(~7x}*?xfGe${`h`=ak-^2E~j7) z6WJpJHI zu>d4FWzLbdI9P}8JBG^m4^+IsBO&QNsr}dX`j=F85oQrSn zqn)MFL{i>=%WQYqKWiQt+?5Ev#T%3VD|!bmXS?%htdDs!=E!Va7-Ni*d7F&mjMos= zCya-0VEu@rfe^A~N5p4OCgOStoem0miPzb9Bw1Dt(% zrAq)*X0(5X1o)t4$=ZfB@pPoMh>qIKHJVZF!VrdXkLV^{B4}fB%NLeXY9tfZK$Ggu zLlSK-#=35b{h*5%3}THYyI%aIV{+}eo0@16dpUy2b6^R=%uayx0*cMwhq5OQaiu;r z&5nl-Uz*Bt563=oCqM97%$x)vC}y@Ra#PdfM{$>KjHKLY136PD)@Qd016cI-_KL>W z%%9W7M}z)WI-?OL#`W8GOiP1{3MY=Bf2>|vMuZg)66u3l#(n=63DvGmQ1Zy}MWv3- zkv{N$FQH`_hYAew1^GH0Z7O*i1J=9oC(^I}*WxfGc*A8+Is5df{4fu@rg!rLJI$=F zi);U86?P8@J3<>J*{`+()c^)b=R>Nhr6F?^lkl2UFVfEBZkCJ!$OrrJ;Ns&oH!<>^ zeMOa?_kOM{>Oao6BL@vV553TZg%u6DrcH1+Ht>Y3bmEVspA}lhSXO!eu;t6rK5TUw>xOx!G4Hh2r}ev6Zg19;yOum@U&EsSG-&mPcqtOBC9 zuf;Q`J;Jin`ha3^ET8M4et`f`QuyX@Y@F`fW@UXSEOwi|Vh_=n*Zq||QC_zAPjs>F zl2&RY^f8*I3zw;BE5N+hbTNEKY_cZ24b?gpzb6cqJ!`H>I;wyNszd(xWVWSCZE|d0q`Z3#$ZDB$4S_kmY`v<*tS4s7K!WB z9j-%%$>W{}27P5biAlS*L>CC;?90|=>d&kxCu-EBXyhu!cAA9oV9k-U) zfW*pwc^RjjOvPcZPRsrxKXU#;#L>UI9mqS#Ll^_YB3s@uBETF>6nw9j1E(0O3N}w-wj`@80-31@R_*cJ16^= ze~tsG&wC>iz1+AY8Pwh03xhD`g2IsD&q!w7Z4px6{^vU~-}mb~&1_mu5_xZ<{rQF^ ztAk0QI5nlVaCDgBoffJYI$@dtpYadr1~iWEFE{7+Y1Y&z(bHSrMrTE<+s_ZzvIo}Z zLOf{{7m$_RI1+UxLEza!i)4ByCEE2A$RFc@OcZd(+$|v)KPQ!iE((d0312=4wQ_xg zDtB+*VgB@i;EsaJEt!Ca3qD1ULzv?MX1lohv zzc@1Gh6l_rgRJ#zrTOG;!6$D($aZ3G0XU%hlnb)@Jbm?G8Ybu6#KV;_yS-O5jCh^f zgC}SH?ZYQ$4>y$34$637FknDjZ4KZ*g$2d%yJf|X9kjEsXp20QLXsYaBm?}}Ty(IF zXhr2bo*sYvdLkd-@6Qi4*eLq->)gXHzP&*iN*83&EH^soKHFK6e^*|RriKU~U!Y#-Ud zlFsJNOcVkch{_HE1-wA?vdq@IZWx(!pGwly}h+D|go!!uL zwz)>!MR0b}q8`UedF(6^n7K0{&6TRFgHw)ou32PPx` z#3(|*p||m$kHTeHyukEWZhAiAtl!m6U8R3?w8No^b*Fww()DACA>(>qzG`s>wwz@Z zGvV>Nln0YKG<*fa+X~SP$Q4d->WFnr`4e^jz1=?9SQTQjcB?@jwdVzHajo)oiPpE3 zR1eneLF@cNT(mxcyl-8MtO~#~*DZ%%y{`lQ5*JuA$+T!vyE@ z6S~y0j!HQ0a2$~)LJl^glwXU>WbkcWld#WnpeRrnkNSdaT&it=N*DI7N3e}43Fn%0 zC&pCfri!V#a~<;e@M;F|ic&Zh^p_8l*!fA@$Tg2)V&OEk3XCei4b!2`F)qzu8--+yW{hJ zKAr#m{^sq;&*Oi1SO5OI;OW)fx$^99Ki>WReEJWEB&GZ_rv3Ko-zOJ--ChU|T`3Dli4 z+}47VkCvv}4v@V1(p((G5Hp!FD>v>OT)0L%pxHXvY!fzo@;M?59mmf>>+1kw$}LV% z`+{az{2MuAs-#v{7iHsnQO$*Z+z>HK^rg7*g8IVk^gZ7%TC%?JFA0382^9+gM7>HO z`1rH2Rbry`ywn-zVfr31aB!>#UkrCdUgXTV1{3k2m#KywLt{uf%hH$75dkC1>8$T5 zz_NqUmG$IAJ0QPIZnV$vE~7AA?_aFTW9LC?ufjJ!>p(1`lJW;jrHXpX2fr@Wl|um4 zSu@rZ#{~bfC@B+X()Cx4f5s|zpaPZi{%T@@XSX+2_;$02QBxwi6N}!3GD2r)sni** z$3z!{f(W4>A&;zyik{_oy0d6X-R$^oqd@h;?u%XicF#_BM9(EF)~|YG@dXU6S!S)G zoDFam-uhH@;G(##i*fk5+gR{+Y~uET=N zw<{ytLk=sez#RRzzIqlK*)B&@P;p-ioWN@@z+|z2K?zz2=`}*QKzoO&l3m;%Vg^4V z6WUQiRoV9g@vqGHhqAMRU&sp?>tY;~v80nhezU+BhZ*Kk5IXA**&in=TRWF`3aChn zxZu3sr!)*dC#=fBV+`V?J%=ZTj|gtJ^?tjYz9P6N&nuFZDqjM9u0Qa%NfwHHK#sHU z?Jh(>zx~fQBlPy034?7?Z1|iMT)mW`ZlJg4XUs=uKA6+_ocz_HCq=i5YURdx&aY?- z|9-sq`StI|pO+VA`cgH)w|EgRx1;#}5O-cifm?rns-*SaT@y6#7<7KE4%Hy_?!ta5 zin|puk74gFM4shND-h3wZDtOuHs)Y=kyEb|2UFoPPbD`!KuoJ!nj4Rg$he%*D57-1pWp-+bOOs1YLjCya>N^TdzN-K5O6-1|-HedB znj+a#m*9uv@zvGkw~GRmu75jSTHO>|zxwsz*U67BNdN5>>GF}5vHJV;GyDDBs0bHb_fnLUW6SpQuBli`bXS2<{i#wH{ zZGz3AX%spir{bDePG9iHEbO#~DtrT3{A#J8c!*157YG{&2|eXmI~7f}85#iAzp$PE z!a5#(oX+M0KJ4LHlTgtZ)NT)Br|^DwpR0V>;FMEi?nLcRU<##2mw8QDJxU|dXSAy+)Trasx61Xe( z_afj0;n_t2N*o-q0qRLqtSLZ`g0^Q23id3w{6x8Sj`m$4J395+UusY&^}WDu3ybxX zaLl6#ZP~ans$rOijsr47hHHid7w@cSKoKU z-|ot-4td}4PI_guoyF{+&P}X+dBE{R)-L=eM)5j{QK?Furp=Bw)?AIhY}ekdh?#b= zN_BtM57j?w75nsV35LGieYZ}cbazEIHhtMaJV^^b@2>Xbm><37>sij1xF%v9n}F;1 z0Vvnq$)n!Qz*U&IXf135nt_TPCP7eo@N#jM*DH!^@J;aAe(kT4F0Ngg>2Vt4n%}55 zH4dO+#|a4>{h?F_wape#_)TwEIGjD^;lNQ6XarfZf0QkMuLPRzuEgXP@et1^G{y3G zR0(-5wYcj;@%O9WZuf9FEiZqv@HV2G)6xj&9kTyw68n4L{k1t#MV2Mg;sDgVx9toUL)au%Q*$VZx>H?Y={WR zg|9=}R*q`$TNt>fD$CnLVdU5$aFk9$yOFt2vzO7vTaZR3{HksuU=y3Np71=}};d@m?J|CXw1RLA&LA4{pSrQ;i?702(HC^EP`bAxP zSd_l8Rz46Y$-ZnVm@rlHD=IM2(eD5Yc$|rBQuk!23}6R?fOmNC&JB|HyHs)giv~9{ zXHz)!k&UIWz_z7AZKt6L3ayncaw1kS`mqc}Mpr5pFYbnM;dRXl%f-LnG#$iYuG>u}~ZOZ!PI%TE`Ep0zd*QE%0!x&5AFnu=~7AXCCCbt^K zj5-RO$(L>NPoy%Y_V>x(91QEJ7|;|3J5}ySS3vyTOmpMK2<-hyopToc)tm+{IBap8 zqiXW83uB^mD4O9fo@&J#bO_Pt*=9DqC@wSzCBh}!4$cu$HqIF@8Klge+*aFo_4T5Y zukj1qOX5YdH;!8n@~{D@=~%Ss3`6)7$vlWr$*246f&$>Q56%oMU#&({=`fbuNr=PQ z)cZ8~LouFYgioGt-JEE2y8leH12;ziF8Ol8gFF+Ow|y#P;K@_@v?YCF-A*ugbC}Q_ z44~bjO*oT3GgF}}c5b!17n%rPl)g`4NNX3cZ}UcI>1%AbJC)}NASiPSb*muC8F!+zC_4#G@ zz&fY9#@>H5Y8{fwKzdr-bs`~#O4PUEkB z0^jQ9q*$XhVG;K+X&)A&Cw@+4uul$M$ehV%is@$m`2c%Wf-~nul@gdfdT{CAywf3i zZv4SMV$H3N0ujNSKgh7q71zVXpY-zh(#>pK`>UsIY66d?K9;NxSN~W?s&#*%&C5@k z827~k#|A~meV-{$7*YxWjpwKC+1-$g&FxhNNe);`JGgh4ZWe7MTFW!nF=zL=vx*Mx zsLY00~y9jNe5jnW1vV&Ska4do4tIZtsp8Vz4*7gm1ZyF#b(jGrhEG3sXLzFqO$@H3^-9FgQ~<` zVhP4CBRJ>*t-3qd4vo*T)QaGLRTz|b5@rp=>ZQGUDu~@F&Q>&sVX@1v+D~YK9mS7q zBDDeFVkV@Mz7CwRSjwJ-U~-(9NC-IhQs2aO!Vg1DLJVc@?hB|#<1xv+2#*@XPTSSk zS&TY#=|XQYMhV3YOuM}^9Hr+)Y_?mmyOk1-Ke9g@xf6J|IoX9k8$y+4-Ko77IOu11 z+Uoh|&r|0LTn0Gh7u<|eWs;}l1OV-xidqdhIwY9IA^a@$li{X@}K`UAL(}SWHnx zW4(R;ln32h=Wa43ntYU|qc@dX*Cp(%uM~r`Y#wiui1?xRc0r2`8Ei=6E{;!X+Jm&d z)Nrd0Z57Ds*V23-^?sAF==ew^6yM`5lFGH8ZOX~j;-HKja^*mQ3<~?Hm$~E2+q`WC zG@k4rO(r;*#$YeON&2PxnqcB-eRJQT zi?~^MMfsFIQ7QXbQ93n#+g4-31ateqVO%O@&_vxwoxh9}rhlyd84u102kTnNw2G(A zq1{m@ce|g$No@Gb6GDIcYvNeIwfJE*y}H17xrGN?Q+{zYpK5TUzDbr?^ljEE6#mO9 zcfU4F9KxH}Xu5U9LzI4nz(xfDyh3xAv&;RlaF)WG95B00;llf5v)!*gra0bjCK4Gp zi%`=&a^h2c44WbBn?*>dVhw1CipZ9$e`)LeUOw!8-4#W;=H3!7I~d6lZG-;HjEdAt zvUak!!5pf?7?)TSz9esGmW~G5emVCZJ>(r(64!2>n1*gf(ocUja%qMsR*}{{H>hsr^HG z-abxM%ay8DoMP~4@5Db<*O4IRxn9Bb^?1f{i@D_H{P>(sdyeTHS6F^@_z2?W%gLL! zCl{9=$m>C5Ni_dGhEM9aK+`}yVm>DMji|Cg)lpQilv>o4+esqj~# z)cSlOm2wYRETFe=5Bp%pZr+@KbxfNX6+I1TC!S`^9GOUi7xKLUFSS7v1f!sF`$UcG z_vH9aKQQCqc=^-aDk2E?wJGOZHYAxs5C3y+1POf26J$sOL`gq7P}0edsoNDK3LuQp zlF!io924`OBUI~oIp35VSH2$KVFuU5!YPvf4;w@W$zf$}I zU1Xs1c$}L%`O1CM8zK`yo!?r z+Q7uUxXg<(;Vgr6P&Y@RJ4?Rmt3!w7b-9{pV%qnitfwb}AJ!Y&Q+}EKW52#TO$0_{ zXvRUzu|8ZGTeklZWb$i=M-FB9ddFH?+`NatmOr^bS*wb`Ef3U+^cvZ9$oYQY*A&Yb zk-fRV-A245FVNSalbZd=bmRb!o|&1Q=;|jc`B61Lq>`KARZ1VmdqC0T>)Z4$@JDUq z;)6&6QX?3Bco(|0TVqGh^*$qZ)?1T;yo_L?&y$3@_C#MYxq0G>w&lH&C_Fs! z;V|wFd;q^sn#ZoZzs~1z6q7UL#QSL~AoYb)P^gw|Qn-6UY&x_+3i0sOKj&Zn!392j zBBVO8o!BAHwCmKq|IJ_1I;=T-1SOhi`BHf!aUnXQBb(p^$QBpo46{ z_$cP6Omdoc*X6Xsoms@DJvoGZ1krUO_%Y396po2=5`#3!KsqTT<{+^Du6SORTfstC zhr|#g@5bTcQ5gVwhD>vd(>cXsm}dG{@D{$)d6tnS1t#rGp!Oy)Dhk)u5k<|$dkHr! z)1;e9Pox$o5u<<$Ns`=UXks*Bsl79`v-jQ?b3+#|?$Z{Wg_@nC3=mla=g#G(91WmM zg-Z|DqC*JV;Ai55=5!Eu`*SzIfvPs6bc0WeCJ_XaoAS@#Ucqk{d~VgqON;qx=WBmMtHcJ+rGIzZ|&&bSAGrw*Y%Ccg2g9v_xhu% zoN@V@96a;QXf}@mmk=X4Si^>FxQE6|xQFH;e!Rwt@R8Tk1o_?V0VB17u=GDq2C)2^ z43#pqSA08o?nOSH5!VOf-#&V8p73@0C$x{S*7&>BV&OCA8>R-6^85rsycfrLmA6l> z^}GB;NSz-4{w~7IKBLuC47wv55&Ta--{0!HhdawCVgy_OR#`@lRdo=eyD*8uu?hXR z>W#P0>au;S09uArMLGBKkk-!^Y8(Ci_Ihze0y{MEUfk5^bgH}q@@}j6==N2T=Jb}>whGkn_9Bu$X?5M1vaOg!F zf+mno3qArr-^i21G?d9WglnFwJBIUBvL|^vgFL5%{0gsx0g}jQpIyUr_}QsM3L5Ds z5%<}0s#&{7>%&wp&biZlfl9l$X=_QrZf0&7kK&FVH_?+j{rp$X;3E&R!VncC-T`e8a9;z;%eiOGzgyo?}k!t1;fKBj1hmv9^(KTl=`{!JJX`U%pbhuIYkeQmZC{*Y6&F*?g9TXZ<%Vu4<(>4C2Sx&C%B*bGp_6`||9f|BQ3lPv@zYu^8L98XN8eJFqbvwf^;g z%)*NGJ9<*9c5D_&KvcGD@w^}*-quDyNr>F>t_JY%J9 zj2ihiiIo>ur=riafopsXNbk>Q61y@lcPIVwWm+J@QM}oGyAGV*ZTP2)yB^cjWzNo6 z{kV92x%1`D8z<_UGh+2spdC}WFW|AQBcI_c8)C}sApF1@!#zEvBIoEMqU4q=LA`XN zg*Uz2ykayHb+}MUN+wSx`u;bp>?~@F3#6+}EEVll3%0J@C4+5owq4|MBZOGa5H~OW z*v)9itrW>=5W)b3v(Z3KeyJSZ`g~&O(cE>Z%7KcR3Y;%V2^ZIRyMOi-E)TbXc81=E9rkON15L|X_MxH#`(!9s8#NoKyFE>r_?<3yqJdkm29DGf^1Egrhx{pQ? z&YqDf%?XfQa4WBM=@!7F6lHqZNQJ}n+g**nz})66EI!%{tezLTl$W}ke+?3JzSw8{ z8hA5J2!NTm)sEWNM#vdS<4I;;c+(xXz`Qj@`9nG_-1pC0;;Ud#5)LopdP#aj>_)3E zn--qzn`sWn;|ZDeYzg z0;q44RCAvL-t0DW(`Na*8#hME@Mg}6(BGmjU~sJ@pVFvp!>;zDF0Y=n9>siSI%E+` zXy4vLD!l8x*+7Sw!otpg)v^R6%HF8LLPi^55aK{)7T;)|MTs-uGaQD>0Q z1REoD-h(*IhZM7V>;+=O?22>vMQU5?)$vceT?DaXQR%eQNk1msPjj82;*LFVFL1`O z)N5^dy7=t-GJfhbZVDabG{ByvPkINnsn-kfB(CIW+~fVS9zQB{@9e-m(c5Cblgu>n zg(cIIcKTd&A&t?jk25|}Kn*p@^q5VBH3;wTO?|~mfUY6Sa1OiVIsd&we2z}esI-Gp z!2_1@_I|P@1E;?D;F-l+xIb}~ED4p%`DYHYGV<#8r0B0l|f;`Zt0mDDwvm$T*Mr|XL^vPmt1V0_Y2X)h7Er)9G23@IW+`CrQI|3}x< z|GC)uAFBU{Tq6qVFQn(3zWihNm(EM?1r2{DwS9N=ba@COO8c=wdfCncu*03vVx6Q1 zv^KueDv=6XVM#@VVhb8gR@9(q*79zRUqX=-@ez{lN>N+3SISe&Dxp?OPTOdEKwXj$ zUj-Z<8=yKD$yuh7=^MNH=?rG7XOYb~6BMF-PaP2AkV@W6H`%OHgBdr}ayG+*0U+p~ z6_Rb|Y_w5h?k*@hXEz;MB&#Gs%qVaH2GIL?VF25A!qaB)bA(g$ zYf1G2>=J@FrFadSS&s%7N{-ZA*slKr5%Caft2JgGUF$ z8fMa?zjH0LI-JxD3ws5ICc7L}XT@%{1WXuPZx?YDLY507|I$45C$$CP;mA~hMV
      mOJwi2Za95mj2{;CHp?PEh>)>r{rqDkJOS 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 z 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 z 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%0AQ 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�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�R;+>(RMYdUF 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

      935^rKC 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

      %e4)$Qf!H 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 z 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 literal 0 HcmV?d00001 diff --git a/debian/control b/debian/control index 8739f368f..cf450484e 100644 --- a/debian/control +++ b/debian/control @@ -12,7 +12,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) , python-psutil, python-requests, python-dnspython, python-openssl - , python-apt, python-miniupnpc, python-dbus, python-jinja2, python-cracklib + , python-apt, python-miniupnpc, python-dbus, python-jinja2 , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget , ca-certificates, netcat-openbsd, iproute diff --git a/debian/install b/debian/install index c616db73a..b540ca749 100644 --- a/debian/install +++ b/debian/install @@ -4,7 +4,7 @@ data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/yunoprompt.service /etc/systemd/system/ -data/other/password/* /usr/local/share/dict/cracklib/ +data/other/password/* /usr/share/yunohost/other/password/ data/other/* /usr/share/yunohost/yunohost-config/moulinette/ data/templates/* /usr/share/yunohost/templates/ data/helpers /usr/share/yunohost/ diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 1b9bde6f9..97b397f2c 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -22,13 +22,13 @@ import sys import os import json -import cracklib import string +import subprocess SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] -MOST_USED_PASSWORDS = '/usr/local/share/dict/cracklib/100000-most-used' +MOST_USED_PASSWORDS = '/usr/share/yunohost/other/password/100000-most-used.txt' # Length, digits, lowers, uppers, others STRENGTH_LEVELS = [ @@ -105,7 +105,7 @@ class PasswordValidator(object): if self.validation_strength < 0: return ("success", "") - listed = password in SMALL_PWD_LIST or self.is_in_cracklib_list(password) + listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password) strength_level = self.strength_level(password) if listed: return ("error", "password_listed") @@ -166,15 +166,19 @@ class PasswordValidator(object): return strength_level - def is_in_cracklib_list(self, password): - try: - cracklib.VeryFascistCheck(password, None, MOST_USED_PASSWORDS) - except ValueError as e: - # We only want the dictionnary check of cracklib, not the is_simple - # test. - if str(e) not in ["is too simple", "is a palindrome"]: - return True - return False + def is_in_most_used_list(self, password): + + # Decompress file if compressed + if os.path.exists("%s.gz" % MOST_USED_PASSWORDS): + os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS) + + # 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 + p = subprocess.Popen(command.split(), stdin=subprocess.PIPE) + p.communicate(input=password) + return not bool(p.returncode) # This file is also meant to be used as an executable by From f5a8113c33a921175e78d6de534d2ed9eafb510c Mon Sep 17 00:00:00 2001 From: frju365 Date: Wed, 31 Oct 2018 18:14:44 +0100 Subject: [PATCH 092/250] [fix] Improve the message w.a. when admins tries to create users with a reserved Email Address (Issue 1216) (#553) * Try to fix issue 1216 * typo * Update fr.json * Update en.json * [typo] When admin create an user * Update fr.json * [enh] explanation why admin can't create this mail addresses * [mod] Improve message --- locales/en.json | 1 + src/yunohost/user.py | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index ebbe08943..0cacc5f82 100644 --- a/locales/en.json +++ b/locales/en.json @@ -255,6 +255,7 @@ "mail_domain_unknown": "Unknown mail address domain '{domain:s}'", "mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'", "mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space", + "mail_unavailable": "This email address is reserved and shall be automatically allocated to the very first user", "maindomain_change_failed": "Unable to change the main domain", "maindomain_changed": "The main domain has been changed", "migrate_tsig_end": "Migration to hmac-sha512 finished", diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 48065f70a..8fd445af1 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -127,6 +127,17 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) + + main_domain = _get_maindomain() + aliases = [ + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, + ] + + if mail in aliases: + raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: @@ -166,13 +177,6 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # If it is the first user, add some aliases if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'): - main_domain = _get_maindomain() - aliases = [ - 'root@' + main_domain, - 'admin@' + main_domain, - 'webmaster@' + main_domain, - 'postmaster@' + main_domain, - ] attr_dict['mail'] = [attr_dict['mail']] + aliases # If exists, remove the redirection from the SSO @@ -306,11 +310,21 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, new_attr_dict['userPassword'] = _hash_user_password(change_password) if mail: + main_domain = _get_maindomain() + aliases = [ + 'root@' + main_domain, + 'admin@' + main_domain, + 'webmaster@' + main_domain, + 'postmaster@' + main_domain, + ] auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: raise MoulinetteError(errno.EINVAL, m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) + if mail in aliases: + raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) + del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] From d6053f5eed8b21cbba10a574d26e2a38b40edc77 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 31 Oct 2018 18:01:00 +0000 Subject: [PATCH 093/250] This ain't used anywhere ? --- data/actionsmap/yunohost.yml | 12 ------------ src/yunohost/tools.py | 13 ------------- 2 files changed, 25 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 102be300d..60f7836d1 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -1452,18 +1452,6 @@ tools: required: True comment: good_practices_about_admin_password - ### tools_validatepw() - validatepw: - action_help: Validate a password - api: PUT /validatepw - arguments: - -p: - full: --password - extra: - password: ask_password - pattern: *pattern_password - required: True - ### tools_maindomain() maindomain: action_help: Check the current main domain, or change it diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index c54355c36..5c6ab6f54 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -144,19 +144,6 @@ def tools_adminpw(auth, new_password): logger.success(m18n.n('admin_password_changed')) -def tools_validatepw(password): - """ - Validate password - - Keyword argument: - password - - """ - - from yunohost.utils.password import assert_password_is_strong_enough - assert_password_is_strong_enough("user", password) - - @is_unit_operation() def tools_maindomain(operation_logger, auth, new_domain=None): """ From 787bfaa05809c124b5e20fc14f9abae7d104753a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 4 Nov 2018 16:14:56 +0100 Subject: [PATCH 094/250] We are in Stretch and use php7 now (#566) --- data/helpers.d/backend | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index 94e26350c..b36235b42 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -237,10 +237,10 @@ ynh_add_fpm_config () { ynh_remove_fpm_config () { local fpm_config_dir=$(ynh_app_setting_get $app fpm_config_dir) local fpm_service=$(ynh_app_setting_get $app fpm_service) - # Assume php version 5 if not set + # Assume php version 7 if not set if [ -z "$fpm_config_dir" ]; then - fpm_config_dir="/etc/php5/fpm" - fpm_service="php5-fpm" + fpm_config_dir="/etc/php/7.0/fpm" + fpm_service="php7.0-fpm" fi ynh_secure_remove "$fpm_config_dir/pool.d/$app.conf" ynh_secure_remove "$fpm_config_dir/conf.d/20-$app.ini" 2>&1 From 361f4e7a60aaab4e479da917660eb7a47ec39125 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 4 Nov 2018 16:47:20 +0100 Subject: [PATCH 095/250] Unused imports --- src/yunohost/tools.py | 5 ++--- src/yunohost/user.py | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index a34b41545..2817a3057 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -32,7 +32,6 @@ import logging import subprocess import pwd import socket -import cracklib from xmlrpclib import Fault from importlib import import_module from collections import OrderedDict @@ -130,9 +129,9 @@ def tools_adminpw(auth, new_password): from yunohost.user import _hash_user_password from yunohost.utils.password import assert_password_is_strong_enough import spwd - + assert_password_is_strong_enough("admin", new_password) - + new_hash = _hash_user_password(new_password) try: diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 990ec4c8e..baf52eb2f 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -32,7 +32,6 @@ import crypt import random import string import subprocess -import cracklib from moulinette import m18n from moulinette.core import MoulinetteError @@ -132,7 +131,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas all_existing_usernames = {x.pw_name for x in pwd.getpwall()} if username in all_existing_usernames: raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) - + main_domain = _get_maindomain() aliases = [ 'root@' + main_domain, @@ -140,9 +139,9 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas 'webmaster@' + main_domain, 'postmaster@' + main_domain, ] - + if mail in aliases: - raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) + raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: @@ -333,7 +332,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, domain=mail[mail.find('@') + 1:])) if mail in aliases: raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) - + del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] From fbfb8bca38ff3cc54b33b4994a02b6f165ebe20e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 4 Nov 2018 17:01:10 +0100 Subject: [PATCH 096/250] Missing single quote in comment :| --- 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 685e60517..dd2eda4a3 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -340,7 +340,7 @@ def _build_dns_conf(domain, ttl=3600): {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} ], "extra": [ - {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org'", "ttl": 3600}, ], } """ From 1c628c8d736c369f4af112b13f6579695923d3f9 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 4 Nov 2018 18:54:59 +0100 Subject: [PATCH 097/250] [enh] Use more blocks for dd in ynh_string_random 200 are not enough if you try to generate a 64 characters string. --- data/helpers.d/string | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/string b/data/helpers.d/string index f708b31b1..0ffb1dcda 100644 --- a/data/helpers.d/string +++ b/data/helpers.d/string @@ -5,7 +5,7 @@ # usage: ynh_string_random [length] # | arg: length - the string length to generate (default: 24) ynh_string_random() { - dd if=/dev/urandom bs=1 count=200 2> /dev/null \ + dd if=/dev/urandom bs=1 count=1000 2> /dev/null \ | tr -c -d 'A-Za-z0-9' \ | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' } From 469c40fc7ea1a4053b1bf9e7ec4c26e27453b391 Mon Sep 17 00:00:00 2001 From: Genma Date: Thu, 1 Nov 2018 16:02:22 +0000 Subject: [PATCH 098/250] Translated using Weblate (French) Currently translated at 88.9% (404 of 454 strings) --- locales/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index ad04bc46f..56cb1e374 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -416,5 +416,8 @@ "service_description_slapd": "stocke les utilisateurs, domaines et leurs informations liées", "service_description_ssh": "vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)", "service_description_yunohost-api": "permet les interactions entre l’interface web de YunoHost et le système", - "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services" + "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services", + "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faîtes.", + "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : '{md_file} 1'", + "log_category_404": "La catégorie de log '{category} 1' n'existe pas." } From bb9ca599737d1ff936b8a3303a28e680558df81e Mon Sep 17 00:00:00 2001 From: Genma Date: Thu, 1 Nov 2018 16:03:38 +0000 Subject: [PATCH 099/250] Translated using Weblate (French) Currently translated at 89.6% (407 of 454 strings) --- locales/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 56cb1e374..463f828b8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -419,5 +419,8 @@ "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services", "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faîtes.", "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : '{md_file} 1'", - "log_category_404": "La catégorie de log '{category} 1' n'existe pas." + "log_category_404": "La catégorie de log '{category} 1' n'existe pas", + "log_link_to_log": "Log complet de cette opération : ' 1{desc} 2 3'", + "log_help_to_get_log": "Pour voir le log de cette opération '{desc} 1', utiliser la commande 'yunohost log display {name} 2'", + "log_link_to_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci 3de fournir le log complet de l'opération 3" } From d5c79f57da6ed37fe7efedcff1195ecb568651de Mon Sep 17 00:00:00 2001 From: Genma Date: Thu, 1 Nov 2018 16:05:36 +0000 Subject: [PATCH 100/250] Translated using Weblate (French) Currently translated at 99.5% (452 of 454 strings) --- locales/fr.json | 51 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 463f828b8..33380f9aa 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -294,7 +294,7 @@ "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin sont identiques pour {domain:s}{path:s}, aucune action.", "app_change_url_no_script": "L’application {app_name:s} ne prend pas encore en charge le changement d’URL. Vous devriez peut-être la mettre à jour.", "app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}", - "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante", + "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante\n{apps:s} 1", "app_already_up_to_date": "{app:s} est déjà à jour", "invalid_url_format": "Format d’URL non valide", "global_settings_bad_choice_for_enum": "La valeur du paramètre {setting:s} est incorrecte. Reçu : {received_type:s}; attendu : {expected_type:s}", @@ -422,5 +422,52 @@ "log_category_404": "La catégorie de log '{category} 1' n'existe pas", "log_link_to_log": "Log complet de cette opération : ' 1{desc} 2 3'", "log_help_to_get_log": "Pour voir le log de cette opération '{desc} 1', utiliser la commande 'yunohost log display {name} 2'", - "log_link_to_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci 3de fournir le log complet de l'opération 3" + "log_link_to_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci 2de fournir le log complet de l'opération 3", + "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, la restauration de vos applications php peut ne pas aboutir (reason: {error:s} 1)", + "log_help_to_get_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci de partager le log de cette opération en utilisant la commande 'yunohost log display {name} 2 --share'", + "log_does_exists": "Il n'existe pas de log de l'opération ayant pour nom '{log} 1', utiliser 'yunohost log list pour voir tous les fichiers de logs disponibles'", + "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement", + "log_app_addaccess": "Ajouter l'accès à '{} 1'", + "log_app_removeaccess": "Enlever l'accès à '{} 1'", + "log_app_clearaccess": "Retirer tous les accès à '{} 1'", + "log_app_fetchlist": "Ajouter une liste d'application", + "log_app_removelist": "Enlever une liste d'application", + "log_app_change_url": "Changer l'url de l'application '{} 1'", + "log_app_install": "Installer l'application '{} 1'", + "log_app_remove": "Enlever l'application '{} 1'", + "log_app_upgrade": "Mettre à jour l'application '{} 1'", + "log_app_makedefault": "Faire de '{} 1' l'application par défaut", + "log_available_on_yunopaste": "Le log est désormais disponible via {url} 1", + "log_backup_restore_system": "Restaurer le système depuis une sauvegarde", + "log_backup_restore_app": "Restaurer '{} 1' depuis une sauvegarde", + "log_remove_on_failed_restore": "Retirer '{} 1' après la restauration depuis une sauvegarde qui a échouée", + "log_remove_on_failed_install": "Enlever '{} 1' après une installation échouée", + "log_domain_add": "Ajouter le domaine '{} 1' dans la configuration du système", + "log_domain_remove": "Enlever le domaine '{} 1' de la configuration du système", + "log_dyndns_subscribe": "Souscrire au sous-domaine '{} 1' de Yunohost", + "log_dyndns_update": "Mettre à jour l'adresse ip associée à votre sous-domaine Yunohost '{} 1'", + "log_letsencrypt_cert_install": "Installer le certificat Let's encryt sur le domaine '{} 1'", + "log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine '{} 1'", + "log_letsencrypt_cert_renew": "Renouveler le certificat Let's encrypt de '{} 1'", + "log_service_enable": "Activer le service '{} 1'", + "log_service_regen_conf": "Régénérer la configuration système de '{} 1'", + "log_user_create": "Ajouter l'utilisateur '{} 1'", + "log_user_delete": "Enlever l'utilisateur '{} 1'", + "log_user_update": "Mettre à jour les informations de l'utilisateur '{} 1'", + "log_tools_maindomain": "Faire de '{} 1' le domaine principal", + "log_tools_migrations_migrate_forward": "Migrer", + "log_tools_migrations_migrate_backward": "Revenir en arrière", + "log_tools_postinstall": "Faire la post-installation du serveur Yunohost", + "log_tools_upgrade": "Mise à jour des paquets Debian", + "log_tools_shutdown": "Eteindre votre serveur", + "log_tools_reboot": "Redémarrer votre serveur", + "mail_unavailable": "Cette adresse mail est réservée et doit être automatiquement attribuée au tout premier utilisateur", + "migration_description_0004_php5_to_php7_pools": "Reconfigurez le pool PHP pour utiliser PHP 7 au lieu de 5", + "migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de postgresql 9.4 vers 9.6", + "migration_0005_postgresql_94_not_installed": "Postgresql n'a pas été installé sur votre système. Rien à faire !", + "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais aucune version Postgresql 9.6 ! Quelque chose d'étrange a du arriver à votre système :( ...", + "migration_0005_not_enough_space": "Pas assez d'espace libre disponible sur {path} 1 pour permettre de faire la migration actuellement :(.", + "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d'au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant 'yunohost user create' ou l'interface d'administration.", + "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx", + "users_available": "Liste des utilisateurs disponibles :" } From 62004c2c52c5b1665197ed0b59ca52bb398e51c1 Mon Sep 17 00:00:00 2001 From: Quenti Date: Thu, 1 Nov 2018 16:19:59 +0000 Subject: [PATCH 101/250] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 89.2% (405 of 454 strings) C’est plutôt « faites » en franais je crois --- locales/oc.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 103c0d3e6..92982e9b6 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -402,5 +402,9 @@ "backup_archive_writing_error": "Impossible d’ajustar los fichièrs a la salvagarda dins l’archiu comprimit", "backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de l’archiu descomprimit", "backup_no_uncompress_archive_dir": "Lo repertòri de l’archiu descomprimit existís pas", - "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses" + "pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses", + "experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.", + "log_corrupted_md_file": "Lo fichièr yaml de metadonada amb los jornals d’audit es corromput : « {md_file} »", + "log_category_404": "La categoria de jornals d’audit « {category} » existís pas", + "log_link_to_log": "Jornal complèt d’aquesta operacion : {desc}." } From 13b0d3c9efa53914d03be467872cee177d557da0 Mon Sep 17 00:00:00 2001 From: Quenti Date: Thu, 1 Nov 2018 16:24:09 +0000 Subject: [PATCH 102/250] Translated using Weblate (Occitan) Currently translated at 89.4% (406 of 454 strings) --- locales/oc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 92982e9b6..01dafb64d 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -406,5 +406,6 @@ "experimental_feature": "Atencion : aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas l’utilizar levat que sapiatz çò que fasètz.", "log_corrupted_md_file": "Lo fichièr yaml de metadonada amb los jornals d’audit es corromput : « {md_file} »", "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_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} »" } From e9482bc0a16ef8b1d000ca54e800f55c7d168993 Mon Sep 17 00:00:00 2001 From: Quenti Date: Thu, 1 Nov 2018 16:26:41 +0000 Subject: [PATCH 103/250] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (454 of 454 strings) Soucis de balise ( 2 et 3) en français --- locales/oc.json | 52 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/locales/oc.json b/locales/oc.json index 01dafb64d..4ead4d637 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -71,7 +71,7 @@ "app_change_url_no_script": "L’aplicacion {app_name:s} pren pas en compte lo cambiament d’URL, benlèu que vos cal la metre a jorn.", "app_make_default_location_already_used": "Impossible de configurar l’aplicacion « {app} » per defaut pel domeni {domain} perque es ja utilizat per l’aplicacion {other_app}", "app_location_install_failed": "Impossible d’installar l’aplicacion a aqueste emplaçament per causa de conflicte amb l’aplicacion {other_app} qu’es ja installada sus {other_path}", - "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta", + "app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta :\n{apps:s}", "appslist_corrupted_json": "Cargament impossible de la lista d’aplicacion. Sembla que {filename:s} siá gastat.", "backup_delete_error": "Impossible de suprimir « {path:s} »", "backup_deleted": "La salvagarda es estada suprimida", @@ -407,5 +407,53 @@ "log_corrupted_md_file": "Lo fichièr yaml de metadonada amb los jornals d’audit es corromput : « {md_file} »", "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 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 (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_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_addaccess": "Ajustar l’accès a « {} »", + "log_app_removeaccess": "Tirar l’accès a « {} »", + "log_app_clearaccess": "Tirar totes los accèsses a « {} »", + "log_app_fetchlist": "Ajustar una lista d’aplicacions", + "log_app_removelist": "Levar una lista d’aplicacions", + "log_app_change_url": "Cambiar l’URL de l’aplicacion « {} »", + "log_app_install": "Installar l’aplicacion « {} »", + "log_app_remove": "Levar l’aplicacion « {} »", + "log_app_upgrade": "Metre a jorn l’aplicacion « {} »", + "log_app_makedefault": "Far venir « {} » l’aplicacion per defaut", + "log_available_on_yunopaste": "Lo jornal es ara disponible via {url}", + "log_backup_restore_system": "Restaurar lo sistèma a partir d’una salvagarda", + "log_backup_restore_app": "Restaurar « {} » a partir d’una salvagarda", + "log_remove_on_failed_restore": "Levar « {} » aprèp un fracàs de restauracion a partir d’una salvagarda", + "log_remove_on_failed_install": "Tirar « {} » aprèp una installacion pas reüssida", + "log_domain_add": "Ajustar lo domeni « {} » dins la configuracion sistèma", + "log_domain_remove": "Tirar lo domeni « {} » d’a la configuracion sistèma", + "log_dyndns_subscribe": "S’abonar al subdomeni YunoHost « {} »", + "log_dyndns_update": "Metre a jorn l’adreça IP ligada a vòstre jos-domeni YunoHost « {} »", + "log_letsencrypt_cert_install": "Installar lo certificat Let's encrypt sul domeni « {} »", + "log_selfsigned_cert_install": "Installar lo certificat auto-signat sul domeni « {} »", + "log_letsencrypt_cert_renew": "Renovar lo certificat Let's encrypt de « {} »", + "log_service_enable": "Activar lo servici « {} »", + "log_service_regen_conf": "Regenerar la configuracion sistèma de « {} »", + "log_user_create": "Ajustar l’utilizaire « {} »", + "log_user_delete": "Levar l’utilizaire « {} »", + "log_user_update": "Metre a jorn las informacions a l’utilizaire « {} »", + "log_tools_maindomain": "Far venir « {} » lo domeni màger", + "log_tools_migrations_migrate_forward": "Migrar", + "log_tools_migrations_migrate_backward": "Tornar en arrièr", + "log_tools_postinstall": "Realizar la post installacion del servidor YunoHost", + "log_tools_upgrade": "Mesa a jorn dels paquets Debian", + "log_tools_shutdown": "Atudar lo servidor", + "log_tools_reboot": "Reaviar lo servidor", + "mail_unavailable": "Aquesta adreça electronica es reservada e deu èsser automaticament atribuida al tot bèl just primièr utilizaire", + "migration_description_0004_php5_to_php7_pools": "Tornar configurar lo pools PHP per utilizar PHP 7 allòc del 5", + "migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de postgresql 9.4 cap a 9.6", + "migration_0005_postgresql_94_not_installed": "Postgresql es pas installat sul sistèma. Pas res de far !", + "migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada ! Quicòm d’estranh a degut arribar a vòstre sistèma :( ...", + "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.", + "recommend_to_add_first_user": "La post installacion es acabda, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create » o ben l’interfàcia d’administracion.", + "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx", + "users_available": "Lista dels utilizaires disponibles :" } From 96e9c729831edb319919861bb89050a0cae09542 Mon Sep 17 00:00:00 2001 From: MyNameIsTroll Date: Thu, 1 Nov 2018 19:51:12 +0000 Subject: [PATCH 104/250] Translated using Weblate (Portuguese) Currently translated at 42.0% (191 of 454 strings) --- locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index e1db1c618..7196e366f 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -192,5 +192,6 @@ "backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?", "backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.", "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido", - "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo" + "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo", + "app_change_url_identical_domains": "45/5000\nO antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer." } From 24dd16b944fdab25da7ced1102a800fd8f30408e Mon Sep 17 00:00:00 2001 From: BoF BoF Date: Thu, 1 Nov 2018 19:35:23 +0000 Subject: [PATCH 105/250] Translated using Weblate (Arabic) Currently translated at 41.4% (188 of 454 strings) --- locales/ar.json | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index f300ec864..d4d8300a9 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -8,9 +8,9 @@ "app_already_up_to_date": "{app:s} تم تحديثه مِن قَبل", "app_argument_choice_invalid": "", "app_argument_invalid": "", - "app_argument_required": "", - "app_change_no_change_url_script": "", - "app_change_url_failed_nginx_reload": "", + "app_argument_required": "المُعامِل '{name:s}' مطلوب", + "app_change_no_change_url_script": "إنّ التطبيق {app_name:s} لا يدعم تغيير الرابط، مِن الممكن أنه يتوجب عليكم تحدثيه.", + "app_change_url_failed_nginx_reload": "فشلت عملية إعادة تشغيل nginx. ها هي نتيجة الأمر 'nginx -t':\n{nginx_errors:s}", "app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.", "app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.", "app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}", @@ -380,5 +380,38 @@ "service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك", "service_description_php5-fpm": "يقوم بتشغيل تطبيقات الـ PHP مع خادوم الويب nginx", "service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية", - "service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام" + "service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام", + "log_category_404": "فئةالسجل '{category}' لا وجود لها", + "log_app_fetchlist": "إضافة قائمة للتطبيقات", + "log_app_removelist": "حذف قائمة للتطبيقات", + "log_app_change_url": "تعديل رابط تطبيق '{}'", + "log_app_install": "تنصيب تطبيق '{}'", + "log_app_remove": "حذف تطبيق '{}'", + "log_app_upgrade": "تحديث تطبيق '{}'", + "log_app_makedefault": "تعيين '{}' كتطبيق افتراضي", + "log_available_on_yunopaste": "هذا السجل متوفر الآن على {url}", + "log_backup_restore_system": "استرجاع النظام مِن نسخة احتياطية", + "log_backup_restore_app": "استرجاع '{}' مِن نسخة احتياطية", + "log_remove_on_failed_install": "حذف '{}' بعد فشل التنصيب", + "log_domain_add": "إضافة النطاق '{}' إلى إعدادات النظام", + "log_domain_remove": "حذف النطاق '{}' مِن إعدادات النظام", + "log_dyndns_subscribe": "تسجيل اسم نطاق واي يونوهوست فرعي '{}'", + "log_dyndns_update": "تحديث عنوان الإيبي ذي الصلة مع اسم النطاق الفرعي واي يونوهوست '{}'", + "log_letsencrypt_cert_install": "تنصيب شهادة Let’s Encrypt على النطاق '{}'", + "log_selfsigned_cert_install": "تنصيب شهادة موقَّعَة ذاتيا على اسم النطاق '{}'", + "log_letsencrypt_cert_renew": "تجديد شهادة Let's Encrypt لـ '{}'", + "log_service_enable": "تنشيط خدمة '{}'", + "log_user_create": "إضافة المستخدم '{}'", + "log_user_delete": "حذف المستخدم '{}'", + "log_user_update": "تحديث معلومات المستخدم '{}'", + "log_tools_maindomain": "جعل '{}' كنطاق أساسي", + "log_tools_upgrade": "تحديث حُزم ديبيان", + "log_tools_shutdown": "إطفاء الخادم", + "log_tools_reboot": "إعادة تشغيل الخادم", + "migration_description_0005_postgresql_9p4_to_9p6": "تهجير قواعد البيانات مِن postgresql 9.4 إلى 9.6", + "service_description_dnsmasq": "مُكلَّف بتحليل أسماء النطاقات (DNS)", + "service_description_mysql": "يقوم بتخزين بيانات التطبيقات (قواعد بيانات SQL)", + "service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد", + "service_description_yunohost-firewall": "يريد فتح و غلق منافذ الإتصال إلى الخدمات", + "users_available": "المستخدمون المتوفرون:" } From 514faf3518830110708ba48736dc0eb98f362893 Mon Sep 17 00:00:00 2001 From: Quenti Date: Thu, 1 Nov 2018 19:23:15 +0000 Subject: [PATCH 106/250] Translated using Weblate (Occitan) Currently translated at 100.0% (454 of 454 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 4ead4d637..ed9bd4017 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -453,7 +453,7 @@ "migration_0005_postgresql_94_not_installed": "Postgresql es pas installat sul sistèma. Pas res de far !", "migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada ! Quicòm d’estranh a degut arribar a vòstre sistèma :( ...", "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.", - "recommend_to_add_first_user": "La post installacion es acabda, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create » o ben l’interfàcia d’administracion.", + "recommend_to_add_first_user": "La post installacion es acabada, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create » o ben l’interfàcia d’administracion.", "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx", "users_available": "Lista dels utilizaires disponibles :" } From 4ee35833764672dd669eed407e07b4602954e084 Mon Sep 17 00:00:00 2001 From: MyNameIsTroll Date: Thu, 1 Nov 2018 19:51:28 +0000 Subject: [PATCH 107/250] Translated using Weblate (Portuguese) Currently translated at 42.0% (191 of 454 strings) --- locales/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index 7196e366f..80a0d5ddd 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -193,5 +193,5 @@ "backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.", "backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido", "backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo", - "app_change_url_identical_domains": "45/5000\nO antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer." + "app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer." } From 0856022fcde4bba90c9c9ae1a9800b8248e6369c Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Fri, 2 Nov 2018 16:26:49 +0000 Subject: [PATCH 108/250] Translated using Weblate (Italian) Currently translated at 55.0% (250 of 454 strings) --- locales/it.json | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/locales/it.json b/locales/it.json index cb5f35d81..ebc262f68 100644 --- a/locales/it.json +++ b/locales/it.json @@ -23,7 +23,7 @@ "service_disabled": "Il servizio '{service:s}' è stato disattivato", "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}'", + "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...", @@ -31,11 +31,11 @@ "admin_password": "Password dell'amministrazione", "admin_password_change_failed": "Impossibile cambiare la password", "admin_password_changed": "La password dell'amministrazione è stata cambiata", - "app_incompatible": "L'app non è compatibile con la tua versione di Yunohost", + "app_incompatible": "L'applicazione {app} è incompatibile con la tua versione YunoHost", "app_install_files_invalid": "Non sono validi i file di installazione", - "app_location_already_used": "Un'app è già installata in questa posizione", - "app_location_install_failed": "Impossibile installare l'applicazione in questa posizione", - "app_manifest_invalid": "Manifesto dell'applicazione non valido", + "app_location_already_used": "L'applicazione '{app}' è già installata in questo percorso ({path})", + "app_location_install_failed": "Impossibile installare l'applicazione in questo percorso perchè andrebbe in conflitto con l'applicazione '{other_app}' già installata in '{other_path}'", + "app_manifest_invalid": "Manifesto dell'applicazione non valido: {error}", "app_no_upgrade": "Nessun applicazione da aggiornare", "app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente", "app_not_properly_removed": "{app:s} non è stata correttamente rimossa", @@ -44,13 +44,13 @@ "app_sources_fetch_failed": "Impossibile riportare i file sorgenti", "app_upgrade_failed": "Impossibile aggiornare {app:s}", "app_upgraded": "{app:s} è stata aggiornata", - "appslist_fetched": "La lista delle applicazioni è stata recuperata", - "appslist_removed": "La lista delle applicazioni è stata rimossa", - "app_package_need_update": "Il pacchetto dell'app deve esser aggiornato per seguire le modifiche di Yunohost", - "app_requirements_checking": "Controllo dei pacchetti necessari...", - "app_requirements_failed": "Impossibile rispondere ai requisiti: {error}", - "app_requirements_unmeet": "Non sono soddisfatti i requisiti, il pacchetto {pkgname} ({version}) deve esser {spec}", - "appslist_unknown": "Lista di applicazioni sconosciuta", + "appslist_fetched": "La lista delle applicazioni {appslist:s} è stata recuperata", + "appslist_removed": "La lista delle applicazioni {appslist:s} è stata rimossa", + "app_package_need_update": "Il pacchetto dell'applicazione {app} deve essere aggiornato per seguire i cambiamenti di YunoHost", + "app_requirements_checking": "Controllo i pacchetti richiesti per {app}...", + "app_requirements_failed": "Impossibile soddisfare i requisiti per {app}: {error}", + "app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}", + "appslist_unknown": "Lista di applicazioni {appslist:s} sconosciuta.", "ask_current_admin_password": "Password attuale dell'amministrazione", "ask_firstname": "Nome", "ask_lastname": "Cognome", @@ -65,8 +65,8 @@ "app_argument_required": "L'argomento '{name:s}' è requisito", "app_id_invalid": "Identificativo dell'applicazione non valido", "app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato", - "appslist_retrieve_error": "Non è possibile riportare la lista remota delle applicazioni: {error}", - "appslist_retrieve_bad_format": "Il file recuperato non è una lista di applicazioni valida", + "appslist_retrieve_error": "Impossibile recuperare la lista di applicazioni remote {appslist:s}: {error:s}", + "appslist_retrieve_bad_format": "Il file recuperato per la lista di applicazioni {appslist:s} non è valido", "backup_archive_broken_link": "Non è possibile accedere al archivio di backup (link rotto verso {path:s})", "backup_archive_hook_not_exec": "Il hook '{hook:s}' non è stato eseguito in questo backup", "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto", @@ -114,7 +114,7 @@ "dyndns_no_domain_registered": "Nessuno dominio è stato registrato con DynDNS", "dyndns_registered": "Il dominio DynDNS è stato registrato", "dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}", - "dyndns_unavailable": "Il sottodominio DynDNS non è disponibile", + "dyndns_unavailable": "Dominio {domain:s} non disponibile.", "executing_command": "Esecuzione del comando '{command:s}'...", "executing_script": "Esecuzione dello script '{script:s}'...", "extracting": "Estrazione...", @@ -185,7 +185,7 @@ "network_check_smtp_ok": "La posta in uscita (SMTP porta 25) non è bloccata", "no_restore_script": "Nessuno script di ripristino trovato per l'applicazone '{app:s}'", "package_unexpected_error": "Un'errore inaspettata si è verificata durante il trattamento del pacchetto '{pkgname}'", - "restore_hook_unavailable": "Il hook di ripristino '{hook:s}' non è disponibile sul tuo sistema", + "restore_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'applcicazione '{app:s}'...", "restore_running_hooks": "Esecuzione dei hook di ripristino...", @@ -203,14 +203,14 @@ "service_conf_up_to_date": "La configurazione è già aggiornata per il servizio '{service}'", "service_conf_updated": "La configurazione è stata aggiornata per il servizio '{service}'", "service_conf_would_be_updated": "La configurazione sarebbe stata aggiornata per il servizio '{service}'", - "service_disable_failed": "Impossibile disattivare il servizio '{service:s}'", - "service_enable_failed": "Impossibile attivare il servizio '{service:s}'", + "service_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_no_log": "Nessuno registro da visualizzare per il servizio '{service:s}'", "service_regenconf_dry_pending_applying": "Verificazione della configurazione in attesa che sarebbe stata applicata per il servizio '{service}'...", "service_regenconf_failed": "Impossibile rigenerare la configurazione per il/i servizio/i: {services}", "service_regenconf_pending_applying": "Applicazione della configurazione in attesa per il servizio '{service}'...", - "service_start_failed": "Impossibile avviare il servizio '{service:s}'", + "service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}", "service_started": "Il servizio '{service:s}' è stato avviato", "service_status_failed": "Impossibile determinare lo stato del servizio '{service:s}'", "service_stopped": "Il servizio '{service:s}' è stato fermato", From b3e60f862329958fadc5bb675ec418e55e721fd3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 5 Nov 2018 18:04:03 +0000 Subject: [PATCH 109/250] Fix a few mistakes because of copypasta?... --- locales/fr.json | 68 ++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 33380f9aa..c48a8e371 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -294,7 +294,7 @@ "app_change_url_identical_domains": "L’ancien et le nouveau couple domaine/chemin sont identiques pour {domain:s}{path:s}, aucune action.", "app_change_url_no_script": "L’application {app_name:s} ne prend pas encore en charge le changement d’URL. Vous devriez peut-être la mettre à jour.", "app_change_url_success": "L’URL de l’application {app:s} a été changée en {domain:s}{path:s}", - "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante\n{apps:s} 1", + "app_location_unavailable": "Cette URL n’est pas disponible ou est en conflit avec une application existante\n{apps:s}", "app_already_up_to_date": "{app:s} est déjà à jour", "invalid_url_format": "Format d’URL non valide", "global_settings_bad_choice_for_enum": "La valeur du paramètre {setting:s} est incorrecte. Reçu : {received_type:s}; attendu : {expected_type:s}", @@ -418,42 +418,42 @@ "service_description_yunohost-api": "permet les interactions entre l’interface web de YunoHost et le système", "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services", "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faîtes.", - "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : '{md_file} 1'", - "log_category_404": "La catégorie de log '{category} 1' n'existe pas", - "log_link_to_log": "Log complet de cette opération : ' 1{desc} 2 3'", - "log_help_to_get_log": "Pour voir le log de cette opération '{desc} 1', utiliser la commande 'yunohost log display {name} 2'", - "log_link_to_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci 2de fournir le log complet de l'opération 3", - "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, la restauration de vos applications php peut ne pas aboutir (reason: {error:s} 1)", - "log_help_to_get_failed_log": "L'opération '{desc} 1' a échouée ! Pour avoir de l'aide, merci de partager le log de cette opération en utilisant la commande 'yunohost log display {name} 2 --share'", - "log_does_exists": "Il n'existe pas de log de l'opération ayant pour nom '{log} 1', utiliser 'yunohost log list pour voir tous les fichiers de logs disponibles'", + "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : '{md_file}'", + "log_category_404": "La catégorie de log '{category}' n'existe pas", + "log_link_to_log": "Log complet de cette opération : ' {desc} '", + "log_help_to_get_log": "Pour voir le log de cette opération '{desc}', utiliser la commande 'yunohost log display {name}'", + "log_link_to_failed_log": "L'opération '{desc}' a échouée ! Pour avoir de l'aide, merci de fournir le log complet de l'opération", + "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, la restauration de vos applications php peut ne pas aboutir (reason: {error:s})", + "log_help_to_get_failed_log": "L'opération '{desc}' a échouée ! Pour avoir de l'aide, merci de partager le log de cette opération en utilisant la commande 'yunohost log display {name} --share'", + "log_does_exists": "Il n'existe pas de log de l'opération ayant pour nom '{log}', utiliser 'yunohost log list pour voir tous les fichiers de logs disponibles'", "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement", - "log_app_addaccess": "Ajouter l'accès à '{} 1'", - "log_app_removeaccess": "Enlever l'accès à '{} 1'", - "log_app_clearaccess": "Retirer tous les accès à '{} 1'", + "log_app_addaccess": "Ajouter l'accès à '{}'", + "log_app_removeaccess": "Enlever l'accès à '{}'", + "log_app_clearaccess": "Retirer tous les accès à '{}'", "log_app_fetchlist": "Ajouter une liste d'application", "log_app_removelist": "Enlever une liste d'application", - "log_app_change_url": "Changer l'url de l'application '{} 1'", - "log_app_install": "Installer l'application '{} 1'", - "log_app_remove": "Enlever l'application '{} 1'", - "log_app_upgrade": "Mettre à jour l'application '{} 1'", - "log_app_makedefault": "Faire de '{} 1' l'application par défaut", - "log_available_on_yunopaste": "Le log est désormais disponible via {url} 1", + "log_app_change_url": "Changer l'url de l'application '{}'", + "log_app_install": "Installer l'application '{}'", + "log_app_remove": "Enlever l'application '{}'", + "log_app_upgrade": "Mettre à jour l'application '{}'", + "log_app_makedefault": "Faire de '{}' l'application par défaut", + "log_available_on_yunopaste": "Le log est désormais disponible via {url}", "log_backup_restore_system": "Restaurer le système depuis une sauvegarde", - "log_backup_restore_app": "Restaurer '{} 1' depuis une sauvegarde", - "log_remove_on_failed_restore": "Retirer '{} 1' après la restauration depuis une sauvegarde qui a échouée", - "log_remove_on_failed_install": "Enlever '{} 1' après une installation échouée", - "log_domain_add": "Ajouter le domaine '{} 1' dans la configuration du système", - "log_domain_remove": "Enlever le domaine '{} 1' de la configuration du système", - "log_dyndns_subscribe": "Souscrire au sous-domaine '{} 1' de Yunohost", - "log_dyndns_update": "Mettre à jour l'adresse ip associée à votre sous-domaine Yunohost '{} 1'", - "log_letsencrypt_cert_install": "Installer le certificat Let's encryt sur le domaine '{} 1'", - "log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine '{} 1'", - "log_letsencrypt_cert_renew": "Renouveler le certificat Let's encrypt de '{} 1'", - "log_service_enable": "Activer le service '{} 1'", - "log_service_regen_conf": "Régénérer la configuration système de '{} 1'", - "log_user_create": "Ajouter l'utilisateur '{} 1'", - "log_user_delete": "Enlever l'utilisateur '{} 1'", - "log_user_update": "Mettre à jour les informations de l'utilisateur '{} 1'", + "log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde", + "log_remove_on_failed_restore": "Retirer '{}' après la restauration depuis une sauvegarde qui a échouée", + "log_remove_on_failed_install": "Enlever '{}' après une installation échouée", + "log_domain_add": "Ajouter le domaine '{}' dans la configuration du système", + "log_domain_remove": "Enlever le domaine '{}' de la configuration du système", + "log_dyndns_subscribe": "Souscrire au sous-domaine '{}' de Yunohost", + "log_dyndns_update": "Mettre à jour l'adresse ip associée à votre sous-domaine Yunohost '{}'", + "log_letsencrypt_cert_install": "Installer le certificat Let's encryt sur le domaine '{}'", + "log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine '{}'", + "log_letsencrypt_cert_renew": "Renouveler le certificat Let's encrypt de '{}'", + "log_service_enable": "Activer le service '{}'", + "log_service_regen_conf": "Régénérer la configuration système de '{}'", + "log_user_create": "Ajouter l'utilisateur '{}'", + "log_user_delete": "Enlever l'utilisateur '{}'", + "log_user_update": "Mettre à jour les informations de l'utilisateur '{}'", "log_tools_maindomain": "Faire de '{} 1' le domaine principal", "log_tools_migrations_migrate_forward": "Migrer", "log_tools_migrations_migrate_backward": "Revenir en arrière", @@ -466,7 +466,7 @@ "migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de postgresql 9.4 vers 9.6", "migration_0005_postgresql_94_not_installed": "Postgresql n'a pas été installé sur votre système. Rien à faire !", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais aucune version Postgresql 9.6 ! Quelque chose d'étrange a du arriver à votre système :( ...", - "migration_0005_not_enough_space": "Pas assez d'espace libre disponible sur {path} 1 pour permettre de faire la migration actuellement :(.", + "migration_0005_not_enough_space": "Pas assez d'espace libre disponible sur {path} pour permettre de faire la migration actuellement :(.", "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d'au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant 'yunohost user create' ou l'interface d'administration.", "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx", "users_available": "Liste des utilisateurs disponibles :" From b67105f04bf0d7e52649040ccca277b40ae21bc0 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Tue, 6 Nov 2018 01:00:57 +0100 Subject: [PATCH 110/250] [enh] Wait for dpkg lock to be free --- data/helpers.d/package | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/data/helpers.d/package b/data/helpers.d/package index db3b50e0e..3ac93a9db 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -1,3 +1,26 @@ +# Check if apt is free to use, or wait, until timeout. +# +# [internal] +# +# usage: ynh_is_dpkg_free +ynh_is_dpkg_free() { + local try + # With seq 1 17, timeout will be almost 30 minutes + for try in `seq 1 17` + do + # Check if /var/lib/dpkg/lock is used by another process + if sudo lsof /var/lib/dpkg/lock > /dev/null + then + echo "apt is already in use..." + # Sleep an exponential time at each round + sleep $(( try * try )) + else + break + fi + done + echo "apt still used, but timeout reached !" +} + # Check either a package is installed or not # # example: ynh_package_is_installed 'yunohost' && echo "ok" @@ -5,6 +28,7 @@ # usage: ynh_package_is_installed name # | arg: name - the package name to check ynh_package_is_installed() { + ynh_is_dpkg_free dpkg-query -W -f '${Status}' "$1" 2>/dev/null \ | grep -c "ok installed" &>/dev/null } @@ -30,6 +54,7 @@ ynh_package_version() { # # usage: ynh_apt update ynh_apt() { + ynh_is_dpkg_free DEBIAN_FRONTEND=noninteractive sudo apt-get -y $@ } @@ -105,6 +130,7 @@ ynh_package_install_from_equivs () { # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg # Install missing dependencies with ynh_package_install + ynh_is_dpkg_free (cp "$controlfile" "${TMPDIR}/control" && cd "$TMPDIR" \ && equivs-build ./control 1>/dev/null \ && sudo dpkg --force-depends \ From 92a886e1bb8d128b4c7d75d00f6d24605e28a935 Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Wed, 7 Nov 2018 13:20:40 +0000 Subject: [PATCH 111/250] Translated using Weblate (Italian) Currently translated at 53.7% (250 of 465 strings) --- locales/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index ebc262f68..cfab6565f 100644 --- a/locales/it.json +++ b/locales/it.json @@ -126,7 +126,7 @@ "hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}", "hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}", "hook_name_unknown": "Nome di hook '{name:s}' sconosciuto", - "installation_complete": "Installazione finita", + "installation_complete": "Installazione completata", "installation_failed": "Installazione fallita", "ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta", "iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta", From e1ceea2ace7c1a4cbad5b7d971886378c9c684ff Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 7 Nov 2018 20:16:55 +0000 Subject: [PATCH 112/250] Translated using Weblate (Occitan) Currently translated at 100.0% (465 of 465 strings) --- locales/oc.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index ed9bd4017..fc75b8bae 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -455,5 +455,15 @@ "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.", "recommend_to_add_first_user": "La post installacion es acabada, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create » o ben l’interfàcia d’administracion.", "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx", - "users_available": "Lista dels utilizaires disponibles :" + "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 per definir un nòu senhal d’utilizaire. 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).", + "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.", + "migration_0006_done": "Lo senhal root es estat remplaçat pel senhal admin.", + "password_listed": "Aqueste senhal fa part dels senhals mai utilizats del monde. Volgatz ben ne causir un mai unic.", + "password_too_simple_1": "Lo senhal deu conténer almens 8 caractèrs", + "password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas e de minusculas", + "password_too_simple_3": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials", + "password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials" } From aef4dd1770da6c2c68706e3f9bf0890acb84b951 Mon Sep 17 00:00:00 2001 From: "Anonymous (technical account)" Date: Wed, 7 Nov 2018 20:26:41 +0000 Subject: [PATCH 113/250] Translated using Weblate (Occitan) Currently translated at 100.0% (465 of 465 strings) --- locales/oc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index fc75b8bae..c0e835ff0 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -465,5 +465,6 @@ "password_too_simple_1": "Lo senhal deu conténer almens 8 caractèrs", "password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas e de minusculas", "password_too_simple_3": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials", - "password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials" + "password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials", + "root_password_desynchronized": "Lo senhal de l'administrator es estat cambiat, mas YunoHost a pas pogut l'espandir al senhal root !" } From 563321141ab68873a7a4679dae0ebc3927a04015 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Tue, 6 Nov 2018 22:39:15 +0000 Subject: [PATCH 114/250] Translated using Weblate (French) Currently translated at 97.6% (454 of 465 strings) --- locales/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index c48a8e371..814f3acd4 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -465,8 +465,8 @@ "migration_description_0004_php5_to_php7_pools": "Reconfigurez le pool PHP pour utiliser PHP 7 au lieu de 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de postgresql 9.4 vers 9.6", "migration_0005_postgresql_94_not_installed": "Postgresql n'a pas été installé sur votre système. Rien à faire !", - "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais aucune version Postgresql 9.6 ! Quelque chose d'étrange a du arriver à votre système :( ...", - "migration_0005_not_enough_space": "Pas assez d'espace libre disponible sur {path} pour permettre de faire la migration actuellement :(.", + "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais pas Postgresql 9.6 !? Quelque chose d’étrange a dû arriver à votre système :( …", + "migration_0005_not_enough_space": "Il n’y a pas assez d’espace libre de disponible sur {path} pour lancer maintenant la migration :(.", "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d'au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant 'yunohost user create' ou l'interface d'administration.", "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx", "users_available": "Liste des utilisateurs disponibles :" From 738b8c0695167f6b2603671c44dbc0c5d65a2418 Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Thu, 8 Nov 2018 15:09:29 +0000 Subject: [PATCH 115/250] Translated using Weblate (Italian) Currently translated at 55.9% (260 of 465 strings) --- locales/it.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index cfab6565f..f0597d3a5 100644 --- a/locales/it.json +++ b/locales/it.json @@ -250,5 +250,15 @@ "certmanager_certificate_fetching_or_enabling_failed": "L'attivazione del nuovo certificato per {domain:s} sembra fallita in qualche modo...", "certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain:s} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!", "certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è a scadere! Usa --force per ignorare", - "certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx" + "certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx", + "app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Guarda se `app changeurl` è disponibile.", + "app_already_up_to_date": "{app:s} è già aggiornata", + "app_change_no_change_url_script": "L'applicazione {app_name:s} non supporta ancora il cambio del proprio URL, potrebbe essere necessario aggiornarla.", + "app_change_url_failed_nginx_reload": "Riavvio di nginx fallito. Questo è il risultato di 'nginx -t':\n{nginx_errors:s}", + "app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain:s}{path:s}'), nessuna operazione necessaria.", + "app_change_url_no_script": "L'applicazione '{app_name:s}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornare l'applicazione.", + "app_change_url_success": "URL dell'applicazione {app:s} cambiato con successo in {domain:s}{path:s}", + "app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio {domain} non riuscita perchè è già stata impostata per l'altra applicazione '{other_app}'", + "app_location_unavailable": "Questo URL non è disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}", + "app_upgrade_app_name": "Aggiornando l'applicazione {app}..." } From 093006d33403f7aa2c46c70a4f4b17df2e7e4ac1 Mon Sep 17 00:00:00 2001 From: Quenti Date: Wed, 7 Nov 2018 20:27:19 +0000 Subject: [PATCH 116/250] Translated using Weblate (Occitan) Currently translated at 100.0% (465 of 465 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index c0e835ff0..7a4814389 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -466,5 +466,5 @@ "password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas e de minusculas", "password_too_simple_3": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials", "password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials", - "root_password_desynchronized": "Lo senhal de l'administrator es estat cambiat, mas YunoHost a pas pogut l'espandir al senhal root !" + "root_password_desynchronized": "Lo senhal de l’administrator es estat cambiat, mas YunoHost a pas pogut l’espandir al senhal root !" } From aa82bc4a54aaf26dc4a45ccab0e3a3b8d63ab1e4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 8 Nov 2018 16:13:31 +0000 Subject: [PATCH 117/250] Update changelog for 3.3.0 --- debian/changelog | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/debian/changelog b/debian/changelog index 1562c4d2f..83e9e9269 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,36 @@ +yunohost (3.3.0) testing; urgency=low + + Highlights + ========== + + * [enh] Synchronize root password with admin password (#527) + * [enh] Check for weak passwords whenever a password is defined (#196) + * [fix] 'dyndns update' now checks the upstream DNS record (#519) + * [fix] Update Metronome configuration file to v3.11 standard (#559) + * [fix] Some php conf files wre not properly removed when an app was uninstalled (#566) + * [i18n] Improve Catalan, French, Occitan, Portuguese, Arabic, Italian translations + + Misc + ==== + + * [enh] Add OCSP Stapling to nginx configuration if using Lets Encrypt (#533) + * [enh] Add CAA record in recommended DNS conf (#528) + * [helpers] Add `ynh_delete_file_checksum` (#524) + * [helpers] When using `ynh_setup_source`, silent unecessary messages (#545) + * [helpers] Use more blocks for dd in `ynh_string_random` (#569) + * [fix] Potential key error when retrieving install_time (#551) + * [fix] Allow `-` in user last names (#565) + * [fix] Fix possible HTTP2 issue with curl (#547) + * [fix] Fix BASE/URI in ldap conf (#554) + * [fix] Use random serial number for CA (prevent browser from complaining about some selfsigned certs) (#557) + * [enh] Pass Host header to YunoHost API (#560) + * [enh] Sort backup list according to their date (#562) + * [fix] Improve UX when admin tries to allocate reserved email alias (#553) + + Thanks to all contributors (ljf, irinia11y, Maniack, xaloc33, Bram, flashemade, Maranda, Josue, frju365, Aleks, randomstuff, jershon, Genma, Quent-in, MyNameIsTroll, ButterflyOfFire, Jibec, silkevicious) ! <3 + + -- Alexandre Aubin Thu, 08 Nov 2018 17:09:00 +0000 + yunohost (3.2.2) stable; urgency=low * [hotfix] mod_auth_ldap: reflect SASL API changes in latest Metronome (#546) From 7f1d9fb81e6b626cf70fd0e00713447dd4c9a003 Mon Sep 17 00:00:00 2001 From: airwoodix Date: Tue, 13 Nov 2018 11:27:21 +0100 Subject: [PATCH 118/250] [fix] app_removeaccess call set.add --- 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 efdac70d7..8b3ffecdc 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1036,7 +1036,7 @@ def app_removeaccess(auth, apps, users=[]): else: for allowed_user in user_list(auth)['users'].keys(): if allowed_user not in users: - allowed_users.append(allowed_user) + allowed_users.add(allowed_user) operation_logger.related_to += [ ('user', x) for x in allowed_users ] operation_logger.flush() From 4d0deea5638d699491f8f2b0955698a00b60592a Mon Sep 17 00:00:00 2001 From: airwoodix Date: Sat, 17 Nov 2018 23:27:33 +0100 Subject: [PATCH 119/250] Fix app_addaccess behaviour when 'allowed_users' is initially empty --- 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 8b3ffecdc..e26700c49 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -965,7 +965,7 @@ def app_addaccess(auth, apps, users=[]): operation_logger.start() allowed_users = set() - if 'allowed_users' in app_settings: + if 'allowed_users' in app_settings and app_settings['allowed_users']: allowed_users = set(app_settings['allowed_users'].split(',')) for allowed_user in users: From b5afd0abc42768eeee7e9ee11f6c723f79b304b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Nov 2018 17:55:30 +0100 Subject: [PATCH 120/250] We do need to use the serial file and to generate it ourselves --- data/hooks/conf_regen/02-ssl | 2 +- data/templates/ssl/openssl.cnf | 2 +- src/yunohost/tools.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index 555ef3cf8..ab4280af9 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -24,7 +24,7 @@ do_init_regen() { # initialize some files [[ -f "${ssl_dir}/serial" ]] \ - || echo "00" > "${ssl_dir}/serial" + || openssl rand -hex 19 > "${ssl_dir}/serial" [[ -f "${ssl_dir}/index.txt" ]] \ || touch "${ssl_dir}/index.txt" diff --git a/data/templates/ssl/openssl.cnf b/data/templates/ssl/openssl.cnf index ac8c422e3..fa5d19fa3 100644 --- a/data/templates/ssl/openssl.cnf +++ b/data/templates/ssl/openssl.cnf @@ -43,7 +43,7 @@ unique_subject = no # Set to 'no' to allow creation of new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/ca/cacert.pem # The CA certificate -#serial = $dir/serial # The current serial number +serial = $dir/serial # The current serial number #crlnumber = $dir/crlnumber # the current crl number # must be commented out to leave a V1 CRL crl = $dir/crl.pem # The current CRL diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 2817a3057..271947b3d 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -383,6 +383,8 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Create SSL CA service_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, From 662666fb5a3b0c554c5cf83e574c708a4470805a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Nov 2018 17:55:51 +0100 Subject: [PATCH 121/250] Readability improvement --- data/hooks/conf_regen/02-ssl | 57 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/data/hooks/conf_regen/02-ssl b/data/hooks/conf_regen/02-ssl index ab4280af9..963ec12ef 100755 --- a/data/hooks/conf_regen/02-ssl +++ b/data/hooks/conf_regen/02-ssl @@ -29,41 +29,52 @@ do_init_regen() { || 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" # create default certificates - if [[ ! -f /etc/yunohost/certs/yunohost.org/ca.pem ]]; then + 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" \ - /etc/yunohost/certs/yunohost.org/ca.pem - ln -sf /etc/yunohost/certs/yunohost.org/ca.pem \ - /etc/ssl/certs/ca-yunohost_crt.pem + + 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 fi - if [[ ! -f /etc/yunohost/certs/yunohost.org/crt.pem ]]; then + if [[ ! -f "$ynh_crt" ]]; then echo -e "\n# Creating initial key and certificate (?)\n" >>$LOGFILE - openssl req -new -config "$openssl_conf" \ - -days 730 -out "${ssl_dir}/certs/yunohost_csr.pem" \ - -keyout "${ssl_dir}/certs/yunohost_key.pem" -nodes -batch >>$LOGFILE 2>&1 - openssl ca -config "$openssl_conf" \ - -days 730 -in "${ssl_dir}/certs/yunohost_csr.pem" \ - -out "${ssl_dir}/certs/yunohost_crt.pem" -batch >>$LOGFILE 2>&1 + + 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 + + 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 last_cert=$(ls $ssl_dir/newcerts/*.pem | sort -V | tail -n 1) chmod 640 "${ssl_dir}/certs/yunohost_key.pem" chmod 640 "$last_cert" - cp "${ssl_dir}/certs/yunohost_key.pem" \ - /etc/yunohost/certs/yunohost.org/key.pem - cp "$last_cert" \ - /etc/yunohost/certs/yunohost.org/crt.pem - ln -sf /etc/yunohost/certs/yunohost.org/crt.pem \ - /etc/ssl/certs/yunohost_crt.pem - ln -sf /etc/yunohost/certs/yunohost.org/key.pem \ - /etc/ssl/private/yunohost_key.pem + cp "${ssl_dir}/certs/yunohost_key.pem" "$ynh_key" + cp "$last_cert" "$ynh_crt" + ln -sf "$ynh_crt" /etc/ssl/certs/yunohost_crt.pem + ln -sf "$ynh_key" /etc/ssl/private/yunohost_key.pem fi } From eb8792376c1ec41a894b4f0963f0477bf2312354 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Tue, 20 Nov 2018 20:47:36 +0100 Subject: [PATCH 122/250] [enh] Better name ynh_wait_dpkg_free --- data/helpers.d/package | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 3ac93a9db..22adb9b15 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -2,8 +2,8 @@ # # [internal] # -# usage: ynh_is_dpkg_free -ynh_is_dpkg_free() { +# usage: ynh_wait_dpkg_free +ynh_wait_dpkg_free() { local try # With seq 1 17, timeout will be almost 30 minutes for try in `seq 1 17` @@ -28,7 +28,7 @@ ynh_is_dpkg_free() { # usage: ynh_package_is_installed name # | arg: name - the package name to check ynh_package_is_installed() { - ynh_is_dpkg_free + ynh_wait_dpkg_free dpkg-query -W -f '${Status}' "$1" 2>/dev/null \ | grep -c "ok installed" &>/dev/null } @@ -54,7 +54,7 @@ ynh_package_version() { # # usage: ynh_apt update ynh_apt() { - ynh_is_dpkg_free + ynh_wait_dpkg_free DEBIAN_FRONTEND=noninteractive sudo apt-get -y $@ } @@ -130,7 +130,7 @@ ynh_package_install_from_equivs () { # Create a fake deb package with equivs-build and the given control file # Install the fake package without its dependencies with dpkg # Install missing dependencies with ynh_package_install - ynh_is_dpkg_free + ynh_wait_dpkg_free (cp "$controlfile" "${TMPDIR}/control" && cd "$TMPDIR" \ && equivs-build ./control 1>/dev/null \ && sudo dpkg --force-depends \ From 98ae285c91ac1309e9963362c59e107c1d0b7fb5 Mon Sep 17 00:00:00 2001 From: ljf Date: Tue, 20 Nov 2018 22:48:35 +0100 Subject: [PATCH 123/250] [enh] Add lsof dependencies in case we decide to remove mariadb --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index be63436da..1f4995195 100644 --- a/debian/control +++ b/debian/control @@ -26,7 +26,7 @@ Depends: ${python:Depends}, ${misc:Depends} , metronome , rspamd (>= 1.6.0), redis-server, opendkim-tools , haveged, fake-hwclock - , equivs + , equivs, lsof Recommends: yunohost-admin , openssh-server, ntp, inetutils-ping | iputils-ping , bash-completion, rsyslog, etckeeper From 5a22b400eb0f2e49199e9161eabaa7fd4e13dda1 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Wed, 21 Nov 2018 18:37:08 +0100 Subject: [PATCH 124/250] [fix] Unknown variable password in user_update --- 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 baf52eb2f..bb39dd58e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -313,7 +313,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if change_password: # Ensure sufficiently complex password - assert_password_is_strong_enough("user", password) + assert_password_is_strong_enough("user", change_password) new_attr_dict['userPassword'] = _hash_user_password(change_password) From 8ba552c0ff9a8f9c4579f68a6c6aecab2d5334f5 Mon Sep 17 00:00:00 2001 From: Syed Faraaz Ahmad Date: Fri, 23 Nov 2018 15:26:10 +0530 Subject: [PATCH 125/250] Fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4033bd6fb..4bd070bea 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This repository is the core of YunoHost code. - [Modules for the XMPP server Metronome](https://github.com/YunoHost/yunohost/tree/stable/lib/metronome/modules). - [Debian files](https://github.com/YunoHost/yunohost/tree/stable/debian) for package creation. -## How does it works? +## How does it work? - Python core scripts are accessible through two interfaces thanks to the [moulinette framework](https://github.com/YunoHost/moulinette): - [CLI](https://en.wikipedia.org/wiki/Command-line_interface) for `yunohost` command. - [API](https://en.wikipedia.org/wiki/Application_programming_interface) for [web administration module](https://github.com/YunoHost/yunohost-admin) (other modules could be implemented). From 6bfcedfe1d30d1579dd093b983545017d971651d Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Fri, 23 Nov 2018 13:21:37 +0100 Subject: [PATCH 126/250] Do not fail on missing fail2ban config during the backup (#558) * Do not fail on missing file during the backup * Fix for allow to backup without a fail2ban config file * Tab -> space consistency --- data/helpers.d/filesystem | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index c07de2ece..badc0e997 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -60,8 +60,16 @@ ynh_backup() { # ============================================================================== # Be sure the source path is not empty [[ -e "${SRC_PATH}" ]] || { - echo "Source path '${SRC_PATH}' does not exist" >&2 - return 1 + echo "!!! Source path '${SRC_PATH}' does not exist !!!" >&2 + + # This is a temporary fix for fail2ban config files missing after the migration to stretch. + if echo "${SRC_PATH}" | grep --quiet "/etc/fail2ban" + then + touch "${SRC_PATH}" + echo "The missing file will be replaced by a dummy one for the backup !!!" >&2 + else + return 1 + fi } # Transform the source path as an absolute path From ae0497b4f06f91e106239757da25f78e00099c02 Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Thu, 8 Nov 2018 15:21:37 +0000 Subject: [PATCH 127/250] Translated using Weblate (Italian) Currently translated at 56.3% (262 of 465 strings) --- locales/it.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index f0597d3a5..2fb843fc0 100644 --- a/locales/it.json +++ b/locales/it.json @@ -260,5 +260,7 @@ "app_change_url_success": "URL dell'applicazione {app:s} cambiato con successo in {domain:s}{path:s}", "app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio {domain} non riuscita perchè è già stata impostata per l'altra applicazione '{other_app}'", "app_location_unavailable": "Questo URL non è disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}", - "app_upgrade_app_name": "Aggiornando l'applicazione {app}..." + "app_upgrade_app_name": "Aggiornando l'applicazione {app}...", + "app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni", + "appslist_corrupted_json": "Caricamento della lista delle applicazioni non riuscita. Sembra che {filename:s} sia corrotto." } From b6c51b39fafa8670f5cb1b496e44ff582b3c3f75 Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Thu, 8 Nov 2018 15:34:09 +0000 Subject: [PATCH 128/250] Translated using Weblate (Italian) Currently translated at 59.1% (275 of 465 strings) --- locales/it.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 2fb843fc0..69be4e394 100644 --- a/locales/it.json +++ b/locales/it.json @@ -262,5 +262,18 @@ "app_location_unavailable": "Questo URL non è disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}", "app_upgrade_app_name": "Aggiornando l'applicazione {app}...", "app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni", - "appslist_corrupted_json": "Caricamento della lista delle applicazioni non riuscita. Sembra che {filename:s} sia corrotto." + "appslist_corrupted_json": "Caricamento della lista delle applicazioni non riuscita. Sembra che {filename:s} sia corrotto.", + "appslist_could_not_migrate": "Migrazione della lista delle applicazioni {appslist:s} non riuscita! Impossibile analizzare l'URL... La vecchia operazione pianificata è stata tenuta in {bkp_file:s}.", + "appslist_migrating": "Migrando la lista di applicazioni {appslist:s} ...", + "appslist_name_already_tracked": "C'è già una lista di applicazioni registrata con il nome {name:s}.", + "appslist_url_already_tracked": "C'è già una lista di applicazioni registrata con URL {url:s}.", + "ask_path": "Percorso", + "backup_abstract_method": "Questo metodo di backup non è ancora stato implementato", + "backup_applying_method_borg": "Inviando tutti i file da salvare nel backup nel deposito borg-backup...", + "backup_applying_method_copy": "Copiando tutti i files nel backup...", + "backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method:s}'...", + "backup_applying_method_tar": "Creando l'archivio tar del backup...", + "backup_archive_mount_failed": "Montaggio dell'archivio del backup non riuscito", + "backup_archive_system_part_not_available": "La parte di sistema '{part:s}' non è disponibile in questo backup", + "backup_archive_writing_error": "Impossibile aggiungere i file al backup nell'archivio compresso" } From 0b9eab786197ba0fc3eaf46f0492c440686c3f14 Mon Sep 17 00:00:00 2001 From: Quenti Date: Sat, 10 Nov 2018 15:43:24 +0000 Subject: [PATCH 129/250] Translated using Weblate (Occitan) Currently translated at 100.0% (465 of 465 strings) --- locales/oc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/oc.json b/locales/oc.json index 7a4814389..91b455210 100644 --- a/locales/oc.json +++ b/locales/oc.json @@ -451,7 +451,7 @@ "migration_description_0004_php5_to_php7_pools": "Tornar configurar lo pools PHP per utilizar PHP 7 allòc del 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de postgresql 9.4 cap a 9.6", "migration_0005_postgresql_94_not_installed": "Postgresql es pas installat sul sistèma. Pas res de far !", - "migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada ! Quicòm d’estranh a degut arribar a vòstre sistèma :( ...", + "migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada !? Quicòm d’estranh a degut arribar a vòstre sistèma :( …", "migration_0005_not_enough_space": "I a pas pro d’espaci disponible sus {path} per lançar la migracion d’aquela passa :(.", "recommend_to_add_first_user": "La post installacion es acabada, mas YunoHost fa besonh d’almens un utilizaire per foncionar coma cal. Vos cal n’ajustar un en utilizant la comanda « yunohost user create » o ben l’interfàcia d’administracion.", "service_description_php7.0-fpm": "executa d’aplicacions escrichas en PHP amb nginx", From ae56546c94124a0a9ada9c6b5b740fa7c2257652 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sat, 17 Nov 2018 22:54:10 +0000 Subject: [PATCH 130/250] Translated using Weblate (French) Currently translated at 99.3% (462 of 465 strings) --- locales/fr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 814f3acd4..70ba9f47b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -469,5 +469,13 @@ "migration_0005_not_enough_space": "Il n’y a pas assez d’espace libre de disponible sur {path} pour lancer maintenant la migration :(.", "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d'au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant 'yunohost user create' ou l'interface d'administration.", "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx", - "users_available": "Liste des utilisateurs disponibles :" + "users_available": "Liste des utilisateurs disponibles :", + "good_practices_about_admin_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe d’administration. Le mot de passe doit comporter au moins 8 caractères – bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase de chiffrement) et/ou d’utiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", + "good_practices_about_user_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères – bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase de chiffrement) et/ou d’utiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", + "migration_description_0006_sync_admin_and_root_passwords": "Synchroniser les mots de passe admin et root", + "migration_0006_disclaimer": "Yunohost s'attend maintenant à ce que les mots de passe admin et root soient synchronisés. En exécutant cette migration, votre mot de passe root sera remplacé par le mot de passe administrateur.", + "migration_0006_done": "Votre mot de passe root a été remplacé par celui de votre adminitrateur.", + "password_listed": "Ce mot de passe est l’un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d’un peu plus unique.", + "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", + "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules et minuscules." } From 06e7485958e758388bcfaaf884b4c16cb53bf594 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sat, 17 Nov 2018 22:58:02 +0000 Subject: [PATCH 131/250] Translated using Weblate (French) Currently translated at 99.5% (463 of 465 strings) --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 70ba9f47b..b2ad1b04b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -477,5 +477,6 @@ "migration_0006_done": "Votre mot de passe root a été remplacé par celui de votre adminitrateur.", "password_listed": "Ce mot de passe est l’un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d’un peu plus unique.", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", - "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules et minuscules." + "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules et minuscules", + "password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux." } From 3d2053c9675561a0ae0b06c8a616a2370bd83edc Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sat, 17 Nov 2018 22:58:26 +0000 Subject: [PATCH 132/250] Translated using Weblate (French) Currently translated at 100.0% (465 of 465 strings) --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index b2ad1b04b..ae3ac4988 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -478,5 +478,7 @@ "password_listed": "Ce mot de passe est l’un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d’un peu plus unique.", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", "password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules et minuscules", - "password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux." + "password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux", + "password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux", + "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost n’a pas pu le propager sur le mot de passe root !" } From 52679e1ee32255891210258d009f272d88a71d5f Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Holcroft Date: Sat, 17 Nov 2018 23:07:18 +0000 Subject: [PATCH 133/250] Translated using Weblate (French) Currently translated at 100.0% (465 of 465 strings) --- locales/fr.json | 240 ++++++++++++++++++++++++------------------------ 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index ae3ac4988..7119039db 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,25 +1,25 @@ { "action_invalid": "Action « {action:s} » incorrecte", - "admin_password": "Mot de passe d'administration", + "admin_password": "Mot de passe d’administration", "admin_password_change_failed": "Impossible de changer le mot de passe", - "admin_password_changed": "Le mot de passe d'administration a été modifié", + "admin_password_changed": "Le mot de passe d’administration a été modifié", "app_already_installed": "{app:s} est déjà installé", - "app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}", + "app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l’un de {choices:s}", "app_argument_invalid": "Valeur invalide pour le paramètre « {name:s} » : {error:s}", "app_argument_missing": "Paramètre manquant « {:s} »", "app_argument_required": "Le paramètre « {name:s} » est requis", - "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", - "app_id_invalid": "Id d'application incorrect", - "app_incompatible": "L'application {app} est incompatible avec votre version de YunoHost", - "app_install_files_invalid": "Fichiers d'installation incorrects", - "app_location_already_used": "L'application '{app}' est déjà installée à cet emplacement ({path})", - "app_location_install_failed": "Impossible d'installer l'application à cet emplacement pour cause de conflit avec l'app '{other_app}' déjà installée sur '{other_path}'", - "app_manifest_invalid": "Manifeste d'application incorrect : {error}", + "app_extraction_failed": "Impossible d’extraire les fichiers d’installation", + "app_id_invalid": "Id d’application incorrect", + "app_incompatible": "L’application {app} est incompatible avec votre version de YunoHost", + "app_install_files_invalid": "Fichiers d’installation incorrects", + "app_location_already_used": "L’application « {app} » est déjà installée à cet emplacement ({path})", + "app_location_install_failed": "Impossible d’installer l’application à cet emplacement pour cause de conflit avec l’app « {other_app} » déjà installée sur « {other_path} »", + "app_manifest_invalid": "Manifeste d’application incorrect : {error}", "app_no_upgrade": "Aucune application à mettre à jour", "app_not_correctly_installed": "{app:s} semble être mal installé", - "app_not_installed": "{app:s} n'est pas installé", - "app_not_properly_removed": "{app:s} n'a pas été supprimé correctement", - "app_package_need_update": "Le paquet de l'application {app} doit être mis à jour pour suivre les changements de YunoHost", + "app_not_installed": "{app:s} n’est pas installé", + "app_not_properly_removed": "{app:s} n’a pas été supprimé correctement", + "app_package_need_update": "Le paquet de l’application {app} doit être mis à jour pour suivre les changements de YunoHost", "app_recent_version_required": "{app:s} nécessite une version plus récente de YunoHost", "app_removed": "{app:s} a été supprimé", "app_requirements_checking": "Vérification des paquets requis pour {app}...", @@ -27,45 +27,45 @@ "app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources", "app_unknown": "Application inconnue", - "app_unsupported_remote_type": "Le type distant utilisé par l'application n'est pas supporté", + "app_unsupported_remote_type": "Le type distant utilisé par l’application n’est pas pris en charge", "app_upgrade_failed": "Impossible de mettre à jour {app:s}", "app_upgraded": "{app:s} a été mis à jour", "appslist_fetched": "La liste d’applications {appslist:s} a été récupérée", "appslist_removed": "La liste d’applications {appslist:s} a été supprimée", "appslist_retrieve_error": "Impossible de récupérer la liste d’applications distante {appslist:s} : {error:s}", "appslist_unknown": "La liste d’applications {appslist:s} est inconnue.", - "ask_current_admin_password": "Mot de passe d'administration actuel", + "ask_current_admin_password": "Mot de passe d’administration actuel", "ask_email": "Adresse courriel", "ask_firstname": "Prénom", "ask_lastname": "Nom", "ask_list_to_remove": "Liste à supprimer", "ask_main_domain": "Domaine principal", - "ask_new_admin_password": "Nouveau mot de passe d'administration", + "ask_new_admin_password": "Nouveau mot de passe d’administration", "ask_password": "Mot de passe", "backup_action_required": "Vous devez préciser ce qui est à sauvegarder", - "backup_app_failed": "Impossible de sauvegarder l'application « {app:s} »", - "backup_archive_app_not_found": "L'application « {app:s} » n'a pas été trouvée dans l'archive de la sauvegarde", + "backup_app_failed": "Impossible de sauvegarder l’application « {app:s} »", + "backup_archive_app_not_found": "L’application « {app:s} » n’a pas été trouvée dans l’archive de la sauvegarde", "backup_archive_hook_not_exec": "Le script « {hook:s} » n'a pas été exécuté dans cette sauvegarde", "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà", - "backup_archive_name_unknown": "L'archive locale de sauvegarde nommée « {name:s} » est inconnue", - "backup_archive_open_failed": "Impossible d'ouvrir l'archive de sauvegarde", + "backup_archive_name_unknown": "L’archive locale de sauvegarde nommée « {name:s} » est inconnue", + "backup_archive_open_failed": "Impossible d’ouvrir l’archive de sauvegarde", "backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde", "backup_created": "Sauvegarde terminée", - "backup_creating_archive": "Création de l'archive de sauvegarde...", + "backup_creating_archive": "Création de l’archive de sauvegarde...", "backup_creation_failed": "Impossible de créer la sauvegarde", "backup_delete_error": "Impossible de supprimer « {path:s} »", "backup_deleted": "La sauvegarde a été supprimée", - "backup_extracting_archive": "Extraction de l'archive de sauvegarde...", + "backup_extracting_archive": "Extraction de l’archive de sauvegarde...", "backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu", "backup_invalid_archive": "Archive de sauvegarde incorrecte", - "backup_nothings_done": "Il n'y a rien à sauvegarder", + "backup_nothings_done": "Il n’y a rien à sauvegarder", "backup_output_directory_forbidden": "Dossier de destination interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", - "backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide", + "backup_output_directory_not_empty": "Le dossier de sortie n’est pas vide", "backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde", - "backup_running_app_script": "Lancement du script de sauvegarde de l'application « {app:s} »...", + "backup_running_app_script": "Lancement du script de sauvegarde de l’application « {app:s} »...", "backup_running_hooks": "Exécution des scripts de sauvegarde...", "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {app:s}", - "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée", + "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d’applications personnalisée", "diagnosis_debian_version_error": "Impossible de déterminer la version de Debian : {error}", "diagnosis_kernel_version_error": "Impossible de récupérer la version du noyau : {error}", "diagnosis_monitor_disk_error": "Impossible de superviser les disques : {error}", @@ -82,22 +82,22 @@ "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", "domain_exists": "Le domaine existe déjà", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d’abord les désinstaller avant de supprimer ce domaine", "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", "done": "Terminé", "downloading": "Téléchargement...", "dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée", - "dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour le domaine DynDNS", + "dyndns_cron_remove_failed": "Impossible d’enlever la tâche cron pour le domaine DynDNS", "dyndns_cron_removed": "La tâche cron pour le domaine DynDNS a été enlevée", - "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS", + "dyndns_ip_update_failed": "Impossible de mettre à jour l’adresse IP sur le domaine DynDNS", "dyndns_ip_updated": "Votre adresse IP a été mise à jour pour le domaine DynDNS", "dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre du temps...", "dyndns_key_not_found": "Clé DNS introuvable pour le domaine", - "dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS", + "dyndns_no_domain_registered": "Aucun domaine n’a été enregistré avec DynDNS", "dyndns_registered": "Le domaine DynDNS a été enregistré", - "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}", + "dyndns_registration_failed": "Impossible d’enregistrer le domaine DynDNS : {error:s}", "dyndns_unavailable": "Le domaine {domain:s} est indisponible.", "executing_command": "Exécution de la commande « {command:s} »...", "executing_script": "Exécution du script « {script:s} »...", @@ -105,7 +105,7 @@ "field_invalid": "Champ incorrect : « {:s} »", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Le pare-feu a été rechargé", - "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", + "firewall_rules_cmd_failed": "Certaines règles du pare-feu n’ont pas pu être appliquées. Pour plus d’informations, consultez le journal.", "format_datetime_short": "%d/%m/%Y %H:%M", "hook_argument_missing": "Argument manquant : '{:s}'", "hook_choice_invalid": "Choix incorrect : '{:s}'", @@ -114,12 +114,12 @@ "hook_list_by_invalid": "La propriété de tri des actions est invalide", "hook_name_unknown": "Nom de script « {name:s} » inconnu", "installation_complete": "Installation terminée", - "installation_failed": "Échec de l'installation", - "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", - "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas", - "ldap_initialized": "L'annuaire LDAP a été initialisé", + "installation_failed": "Échec de l’installation", + "ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge", + "iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge", + "ldap_initialized": "L’annuaire LDAP a été initialisé", "license_undefined": "indéfinie", - "mail_alias_remove_failed": "Impossible de supprimer l'alias courriel « {mail:s} »", + "mail_alias_remove_failed": "Impossible de supprimer l’alias courriel « {mail:s} »", "mail_domain_unknown": "Le domaine « {domain:s} » du courriel est inconnu", "mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert « {mail:s} »", "maindomain_change_failed": "Impossible de modifier le domaine principal", @@ -127,29 +127,29 @@ "monitor_disabled": "La supervision du serveur a été désactivé", "monitor_enabled": "La supervision du serveur a été activé", "monitor_glances_con_failed": "Impossible de se connecter au serveur Glances", - "monitor_not_enabled": "Le suivi de l'état du serveur n'est pas activé", + "monitor_not_enabled": "Le suivi de l’état du serveur n’est pas activé", "monitor_period_invalid": "Période de temps incorrecte", "monitor_stats_file_not_found": "Le fichier de statistiques est introuvable", - "monitor_stats_no_update": "Aucune donnée de l'état du serveur à mettre à jour", - "monitor_stats_period_unavailable": "Aucune statistique n'est disponible pour la période", + "monitor_stats_no_update": "Aucune donnée de l’état du serveur à mettre à jour", + "monitor_stats_period_unavailable": "Aucune statistique n’est disponible pour la période", "mountpoint_unknown": "Point de montage inconnu", "mysql_db_creation_failed": "Impossible de créer la base de données MySQL", - "mysql_db_init_failed": "Impossible d'initialiser la base de données MySQL", + "mysql_db_init_failed": "Impossible d’initialiser la base de données MySQL", "mysql_db_initialized": "La base de données MySQL a été initialisée", - "network_check_mx_ko": "L'enregistrement DNS MX n'est pas précisé", + "network_check_mx_ko": "L’enregistrement DNS MX n’est pas précisé", "network_check_smtp_ko": "Le trafic courriel sortant (port 25 SMTP) semble bloqué par votre réseau", - "network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n'est pas bloqué", + "network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n’est pas bloqué", "new_domain_required": "Vous devez spécifier le nouveau domaine principal", "no_appslist_found": "Aucune liste d’applications n’a été trouvée", - "no_internet_connection": "Le serveur n'est pas connecté à Internet", - "no_ipv6_connectivity": "La connectivité IPv6 n'est pas disponible", - "no_restore_script": "Le script de sauvegarde n'a pas été trouvé pour l'application « {app:s} »", + "no_internet_connection": "Le serveur n’est pas connecté à Internet", + "no_ipv6_connectivity": "La connectivité IPv6 n’est pas disponible", + "no_restore_script": "Le script de sauvegarde n’a pas été trouvé pour l’application « {app:s} »", "no_such_conf_file": "Le fichier {file:s} n’existe pas, il ne peut pas être copié", - "not_enough_disk_space": "L'espace disque est insuffisant sur « {path:s} »", - "package_not_installed": "Le paquet « {pkgname} » n'est pas installé", + "not_enough_disk_space": "L’espace disque est insuffisant sur « {path:s} »", + "package_not_installed": "Le paquet « {pkgname} » n’est pas installé", "package_unexpected_error": "Une erreur inattendue est survenue avec le paquet « {pkgname} »", "package_unknown": "Paquet « {pkgname} » inconnu", - "packages_no_upgrade": "Il n'y a aucun paquet à mettre à jour", + "packages_no_upgrade": "Il n’y a aucun paquet à mettre à jour", "packages_upgrade_critical_later": "Les paquets critiques ({packages:s}) seront mis à jour ultérieurement", "packages_upgrade_failed": "Impossible de mettre à jour tous les paquets", "path_removal_failed": "Impossible de supprimer le chemin {:s}", @@ -160,7 +160,7 @@ "pattern_lastname": "Doit être un nom valide", "pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas", "pattern_mailbox_quota": "Doit être une taille avec le suffixe b/k/M/G/T ou 0 pour désactiver le quota", - "pattern_password": "Doit être composé d'au moins 3 caractères", + "pattern_password": "Doit être composé d’au moins 3 caractères", "pattern_port": "Doit être un numéro de port valide (ex. : 0-65535)", "pattern_port_or_range": "Doit être un numéro de port valide (ex. : 0-65535) ou une gamme de ports (ex. : 100:200)", "pattern_positive_number": "Doit être un nombre positif", @@ -168,24 +168,24 @@ "port_already_closed": "Le port {port:d} est déjà fermé pour les connexions {ip_version:s}", "port_already_opened": "Le port {port:d} est déjà ouvert pour les connexions {ip_version:s}", "port_available": "Le port {port:d} est disponible", - "port_unavailable": "Le port {port:d} n'est pas disponible", + "port_unavailable": "Le port {port:d} n’est pas disponible", "restore_action_required": "Vous devez préciser ce qui est à restaurer", - "restore_already_installed_app": "Une application est déjà installée avec l'id « {app:s} »", - "restore_app_failed": "Impossible de restaurer l'application « {app:s} »", + "restore_already_installed_app": "Une application est déjà installée avec l’id « {app:s} »", + "restore_app_failed": "Impossible de restaurer l’application « {app:s} »", "restore_cleaning_failed": "Impossible de nettoyer le dossier temporaire de restauration", "restore_complete": "Restauration terminée", "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]", "restore_failed": "Impossible de restaurer le système", - "restore_hook_unavailable": "Le script de restauration « {part:s} » n'est pas disponible sur votre système, et n’est pas non plus dans l’archive", - "restore_nothings_done": "Rien n'a été restauré", - "restore_running_app_script": "Lancement du script de restauration pour l'application « {app:s} »...", + "restore_hook_unavailable": "Le script de restauration « {part:s} » n’est pas disponible sur votre système, et n’est pas non plus dans l’archive", + "restore_nothings_done": "Rien n’a été restauré", + "restore_running_app_script": "Lancement du script de restauration pour l’application « {app:s} »...", "restore_running_hooks": "Exécution des scripts de restauration...", "service_add_configuration": "Ajout du fichier de configuration {file:s}", - "service_add_failed": "Impossible d'ajouter le service « {service:s} »", + "service_add_failed": "Impossible d’ajouter le service « {service:s} »", "service_added": "Le service « {service:s} » a été ajouté", "service_already_started": "Le service « {service:s} » est déjà démarré", "service_already_stopped": "Le service « {service:s} » est déjà arrêté", - "service_cmd_exec_failed": "Impossible d'exécuter la commande « {command:s} »", + "service_cmd_exec_failed": "Impossible d’exécuter la commande « {command:s} »", "service_conf_file_backed_up": "Le fichier de configuration « {conf} » a été sauvegardé dans « {backup} »", "service_conf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration « {new} » vers « {conf} »", "service_conf_file_manually_modified": "Le fichier de configuration « {conf} » a été modifié manuellement et ne sera pas mis à jour", @@ -208,7 +208,7 @@ "service_regenconf_dry_pending_applying": "Vérification des configurations en attentes qui pourraient être appliquées pour le service « {service} »…", "service_regenconf_failed": "Impossible de régénérer la configuration pour les services : {services}", "service_regenconf_pending_applying": "Application des configurations en attentes pour le service « {service} »…", - "service_remove_failed": "Impossible d'enlever le service « {service:s} »", + "service_remove_failed": "Impossible d’enlever le service « {service:s} »", "service_removed": "Le service « {service:s} » a été enlevé", "service_start_failed": "Impossible de démarrer le service « {service:s} »\n\nJournaux récents : {logs:s}", "service_started": "Le service « {service:s} » a été démarré", @@ -221,38 +221,38 @@ "ssowat_conf_generated": "La configuration de SSOwat a été générée", "ssowat_conf_updated": "La configuration de SSOwat a été mise à jour", "system_upgraded": "Le système a été mis à jour", - "system_username_exists": "Le nom d'utilisateur existe déjà dans les utilisateurs système", - "unbackup_app": "L'application « {app:s} » ne sera pas sauvegardée", + "system_username_exists": "Le nom d’utilisateur existe déjà dans les utilisateurs système", + "unbackup_app": "L’application « {app:s} » ne sera pas sauvegardée", "unexpected_error": "Une erreur inattendue est survenue", "unit_unknown": "Unité « {unit:s} » inconnue", "unlimit": "Pas de quota", - "unrestore_app": "L'application « {app:s} » ne sera pas restaurée", - "update_cache_failed": "Impossible de mettre à jour le cache de l'APT", + "unrestore_app": "L’application « {app:s} » ne sera pas restaurée", + "update_cache_failed": "Impossible de mettre à jour le cache de l’APT", "updating_apt_cache": "Mise à jour de la liste des paquets disponibles...", "upgrade_complete": "Mise à jour terminée", "upgrading_packages": "Mise à jour des paquets...", - "upnp_dev_not_found": "Aucun périphérique compatible UPnP n'a été trouvé", + "upnp_dev_not_found": "Aucun périphérique compatible UPnP n’a été trouvé", "upnp_disabled": "UPnP a été désactivé", "upnp_enabled": "UPnP a été activé", - "upnp_port_open_failed": "Impossible d'ouvrir les ports avec UPnP", - "user_created": "L'utilisateur a été créé", - "user_creation_failed": "Impossible de créer l'utilisateur", - "user_deleted": "L'utilisateur a été supprimé", - "user_deletion_failed": "Impossible de supprimer l'utilisateur", - "user_home_creation_failed": "Impossible de créer le dossier personnel de l'utilisateur", - "user_info_failed": "Impossible de récupérer les informations de l'utilisateur", + "upnp_port_open_failed": "Impossible d’ouvrir les ports avec UPnP", + "user_created": "L’utilisateur a été créé", + "user_creation_failed": "Impossible de créer l’utilisateur", + "user_deleted": "L’utilisateur a été supprimé", + "user_deletion_failed": "Impossible de supprimer l’utilisateur", + "user_home_creation_failed": "Impossible de créer le dossier personnel de l’utilisateur", + "user_info_failed": "Impossible de récupérer les informations de l’utilisateur", "user_unknown": "Utilisateur « {user:s} » inconnu", - "user_update_failed": "Impossible de modifier l'utilisateur", - "user_updated": "L'utilisateur a été modifié", + "user_update_failed": "Impossible de modifier l’utilisateur", + "user_updated": "L’utilisateur a été modifié", "yunohost_already_installed": "YunoHost est déjà installé", - "yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification", + "yunohost_ca_creation_failed": "Impossible de créer l’autorité de certification", "yunohost_configured": "YunoHost a été configuré", "yunohost_installing": "Installation de YunoHost...", - "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »", + "yunohost_not_installed": "YunoHost n’est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »", "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner)", "certmanager_domain_unknown": "Domaine inconnu {domain:s}", "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} n’est pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force)", - "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain:s} a échoué…", + "certmanager_certificate_fetching_or_enabling_failed": "Il semble que l’activation du nouveau certificat pour {domain:s} a échoué…", "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} n’est pas fourni par Let’s Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point d’expirer ! Utilisez --force pour contourner", "certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} n’est pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes", @@ -273,15 +273,15 @@ "domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.", "certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour l’autorité du certificat auto-signé est introuvable (fichier : {file:s})", "certmanager_unable_to_parse_self_CA_name": "Impossible d’analyser le nom de l’autorité du certificat auto-signé (fichier : {file:s})", - "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie", + "mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l’espace disque occupé par la messagerie", "domains_available": "Domaines disponibles :", - "backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path:s})", + "backup_archive_broken_link": "Impossible d’accéder à l’archive de sauvegarde (lien invalide vers {path:s})", "certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.", "certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci d’attendre quelques heures qu’il se propage. Si le problème persiste, envisager d’ajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)", "certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec l’IP {ip:s}). Vous rencontrez peut-être un problème d’hairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.", "certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Let’s Encrypt. L’installation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.", "appslist_retrieve_bad_format": "Le fichier récupéré pour la liste d’applications {appslist:s} n’est pas valide", - "domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte", + "domain_hostname_failed": "Échec de la création d’un nouveau nom d’hôte", "yunohost_ca_creation_success": "L’autorité de certification locale a été créée.", "appslist_name_already_tracked": "Il y a déjà une liste d’applications enregistrée avec le nom {name:s}.", "appslist_url_already_tracked": "Il y a déjà une liste d’applications enregistrée avec l’URL {url:s}.", @@ -368,8 +368,8 @@ "ask_path": "Chemin", "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.", "dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}.", - "app_make_default_location_already_used": "Impossible de configurer l'app '{app}' par défaut pour le domaine {domain}, déjà utilisé par l'autre app '{other_app}'", - "app_upgrade_app_name": "Mise à jour de l'application {app}...", + "app_make_default_location_already_used": "Impossible de configurer l’app « {app} » par défaut pour le domaine {domain}, déjà utilisé par l’autre app « {other_app} »", + "app_upgrade_app_name": "Mise à jour de l’application {app}...", "backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier d’archives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas, vous avez probablement oublié de monter ou de connecter votre disque / clef USB.", "migrate_tsig_end": "La migration à hmac-sha512 est terminée", "migrate_tsig_failed": "La migration du domaine dyndns {domain} à hmac-sha512 a échoué, annulation des modifications. Erreur : {error_code} - {error}", @@ -393,7 +393,7 @@ "migration_0003_not_jessie": "La distribution Debian actuelle n’est pas Jessie !", "migration_0003_system_not_fully_up_to_date": "Votre système n’est pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer à migration à Stretch.", "migration_0003_still_on_jessie_after_main_upgrade": "Quelque chose s’est ma passé pendant la mise à niveau principale : le système est toujours sur Jessie ?!? Pour investiguer le problème, veuillez regarder {log} 🙁…", - "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus d’informations sur https://yunohost.org/backup ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu'à quelques heures pour que tout soit à niveau.\n\nDe plus, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). L’ancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !", + "migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si l’équipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus d’informations sur https://yunohost.org/backup ;\n - d’être patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu’à quelques heures pour que tout soit à niveau.\n\nDe plus, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). L’ancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !", "migration_0003_problematic_apps_warning": "Veuillez noter que les applications suivantes, éventuellement problématiques, ont été détectées. Il semble qu’elles n’aient pas été installées depuis une liste d’application ou qu’elles ne soit pas marquées «working ». En conséquence, nous ne pouvons pas garantir qu’elles fonctionneront après la mise à niveau : {problematic_apps}", "migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}", "migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.", @@ -417,44 +417,44 @@ "service_description_ssh": "vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)", "service_description_yunohost-api": "permet les interactions entre l’interface web de YunoHost et le système", "service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services", - "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l'utiliser à moins que vous ne sachiez ce que vous faîtes.", - "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : '{md_file}'", - "log_category_404": "La catégorie de log '{category}' n'existe pas", - "log_link_to_log": "Log complet de cette opération : ' {desc} '", - "log_help_to_get_log": "Pour voir le log de cette opération '{desc}', utiliser la commande 'yunohost log display {name}'", - "log_link_to_failed_log": "L'opération '{desc}' a échouée ! Pour avoir de l'aide, merci de fournir le log complet de l'opération", + "experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas l’utiliser à moins que vous ne sachiez ce que vous faîtes.", + "log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : {md_file}", + "log_category_404": "La catégorie de log « {category} » n’existe pas", + "log_link_to_log": "Log complet de cette opération : « {desc} »", + "log_help_to_get_log": "Pour voir le log de cette opération « {desc} », utiliser la commande « yunohost log display {name} »", + "log_link_to_failed_log": "L’opération « {desc} » a échouée ! Pour avoir de l’aide, merci de fournir le log complet de l’opération", "backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, la restauration de vos applications php peut ne pas aboutir (reason: {error:s})", - "log_help_to_get_failed_log": "L'opération '{desc}' a échouée ! Pour avoir de l'aide, merci de partager le log de cette opération en utilisant la commande 'yunohost log display {name} --share'", - "log_does_exists": "Il n'existe pas de log de l'opération ayant pour nom '{log}', utiliser 'yunohost log list pour voir tous les fichiers de logs disponibles'", - "log_operation_unit_unclosed_properly": "L'opération ne s'est pas terminée correctement", - "log_app_addaccess": "Ajouter l'accès à '{}'", - "log_app_removeaccess": "Enlever l'accès à '{}'", - "log_app_clearaccess": "Retirer tous les accès à '{}'", - "log_app_fetchlist": "Ajouter une liste d'application", - "log_app_removelist": "Enlever une liste d'application", - "log_app_change_url": "Changer l'url de l'application '{}'", - "log_app_install": "Installer l'application '{}'", - "log_app_remove": "Enlever l'application '{}'", - "log_app_upgrade": "Mettre à jour l'application '{}'", - "log_app_makedefault": "Faire de '{}' l'application par défaut", + "log_help_to_get_failed_log": "L’opération « {desc} » a échouée ! Pour avoir de l’aide, merci de partager le log de cette opération en utilisant la commande « yunohost log display {name} --share »", + "log_does_exists": "Il n’existe pas de log de l’opération ayant pour nom « {log} », utiliser « yunohost log list pour voir tous les fichiers de logs disponibles »", + "log_operation_unit_unclosed_properly": "L’opération ne s’est pas terminée correctement", + "log_app_addaccess": "Ajouter l’accès à « {} »", + "log_app_removeaccess": "Enlever l’accès à « {} »", + "log_app_clearaccess": "Retirer tous les accès à « {} »", + "log_app_fetchlist": "Ajouter une liste d’application", + "log_app_removelist": "Enlever une liste d’application", + "log_app_change_url": "Changer l’url de l’application « {} »", + "log_app_install": "Installer l’application « {} »", + "log_app_remove": "Enlever l’application « {} »", + "log_app_upgrade": "Mettre à jour l’application « {} »", + "log_app_makedefault": "Faire de « {} » l’application par défaut", "log_available_on_yunopaste": "Le log est désormais disponible via {url}", "log_backup_restore_system": "Restaurer le système depuis une sauvegarde", - "log_backup_restore_app": "Restaurer '{}' depuis une sauvegarde", - "log_remove_on_failed_restore": "Retirer '{}' après la restauration depuis une sauvegarde qui a échouée", - "log_remove_on_failed_install": "Enlever '{}' après une installation échouée", - "log_domain_add": "Ajouter le domaine '{}' dans la configuration du système", - "log_domain_remove": "Enlever le domaine '{}' de la configuration du système", - "log_dyndns_subscribe": "Souscrire au sous-domaine '{}' de Yunohost", - "log_dyndns_update": "Mettre à jour l'adresse ip associée à votre sous-domaine Yunohost '{}'", - "log_letsencrypt_cert_install": "Installer le certificat Let's encryt sur le domaine '{}'", - "log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine '{}'", - "log_letsencrypt_cert_renew": "Renouveler le certificat Let's encrypt de '{}'", - "log_service_enable": "Activer le service '{}'", - "log_service_regen_conf": "Régénérer la configuration système de '{}'", - "log_user_create": "Ajouter l'utilisateur '{}'", - "log_user_delete": "Enlever l'utilisateur '{}'", - "log_user_update": "Mettre à jour les informations de l'utilisateur '{}'", - "log_tools_maindomain": "Faire de '{} 1' le domaine principal", + "log_backup_restore_app": "Restaurer « {} » depuis une sauvegarde", + "log_remove_on_failed_restore": "Retirer « {} » après la restauration depuis une sauvegarde qui a échouée", + "log_remove_on_failed_install": "Enlever « {} » après une installation échouée", + "log_domain_add": "Ajouter le domaine « {} » dans la configuration du système", + "log_domain_remove": "Enlever le domaine « {} » de la configuration du système", + "log_dyndns_subscribe": "Souscrire au sous-domaine « {} » de Yunohost", + "log_dyndns_update": "Mettre à jour l’adresse ip associée à votre sous-domaine Yunohost « {} »", + "log_letsencrypt_cert_install": "Installer le certificat Let’s encryt sur le domaine « {} »", + "log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine « {} »", + "log_letsencrypt_cert_renew": "Renouveler le certificat Let’s encrypt de « {} »", + "log_service_enable": "Activer le service « {} »", + "log_service_regen_conf": "Régénérer la configuration système de « {} »", + "log_user_create": "Ajouter l’utilisateur « {} »", + "log_user_delete": "Enlever l’utilisateur « {} »", + "log_user_update": "Mettre à jour les informations de l’utilisateur « {} »", + "log_tools_maindomain": "Faire de « {} » le domaine principal", "log_tools_migrations_migrate_forward": "Migrer", "log_tools_migrations_migrate_backward": "Revenir en arrière", "log_tools_postinstall": "Faire la post-installation du serveur Yunohost", @@ -464,16 +464,16 @@ "mail_unavailable": "Cette adresse mail est réservée et doit être automatiquement attribuée au tout premier utilisateur", "migration_description_0004_php5_to_php7_pools": "Reconfigurez le pool PHP pour utiliser PHP 7 au lieu de 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de postgresql 9.4 vers 9.6", - "migration_0005_postgresql_94_not_installed": "Postgresql n'a pas été installé sur votre système. Rien à faire !", + "migration_0005_postgresql_94_not_installed": "Postgresql n’a pas été installé sur votre système. Rien à faire !", "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais pas Postgresql 9.6 !? Quelque chose d’étrange a dû arriver à votre système :( …", "migration_0005_not_enough_space": "Il n’y a pas assez d’espace libre de disponible sur {path} pour lancer maintenant la migration :(.", - "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d'au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant 'yunohost user create' ou l'interface d'administration.", + "recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin d’au moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant « yunohost user create » ou l’interface d’administration.", "service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx", "users_available": "Liste des utilisateurs disponibles :", "good_practices_about_admin_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe d’administration. Le mot de passe doit comporter au moins 8 caractères – bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase de chiffrement) et/ou d’utiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "good_practices_about_user_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères – bien qu’il soit recommandé d’utiliser un mot de passe plus long (c’est-à-dire une phrase de chiffrement) et/ou d’utiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).", "migration_description_0006_sync_admin_and_root_passwords": "Synchroniser les mots de passe admin et root", - "migration_0006_disclaimer": "Yunohost s'attend maintenant à ce que les mots de passe admin et root soient synchronisés. En exécutant cette migration, votre mot de passe root sera remplacé par le mot de passe administrateur.", + "migration_0006_disclaimer": "Yunohost s’attend maintenant à ce que les mots de passe admin et root soient synchronisés. En exécutant cette migration, votre mot de passe root sera remplacé par le mot de passe administrateur.", "migration_0006_done": "Votre mot de passe root a été remplacé par celui de votre adminitrateur.", "password_listed": "Ce mot de passe est l’un des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose d’un peu plus unique.", "password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères", From 5126b3e450b3e4eb5208653e8b6e15649b76aeb2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 23 Nov 2018 14:58:44 +0000 Subject: [PATCH 134/250] Update changelog for 3.3.1 --- debian/changelog | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/debian/changelog b/debian/changelog index 83e9e9269..ca4d8d928 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,17 @@ +yunohost (3.3.1) stable; urgency=low + + * [fix] Wait for dpkg lock to be free in apt helpers (#571) + * [fix] app_removeaccess call set.add (#573) + * [fix] Fix app_addaccess behaviour when 'allowed_users' is initially empty (#575) + * [fix] Typo in user_update when update password (#577) + * [fix] Do not fail on missing fail2ban config during the backup (#558) + * [fix] Generate a random serial for local certification auth (followup of #557) + * [i18n] Update Italian, Occitan, French translations + + Thanks to all contributors (Maniack, airwoodix, Aleks, ljf, silkevicious, Quent-in, Jibec) <3 ! + + -- Alexandre Aubin Fri, 23 Nov 2018 15:58:00 +0000 + yunohost (3.3.0) testing; urgency=low Highlights From a455cae3bf213067f875fa00f55a4244a48f92d2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 26 Nov 2018 14:56:47 +0000 Subject: [PATCH 135/250] Typo + fixes for helper doc generation --- data/helpers.d/print | 2 +- doc/generate_helper_doc.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/data/helpers.d/print b/data/helpers.d/print index 93d402e64..2f451bc24 100644 --- a/data/helpers.d/print +++ b/data/helpers.d/print @@ -7,7 +7,7 @@ ynh_die() { # Display a message in the 'INFO' logging category # -# usage: ynh_info "Some message" +# usage: ynh_print_info "Some message" ynh_print_info() { echo "$1" >> "$YNH_STDINFO" } diff --git a/doc/generate_helper_doc.py b/doc/generate_helper_doc.py index 20022e253..7d8c489b7 100644 --- a/doc/generate_helper_doc.py +++ b/doc/generate_helper_doc.py @@ -11,7 +11,7 @@ def render(data): from ansi2html.style import get_styles conv = Ansi2HTMLConverter() - shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme))) + shell_css = "\n".join(map(str, get_styles(conv.dark_bg))) def shell_to_html(shell): return conv.convert(shell, False) @@ -28,6 +28,7 @@ class Parser(): def __init__(self, filename): + self.filename = filename self.file = open(filename, "r").readlines() self.blocks = None @@ -42,6 +43,9 @@ class Parser(): "code": [] } for i, line in enumerate(self.file): + + if line.startswith("#!/bin/bash"): + continue line = line.rstrip().replace("\t", " ") @@ -64,7 +68,7 @@ class Parser(): else: # We're getting out of a comment bloc, we should find # the name of the function - assert len(line.split()) >= 1 + 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 @@ -143,11 +147,11 @@ class Parser(): b["usage"] = b["usage"].strip() + def is_global_comment(line): return line.startswith('#') def malformed_error(line_number): - import pdb; pdb.set_trace() return "Malformed file line {} ?".format(line_number) def main(): From 76121ea0847f2ce3be0db4605bbef67360d28862 Mon Sep 17 00:00:00 2001 From: liberodark Date: Tue, 27 Nov 2018 18:27:01 +0100 Subject: [PATCH 136/250] HTTP2 On (#580) * HTTP2 On for better security * Remove old lines --- data/templates/nginx/server.tpl.conf | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 9acc641ae..db42a8e65 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -20,11 +20,8 @@ server { } server { - # Disabling http2 for now as it's causing weird issues with curl - #listen 443 ssl http2; - #listen [::]:443 ssl http2; - listen 443 ssl; - listen [::]:443 ssl; + listen 443 ssl http2; + listen [::]:443 ssl http2; server_name {{ domain }}; ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem; From 190669228936aafa538bd80fe91229d595e30724 Mon Sep 17 00:00:00 2001 From: liberodark Date: Tue, 27 Nov 2018 18:30:39 +0100 Subject: [PATCH 137/250] Remove ECDH curve or change it ? (#579) Update ECDH curves recommended by Mozilla, now that we are on stretch --- data/templates/nginx/server.tpl.conf | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index db42a8e65..464639952 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -30,12 +30,7 @@ server { ssl_session_cache shared:SSL:50m; # As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519 - # (this doesn't work on jessie though ...?) - # ssl_ecdh_curve secp521r1:secp384r1:prime256v1; - - # As suggested by https://cipherli.st/ - ssl_ecdh_curve secp384r1; - + ssl_ecdh_curve secp521r1:secp384r1:prime256v1; ssl_prefer_server_ciphers on; # Ciphers with intermediate compatibility From 8598d81bb1a18589127b25ba7156986f719c14d1 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 14:15:55 +0200 Subject: [PATCH 138/250] [wip] Standardize ssh config --- data/hooks/conf_regen/03-ssh | 2 +- locales/en.json | 6 ++++++ src/yunohost/tools.py | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index a469b7a66..e3e03877e 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -7,7 +7,7 @@ do_pre_regen() { cd /usr/share/yunohost/templates/ssh - # only overwrite SSH configuration on an ISO installation + # Don't overwrite configuration if from_script if [[ ! -f /etc/yunohost/from_script ]]; then # do not listen to IPv6 if unavailable [[ -f /proc/net/if_inet6 ]] \ diff --git a/locales/en.json b/locales/en.json index 6ce22ca80..d63fc4a69 100644 --- a/locales/en.json +++ b/locales/en.json @@ -292,6 +292,12 @@ "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migration_0006_done": "Your root password have been replaced by your admin password.", + "migration_0007_general_warning": "To ensure a global security of your server, YunoHost recommends to let it manage the SSH configuration of your server. Your current SSH configuration differs from common default configuration. If you let YunoHost reconfigure it, the way to access with SSH to your server could change after this migration:", + "migration_0007_port": "- you will have to connect using port 22 instead of your custom SSH port. Feel free to reconfigure it", + "migration_0007_root": "- you will not be able to connect with root user, instead you will have to use admin user.", + "migration_0007_dsa": "- you might need to invalidate a warning and to recheck fingerprint of your server, because DSA key will be disabled.", + "migration_0007_risk": "If you agree to let YunoHost replace your configuration and change the way to access your server, make the migration else skip it.", + "migration_0007_no_risk": "No major change in the way has been found, but it's difficult to be sure. If you agree to let YunoHost replace your configuration and change the way to access your server, make the migration else skip it.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 271947b3d..a0549321a 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -440,6 +440,12 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, service_start("yunohost-firewall") service_regen_conf(force=True) + + # Restore specific ssh conf + bkp_sshd_conf = '/etc/ssh/sshd_config.to_restore' + if os.path.exists(bkp_sshd_conf): + os.rename(bkp_sshd_conf, '/etc/ssh/sshd_config') + logger.success(m18n.n('yunohost_configured')) logger.warning(m18n.n('recommend_to_add_first_user')) From 7f3a35dac0bd33a15247ca3df866551953d9a39c Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 14:38:46 +0200 Subject: [PATCH 139/250] [enh] Don't change ssh conf if not specified directly --- src/yunohost/service.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 5b7680a80..9ab301933 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -39,8 +39,8 @@ from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils import log, filesystem -from yunohost.hook import hook_callback 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') @@ -422,6 +422,12 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, # return the arguments to pass to the script return pre_args + [service_pending_path, ] + # Don't regen SSH if not specifically specified + if not names: + names = hook_list('conf_regen', list_by='name', + show_info=False)['hooks'] + names.remove('ssh') + pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call) # Update the services name From f0d0a715869af02b2d0594265d04c6c6efe37ba2 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 14:54:07 +0200 Subject: [PATCH 140/250] [wip] Ask user for keeping or not sshd config --- .../0006_manage_sshd_config.py | 56 ++++++++++++++ .../data_migrations/0007_reset_sshd_config.py | 77 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/yunohost/data_migrations/0006_manage_sshd_config.py create mode 100644 src/yunohost/data_migrations/0007_reset_sshd_config.py diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py new file mode 100644 index 000000000..413c3fefe --- /dev/null +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -0,0 +1,56 @@ +import subprocess +import os + +from shutil import copyfile + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration +from yunohost.service import service_regen_conf, _get_conf_hashes, + _calculate_hash + +logger = getActionLogger('yunohost.migration') + + +class MyMigration(Migration): + """ + Ensure SSH conf is managed by YunoHost, reapply initial change and setup an + extension dir + """ + + def migrate(self): + + # Create sshd_config.d dir + if not os.path.exists('/etc/ssh/sshd_config.d'): + mkdir('/etc/ssh/sshd_config.d', '0755', uid='root', gid='root') + + # Manage SSHd in all case + if os.path.exists('/etc/yunohost/from_script'): + rm('/etc/yunohost/from_script') + copyfile('/etc/ssh/sshd_config', '/etc/ssh/sshd_config.restore') + service_regen_conf(names=['ssh'], force=True) + os.rename('/etc/ssh/sshd_config.restore', '/etc/ssh/sshd_config') + + # If custom conf, add 'Include' instruction + ynh_hash = _get_conf_hashes('ssh')['/etc/ssh/sshd_config'] + current_hash = _calculate_hash('/etc/ssh/sshd_config') + if ynh_hash == current_hash: + return + + add_include = False + include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' + for line in open('/etc/ssh/sshd_config'): + if re.match(root_rgx, line) is not None: + add_include = True + break + + if add_include: + with open("/etc/ssh/sshd_config", "a") as conf: + conf.write('Include sshd_config.d/*') + + def backward(self): + + raise MoulinetteError(m18n.n("migration_0006_backward_impossible")) + diff --git a/src/yunohost/data_migrations/0007_reset_sshd_config.py b/src/yunohost/data_migrations/0007_reset_sshd_config.py new file mode 100644 index 000000000..701b60e3c --- /dev/null +++ b/src/yunohost/data_migrations/0007_reset_sshd_config.py @@ -0,0 +1,77 @@ +import subprocess +import os + +from shutil import copyfile + +from moulinette import m18n +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +from yunohost.tools import Migration +from yunohost.service import service_regen_conf + +logger = getActionLogger('yunohost.migration') + + +class MyMigration(Migration): + "Reset SSH conf to the YunoHost one" + + mode = "manual" + + def migrate(self): + service_regen_conf(names=['ssh'], force=True) + + def backward(self): + + raise MoulinetteError(m18n.n("migration_0007_backward_impossible")) + + @property + def disclaimer(self): + + # Avoid having a super long disclaimer + ynh_hash = _get_conf_hashes('ssh')['/etc/ssh/sshd_config'] + current_hash = _calculate_hash('/etc/ssh/sshd_config') + if ynh_hash == current_hash: + return None + + # Detect major risk to migrate to the new configuration + dsa = False + port_rgx = r'^[ \t]*Port[ \t]+(\d+)[ \t]*(?:#.*)?$' + root_rgx = r'^[ \t]*PermitRootLogin[ \t]([\w-]*)[ \t]*(?:#.*)?$' + dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$' + for line in open('/etc/ssh/sshd_config'): + + ports = re.findall(port_rgx, line) + + root_login = re.match(root_rgx, line) + if root_login is not None: + root_login = root_login.group(1) + + if not dsa and re.match(dsa_rgx, line): + dsa = True + + if len(ports) == 0: + ports = ['22'] + + port = ports != ['22'] + + root_user = root_login in ['yes'] + + # Build message + message = m18n.n("migration_0007_general_warning") + + if port: + message += "\n\n" + m18n.n("migration_0007_port") + + if root_user: + message += "\n\n" + m18n.n("migration_0007_root") + + if dsa: + message += "\n\n" + m18n.n("migration_0007_dsa") + + if port or root_user or dsa: + message += "\n\n" + m18n.n("migration_0007_risk") + else: + message += "\n\n" + m18n.n("migration_0007_no_risk") + + return message From c2b225d3765938a5d4536badbdb1700ee1064801 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 19:57:48 +0200 Subject: [PATCH 141/250] [fix] A lot of bug on the wip work on sshd migration --- data/templates/ssh/sshd_config | 2 ++ locales/en.json | 2 ++ .../0006_manage_sshd_config.py | 13 +++++--- .../data_migrations/0007_reset_sshd_config.py | 32 ++++++++++++------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 8c5a7fb95..b79ffd3bf 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -94,3 +94,5 @@ Match User sftpusers AllowTcpForwarding no GatewayPorts no X11Forwarding no + +Include sshd_config.d/* diff --git a/locales/en.json b/locales/en.json index d63fc4a69..94e8a6384 100644 --- a/locales/en.json +++ b/locales/en.json @@ -274,6 +274,8 @@ "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", "migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", + "migration_description_0006_manage_sshd_config": "Manage SSH conf in a better way", + "migration_description_0007_reset_sshd_config": "Reset SSH conf to the YunoHost default conf", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py index 413c3fefe..d4740192e 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -1,15 +1,17 @@ import subprocess import os +import re from shutil import copyfile from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from moulinette.utils.filesystem import mkdir, rm from yunohost.tools import Migration -from yunohost.service import service_regen_conf, _get_conf_hashes, - _calculate_hash +from yunohost.service import service_regen_conf, _get_conf_hashes, \ + _calculate_hash, _run_service_command logger = getActionLogger('yunohost.migration') @@ -24,7 +26,7 @@ class MyMigration(Migration): # Create sshd_config.d dir if not os.path.exists('/etc/ssh/sshd_config.d'): - mkdir('/etc/ssh/sshd_config.d', '0755', uid='root', gid='root') + mkdir('/etc/ssh/sshd_config.d', 0755, uid='root', gid='root') # Manage SSHd in all case if os.path.exists('/etc/yunohost/from_script'): @@ -42,7 +44,7 @@ class MyMigration(Migration): add_include = False include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' for line in open('/etc/ssh/sshd_config'): - if re.match(root_rgx, line) is not None: + if re.match(include_rgx, line) is not None: add_include = True break @@ -50,6 +52,9 @@ class MyMigration(Migration): with open("/etc/ssh/sshd_config", "a") as conf: conf.write('Include sshd_config.d/*') + if not _run_service_command('restart', 'ssh'): + self.backward() + def backward(self): raise MoulinetteError(m18n.n("migration_0006_backward_impossible")) diff --git a/src/yunohost/data_migrations/0007_reset_sshd_config.py b/src/yunohost/data_migrations/0007_reset_sshd_config.py index 701b60e3c..a9fb9fa14 100644 --- a/src/yunohost/data_migrations/0007_reset_sshd_config.py +++ b/src/yunohost/data_migrations/0007_reset_sshd_config.py @@ -1,5 +1,6 @@ import subprocess import os +import re from shutil import copyfile @@ -8,7 +9,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.service import service_regen_conf +from yunohost.service import service_regen_conf, _get_conf_hashes, _calculate_hash logger = getActionLogger('yunohost.migration') @@ -16,8 +17,6 @@ logger = getActionLogger('yunohost.migration') class MyMigration(Migration): "Reset SSH conf to the YunoHost one" - mode = "manual" - def migrate(self): service_regen_conf(names=['ssh'], force=True) @@ -26,26 +25,37 @@ class MyMigration(Migration): raise MoulinetteError(m18n.n("migration_0007_backward_impossible")) @property - def disclaimer(self): + def mode(self): # Avoid having a super long disclaimer - ynh_hash = _get_conf_hashes('ssh')['/etc/ssh/sshd_config'] + ynh_hash = _get_conf_hashes('ssh') + if '/etc/ssh/sshd_config' in ynh_hash: + ynh_hash = ynh_hash['/etc/ssh/sshd_config'] current_hash = _calculate_hash('/etc/ssh/sshd_config') if ynh_hash == current_hash: + return "auto" + + return "manual" + + + @property + def disclaimer(self): + + if self.mode == "auto": return None # Detect major risk to migrate to the new configuration dsa = False + ports = [] + root_login = [] port_rgx = r'^[ \t]*Port[ \t]+(\d+)[ \t]*(?:#.*)?$' - root_rgx = r'^[ \t]*PermitRootLogin[ \t]([\w-]*)[ \t]*(?:#.*)?$' + root_rgx = r'^[ \t]*PermitRootLogin[ \t]([^# \t]*)[ \t]*(?:#.*)?$' dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$' for line in open('/etc/ssh/sshd_config'): - ports = re.findall(port_rgx, line) + ports = ports + re.findall(port_rgx, line) - root_login = re.match(root_rgx, line) - if root_login is not None: - root_login = root_login.group(1) + root_login = root_login + re.findall(root_rgx, line) if not dsa and re.match(dsa_rgx, line): dsa = True @@ -55,7 +65,7 @@ class MyMigration(Migration): port = ports != ['22'] - root_user = root_login in ['yes'] + root_user = root_login and root_login[-1] != 'no' # Build message message = m18n.n("migration_0007_general_warning") From 4e92a36322dae60021738c0d5ef71e67247a45a8 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 20:20:35 +0200 Subject: [PATCH 142/250] [fix] Backward if can't restart --- locales/en.json | 2 ++ .../0006_manage_sshd_config.py | 33 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/locales/en.json b/locales/en.json index 94e8a6384..df69ca1a4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -294,6 +294,8 @@ "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migration_0006_done": "Your root password have been replaced by your admin password.", + "migration_0006_cancelled": "YunoHost has failed to improve the way your ssh conf is managed.", + "migration_0006_cannot_restart": "SSH can't be restarted after we tried to cancel the migration 6.", "migration_0007_general_warning": "To ensure a global security of your server, YunoHost recommends to let it manage the SSH configuration of your server. Your current SSH configuration differs from common default configuration. If you let YunoHost reconfigure it, the way to access with SSH to your server could change after this migration:", "migration_0007_port": "- you will have to connect using port 22 instead of your custom SSH port. Feel free to reconfigure it", "migration_0007_root": "- you will not be able to connect with root user, instead you will have to use admin user.", diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py index d4740192e..cd9204846 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -31,31 +31,36 @@ class MyMigration(Migration): # Manage SSHd in all case if os.path.exists('/etc/yunohost/from_script'): rm('/etc/yunohost/from_script') - copyfile('/etc/ssh/sshd_config', '/etc/ssh/sshd_config.restore') + copyfile('/etc/ssh/sshd_config', '/etc/ssh/sshd_config.bkp') service_regen_conf(names=['ssh'], force=True) - os.rename('/etc/ssh/sshd_config.restore', '/etc/ssh/sshd_config') + copyfile('/etc/ssh/sshd_config.bkp', '/etc/ssh/sshd_config') # If custom conf, add 'Include' instruction ynh_hash = _get_conf_hashes('ssh')['/etc/ssh/sshd_config'] current_hash = _calculate_hash('/etc/ssh/sshd_config') - if ynh_hash == current_hash: - return + if ynh_hash != current_hash: - add_include = False - include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' - for line in open('/etc/ssh/sshd_config'): - if re.match(include_rgx, line) is not None: - add_include = True - break + add_include = False + include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' + for line in open('/etc/ssh/sshd_config'): + if re.match(include_rgx, line) is not None: + add_include = True + break - if add_include: - with open("/etc/ssh/sshd_config", "a") as conf: - conf.write('Include sshd_config.d/*') + if add_include: + with open("/etc/ssh/sshd_config", "a") as conf: + conf.write('Include sshd_config.d/*') + # Restart ssh and backward if it fail if not _run_service_command('restart', 'ssh'): self.backward() + raise MoulinetteError(m18n.n("migration_0006_cancel")) + def backward(self): + # We don't backward completely but it should be enough - raise MoulinetteError(m18n.n("migration_0006_backward_impossible")) + copyfile('/etc/ssh/sshd_config.bkp', '/etc/ssh/sshd_config') + if not _run_service_command('restart', 'ssh'): + raise MoulinetteError(m18n.n("migration_0006_cannot_restart")) From 4602439c013d9db6f9a92f34899788fab0df9f56 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 20:29:11 +0200 Subject: [PATCH 143/250] [fix] Pep8 and define SSHD_CONF --- .../0006_manage_sshd_config.py | 25 +++++++++---------- .../data_migrations/0007_reset_sshd_config.py | 8 ++---- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py index cd9204846..4af486b6e 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -1,4 +1,3 @@ -import subprocess import os import re @@ -15,6 +14,8 @@ from yunohost.service import service_regen_conf, _get_conf_hashes, \ logger = getActionLogger('yunohost.migration') +SSHD_CONF = '/etc/ssh/sshd_config' + class MyMigration(Migration): """ @@ -25,30 +26,30 @@ class MyMigration(Migration): def migrate(self): # Create sshd_config.d dir - if not os.path.exists('/etc/ssh/sshd_config.d'): - mkdir('/etc/ssh/sshd_config.d', 0755, uid='root', gid='root') + if not os.path.exists(SSHD_CONF + '.d'): + mkdir(SSHD_CONF + '.d', 0755, uid='root', gid='root') # Manage SSHd in all case if os.path.exists('/etc/yunohost/from_script'): rm('/etc/yunohost/from_script') - copyfile('/etc/ssh/sshd_config', '/etc/ssh/sshd_config.bkp') + copyfile(SSHD_CONF, '/etc/ssh/sshd_config.bkp') service_regen_conf(names=['ssh'], force=True) - copyfile('/etc/ssh/sshd_config.bkp', '/etc/ssh/sshd_config') + copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) # If custom conf, add 'Include' instruction - ynh_hash = _get_conf_hashes('ssh')['/etc/ssh/sshd_config'] - current_hash = _calculate_hash('/etc/ssh/sshd_config') + ynh_hash = _get_conf_hashes('ssh')[SSHD_CONF] + current_hash = _calculate_hash(SSHD_CONF) + include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' if ynh_hash != current_hash: add_include = False - include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' - for line in open('/etc/ssh/sshd_config'): + for line in open(SSHD_CONF): if re.match(include_rgx, line) is not None: add_include = True break if add_include: - with open("/etc/ssh/sshd_config", "a") as conf: + with open(SSHD_CONF, "a") as conf: conf.write('Include sshd_config.d/*') # Restart ssh and backward if it fail @@ -56,11 +57,9 @@ class MyMigration(Migration): self.backward() raise MoulinetteError(m18n.n("migration_0006_cancel")) - def backward(self): # We don't backward completely but it should be enough - copyfile('/etc/ssh/sshd_config.bkp', '/etc/ssh/sshd_config') + copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) if not _run_service_command('restart', 'ssh'): raise MoulinetteError(m18n.n("migration_0006_cannot_restart")) - diff --git a/src/yunohost/data_migrations/0007_reset_sshd_config.py b/src/yunohost/data_migrations/0007_reset_sshd_config.py index a9fb9fa14..5a097968d 100644 --- a/src/yunohost/data_migrations/0007_reset_sshd_config.py +++ b/src/yunohost/data_migrations/0007_reset_sshd_config.py @@ -1,15 +1,12 @@ -import subprocess -import os import re -from shutil import copyfile - from moulinette import m18n from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.service import service_regen_conf, _get_conf_hashes, _calculate_hash +from yunohost.service import service_regen_conf, _get_conf_hashes, \ + _calculate_hash logger = getActionLogger('yunohost.migration') @@ -37,7 +34,6 @@ class MyMigration(Migration): return "manual" - @property def disclaimer(self): From 8e0086d49397e701efbbc2ec4f31ab044b8e5920 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 26 Aug 2018 23:40:26 +0200 Subject: [PATCH 144/250] [fix] Allow user to trigger the moment when they remove dsa --- data/hooks/conf_regen/03-ssh | 5 +++++ data/templates/ssh/sshd_config | 8 +++----- .../data_migrations/0006_manage_sshd_config.py | 11 +++++++++++ .../data_migrations/0007_reset_sshd_config.py | 7 +++++-- src/yunohost/settings.py | 1 + 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index e3e03877e..563394d40 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -13,6 +13,11 @@ do_pre_regen() { [[ -f /proc/net/if_inet6 ]] \ || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config + # Add DSA HostKey to let user remove it with migration 7 + if [[ "$(yunohost settings 'service.ssh._deprecated_dsa_hostkey')" == "True" ]]; then + sed -i '/HostKey \/etc\/ssh\/ssh_host_rsa_key/a HostKey /etc/ssh/ssh_host_dsa_key' sshd_config + fi + install -D -m 644 sshd_config "${pending_dir}/etc/ssh/sshd_config" fi } diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index b79ffd3bf..66aacc5f0 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -9,14 +9,12 @@ ListenAddress 0.0.0.0 Protocol 2 # HostKeys for protocol version 2 HostKey /etc/ssh/ssh_host_rsa_key -HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key + #Privilege Separation is turned on for security UsePrivilegeSeparation yes -# Lifetime and size of ephemeral version 1 server key -KeyRegenerationInterval 3600 -ServerKeyBits 768 - # Logging SyslogFacility AUTH LogLevel INFO diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py index 4af486b6e..13b0bbadf 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -11,6 +11,7 @@ from moulinette.utils.filesystem import mkdir, rm from yunohost.tools import Migration from yunohost.service import service_regen_conf, _get_conf_hashes, \ _calculate_hash, _run_service_command +from yunohost.settings import settings_set logger = getActionLogger('yunohost.migration') @@ -25,6 +26,16 @@ class MyMigration(Migration): def migrate(self): + # Check if deprecated DSA Host Key is in config + dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$' + dsa = False + for line in open(SSHD_CONF): + if re.match(dsa_rgx, line) is not None: + dsa = True + break + if dsa: + settings_set("service.ssh._deprecated_dsa_hostkey", True) + # Create sshd_config.d dir if not os.path.exists(SSHD_CONF + '.d'): mkdir(SSHD_CONF + '.d', 0755, uid='root', gid='root') diff --git a/src/yunohost/data_migrations/0007_reset_sshd_config.py b/src/yunohost/data_migrations/0007_reset_sshd_config.py index 5a097968d..af8f83ce7 100644 --- a/src/yunohost/data_migrations/0007_reset_sshd_config.py +++ b/src/yunohost/data_migrations/0007_reset_sshd_config.py @@ -7,6 +7,7 @@ from moulinette.utils.log import getActionLogger from yunohost.tools import Migration from yunohost.service import service_regen_conf, _get_conf_hashes, \ _calculate_hash +from yunohost.settings import settings_set, settings_get logger = getActionLogger('yunohost.migration') @@ -15,6 +16,7 @@ class MyMigration(Migration): "Reset SSH conf to the YunoHost one" def migrate(self): + settings_set("service.ssh._deprecated_dsa_hostkey", False) service_regen_conf(names=['ssh'], force=True) def backward(self): @@ -29,7 +31,8 @@ class MyMigration(Migration): if '/etc/ssh/sshd_config' in ynh_hash: ynh_hash = ynh_hash['/etc/ssh/sshd_config'] current_hash = _calculate_hash('/etc/ssh/sshd_config') - if ynh_hash == current_hash: + dsa = settings_get("service.ssh._deprecated_dsa_hostkey") + if ynh_hash == current_hash and not dsa: return "auto" return "manual" @@ -53,7 +56,7 @@ class MyMigration(Migration): root_login = root_login + re.findall(root_rgx, line) - if not dsa and re.match(dsa_rgx, line): + if not dsa and re.match(dsa_rgx, line) is not None: dsa = True if len(ports) == 0: diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index d2526316e..1539435c6 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -39,6 +39,7 @@ DEFAULTS = OrderedDict([ # -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._deprecated_dsa_hostkey", {"type": "bool", "default": False}), ]) From b5896e88c3db60569233da8628b38dc81e7d8cd4 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sat, 15 Sep 2018 15:14:51 +0200 Subject: [PATCH 145/250] [enh] Display only used fingerprint in bootprompt --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 2705dbcdc..41fb83899 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address) # Fetch SSH fingerprints i=0 -for key in /etc/ssh/ssh_host_*_key.pub ; do +for key in /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_key.pub ; do output=$(ssh-keygen -l -f $key) fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)" i=$(($i + 1)) From b4e72173cd4b076ceb36d1513e0278ffcce0af72 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sat, 15 Sep 2018 15:24:03 +0200 Subject: [PATCH 146/250] [fix] If some ssh keys are missing --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index 41fb83899..a86d29558 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address) # Fetch SSH fingerprints i=0 -for key in /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_key.pub ; do +for key in /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_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)) From 07e5ead0382bb8cb6066801a08451f6bde17f2fd Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 16 Sep 2018 18:37:43 +0200 Subject: [PATCH 147/250] [fix] Sometimes I need to sleep --- bin/yunoprompt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index a86d29558..bca5c2cb3 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address) # Fetch SSH fingerprints i=0 -for key in /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_key.pub 2> /dev/null ; do +for key in $(ls /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_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)) From 31c8b88f4470aca75afd5bb5f9f720da2cddbeae Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 21 Sep 2018 15:57:59 +0000 Subject: [PATCH 148/250] Wordin --- locales/en.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index df69ca1a4..a206b9fb8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -294,14 +294,14 @@ "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migration_0006_done": "Your root password have been replaced by your admin password.", - "migration_0006_cancelled": "YunoHost has failed to improve the way your ssh conf is managed.", - "migration_0006_cannot_restart": "SSH can't be restarted after we tried to cancel the migration 6.", - "migration_0007_general_warning": "To ensure a global security of your server, YunoHost recommends to let it manage the SSH configuration of your server. Your current SSH configuration differs from common default configuration. If you let YunoHost reconfigure it, the way to access with SSH to your server could change after this migration:", - "migration_0007_port": "- you will have to connect using port 22 instead of your custom SSH port. Feel free to reconfigure it", - "migration_0007_root": "- you will not be able to connect with root user, instead you will have to use admin user.", - "migration_0007_dsa": "- you might need to invalidate a warning and to recheck fingerprint of your server, because DSA key will be disabled.", - "migration_0007_risk": "If you agree to let YunoHost replace your configuration and change the way to access your server, make the migration else skip it.", - "migration_0007_no_risk": "No major change in the way has been found, but it's difficult to be sure. If you agree to let YunoHost replace your configuration and change the way to access your server, make the migration else skip it.", + "migration_0006_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.", + "migration_0006_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.", + "migration_0007_general_warning": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", + "migration_0007_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;", + "migration_0007_root": " - you will not be able to connect with root user, instead you will have to use the admin user ;", + "migration_0007_dsa": " - you might need to invalidate a warning and to recheck the fingerprint of your server, because DSA key will be disabled ;", + "migration_0007_risk": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration though it is not recommended.", + "migration_0007_no_risk": "No major risk has been indentified about overriding your SSH configuration - but it's difficult to be sure. If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration though it is not recommended.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", From 68906a1e982adb9785d521c4d3ff21a47dd99d6a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 22:16:36 +0200 Subject: [PATCH 149/250] Improve comments --- .../0006_manage_sshd_config.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_manage_sshd_config.py index 13b0bbadf..68ee020fd 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_manage_sshd_config.py @@ -20,8 +20,14 @@ SSHD_CONF = '/etc/ssh/sshd_config' class MyMigration(Migration): """ - Ensure SSH conf is managed by YunoHost, reapply initial change and setup an - extension dir + This is an automatic migration, that ensure SSH conf is managed by YunoHost + (even if the "from_script" flag is present) + + If the from_script flag exists, then we keep the current SSH conf such that it + will appear as "manually modified" to the regenconf. + + The admin can then choose in the next migration (manual, thi time) wether or + not to actually use the recommended configuration. """ def migrate(self): @@ -40,25 +46,34 @@ class MyMigration(Migration): if not os.path.exists(SSHD_CONF + '.d'): mkdir(SSHD_CONF + '.d', 0755, uid='root', gid='root') - # Manage SSHd in all case + # Here, we make it so that /etc/ssh/sshd_config is managed + # by the regen conf (in particular in the case where the + # from_script flag is present - in which case it was *not* + # managed by the regenconf) + # But because we can't be sure the user wants to use the + # recommended conf, we backup then restore the /etc/ssh/sshd_config + # right after the regenconf, such that it will appear as + # "manually modified". if os.path.exists('/etc/yunohost/from_script'): rm('/etc/yunohost/from_script') copyfile(SSHD_CONF, '/etc/ssh/sshd_config.bkp') service_regen_conf(names=['ssh'], force=True) copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) - # If custom conf, add 'Include' instruction + # If we detect the conf as manually modified ynh_hash = _get_conf_hashes('ssh')[SSHD_CONF] current_hash = _calculate_hash(SSHD_CONF) - include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' if ynh_hash != current_hash: + # And if there's not already an "Include ssh_config.d/*" directive + include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' add_include = False for line in open(SSHD_CONF): if re.match(include_rgx, line) is not None: add_include = True break + # We add an "Include sshd_config.d/*" directive if add_include: with open(SSHD_CONF, "a") as conf: conf.write('Include sshd_config.d/*') @@ -69,8 +84,8 @@ class MyMigration(Migration): raise MoulinetteError(m18n.n("migration_0006_cancel")) def backward(self): - # We don't backward completely but it should be enough + # We don't backward completely but it should be enough copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) if not _run_service_command('restart', 'ssh'): raise MoulinetteError(m18n.n("migration_0006_cannot_restart")) From 7b6bf6f4b890f8f1b1303f691258ea290f6bc278 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 20:27:24 +0000 Subject: [PATCH 150/250] Missing 'get' --- data/hooks/conf_regen/03-ssh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 563394d40..2c9261193 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -14,7 +14,7 @@ do_pre_regen() { || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config # Add DSA HostKey to let user remove it with migration 7 - if [[ "$(yunohost settings 'service.ssh._deprecated_dsa_hostkey')" == "True" ]]; then + if [[ "$(yunohost settings get 'service.ssh._deprecated_dsa_hostkey')" == "True" ]]; then sed -i '/HostKey \/etc\/ssh\/ssh_host_rsa_key/a HostKey /etc/ssh/ssh_host_dsa_key' sshd_config fi From e8393a3d26777bdce09ffb77efb06ba6e8fcb754 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 20:47:47 +0000 Subject: [PATCH 151/250] Improve comments, naming and descriptions --- locales/en.json | 4 ++-- ...006_ssh_conf_managed_by_yunohost_step1.py} | 12 +++++++---- ...007_ssh_conf_managed_by_yunohost_step2.py} | 21 ++++++++++++++++--- 3 files changed, 28 insertions(+), 9 deletions(-) rename src/yunohost/data_migrations/{0006_manage_sshd_config.py => 0006_ssh_conf_managed_by_yunohost_step1.py} (86%) rename src/yunohost/data_migrations/{0007_reset_sshd_config.py => 0007_ssh_conf_managed_by_yunohost_step2.py} (76%) diff --git a/locales/en.json b/locales/en.json index a206b9fb8..803d5c937 100644 --- a/locales/en.json +++ b/locales/en.json @@ -274,8 +274,8 @@ "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", "migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", - "migration_description_0006_manage_sshd_config": "Manage SSH conf in a better way", - "migration_description_0007_reset_sshd_config": "Reset SSH conf to the YunoHost default conf", + "migration_description_0006_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)", + "migration_description_0007_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", diff --git a/src/yunohost/data_migrations/0006_manage_sshd_config.py b/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py similarity index 86% rename from src/yunohost/data_migrations/0006_manage_sshd_config.py rename to src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py index 68ee020fd..c3a503492 100644 --- a/src/yunohost/data_migrations/0006_manage_sshd_config.py +++ b/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py @@ -20,14 +20,18 @@ SSHD_CONF = '/etc/ssh/sshd_config' class MyMigration(Migration): """ - This is an automatic migration, that ensure SSH conf is managed by YunoHost - (even if the "from_script" flag is present) + This is the first step of a couple of migrations that ensure SSH conf is + managed by YunoHost (even if the "from_script" flag is present, which was + previously preventing it from being managed by YunoHost) + The goal of this first (automatic) migration is to make sure that the + sshd_config is managed by the regen-conf mechanism. + If the from_script flag exists, then we keep the current SSH conf such that it will appear as "manually modified" to the regenconf. - The admin can then choose in the next migration (manual, thi time) wether or - not to actually use the recommended configuration. + In step 2 (manual), the admin will be able to choose wether or not to actually + use the recommended configuration, with an appropriate disclaimer. """ def migrate(self): diff --git a/src/yunohost/data_migrations/0007_reset_sshd_config.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py similarity index 76% rename from src/yunohost/data_migrations/0007_reset_sshd_config.py rename to src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py index af8f83ce7..10e319b2d 100644 --- a/src/yunohost/data_migrations/0007_reset_sshd_config.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py @@ -13,7 +13,18 @@ logger = getActionLogger('yunohost.migration') class MyMigration(Migration): - "Reset SSH conf to the YunoHost one" + """ + In this second step, the admin is asked if it's okay to use + the recommended SSH configuration - which also implies + disabling deprecated DSA key. + + This has important implications in the way the user may connect + to its server (key change, and a spooky warning might be given + by SSH later) + + A disclaimer explaining the various things to be aware of is + shown - and the user may also choose to skip this migration. + """ def migrate(self): settings_set("service.ssh._deprecated_dsa_hostkey", False) @@ -26,7 +37,10 @@ class MyMigration(Migration): @property def mode(self): - # Avoid having a super long disclaimer + # If the conf is already up to date + # and no DSA key is used, then we're good to go + # and the migration can be done automatically + # (basically nothing shall change) ynh_hash = _get_conf_hashes('ssh') if '/etc/ssh/sshd_config' in ynh_hash: ynh_hash = ynh_hash['/etc/ssh/sshd_config'] @@ -43,7 +57,8 @@ class MyMigration(Migration): if self.mode == "auto": return None - # Detect major risk to migrate to the new configuration + # Detect key things to be aware of before enabling the + # recommended configuration dsa = False ports = [] root_login = [] From 6145199564728e6e6a53b73a3131caf5377599d9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 21:16:51 +0000 Subject: [PATCH 152/250] Improve semantic / simplify a few things --- locales/en.json | 10 ++--- ...0007_ssh_conf_managed_by_yunohost_step2.py | 37 ++++++++----------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/locales/en.json b/locales/en.json index 803d5c937..54792a827 100644 --- a/locales/en.json +++ b/locales/en.json @@ -296,12 +296,12 @@ "migration_0006_done": "Your root password have been replaced by your admin password.", "migration_0006_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.", "migration_0006_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.", - "migration_0007_general_warning": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", + "migration_0007_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", "migration_0007_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;", - "migration_0007_root": " - you will not be able to connect with root user, instead you will have to use the admin user ;", - "migration_0007_dsa": " - you might need to invalidate a warning and to recheck the fingerprint of your server, because DSA key will be disabled ;", - "migration_0007_risk": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration though it is not recommended.", - "migration_0007_no_risk": "No major risk has been indentified about overriding your SSH configuration - but it's difficult to be sure. If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration though it is not recommended.", + "migration_0007_root": " - you will not be able to connect as root through SSH. Instead you should use the admin user ;", + "migration_0007_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a warning from your SSH client, and recheck the fingerprint of your server ;", + "migration_0007_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", + "migration_0007_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;) ! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py index 10e319b2d..c6355ac61 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py @@ -11,6 +11,7 @@ from yunohost.settings import settings_set, settings_get logger = getActionLogger('yunohost.migration') +SSHD_CONF = '/etc/ssh/sshd_config' class MyMigration(Migration): """ @@ -41,10 +42,8 @@ class MyMigration(Migration): # and no DSA key is used, then we're good to go # and the migration can be done automatically # (basically nothing shall change) - ynh_hash = _get_conf_hashes('ssh') - if '/etc/ssh/sshd_config' in ynh_hash: - ynh_hash = ynh_hash['/etc/ssh/sshd_config'] - current_hash = _calculate_hash('/etc/ssh/sshd_config') + ynh_hash = _get_conf_hashes('ssh').get(SSHD_CONF, None) + current_hash = _calculate_hash(SSHD_CONF) dsa = settings_get("service.ssh._deprecated_dsa_hostkey") if ynh_hash == current_hash and not dsa: return "auto" @@ -59,43 +58,39 @@ class MyMigration(Migration): # Detect key things to be aware of before enabling the # recommended configuration - dsa = False + dsa_key_enabled = False ports = [] root_login = [] port_rgx = r'^[ \t]*Port[ \t]+(\d+)[ \t]*(?:#.*)?$' root_rgx = r'^[ \t]*PermitRootLogin[ \t]([^# \t]*)[ \t]*(?:#.*)?$' dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$' - for line in open('/etc/ssh/sshd_config'): + for line in open(SSHD_CONF): ports = ports + re.findall(port_rgx, line) root_login = root_login + re.findall(root_rgx, line) - if not dsa and re.match(dsa_rgx, line) is not None: - dsa = True + if not dsa_key_enabled and re.match(dsa_rgx, line) is not None: + dsa_key_enabled = True - if len(ports) == 0: - ports = ['22'] - - port = ports != ['22'] - - root_user = root_login and root_login[-1] != 'no' + custom_port = ports != ['22'] and ports != [] + root_login_enabled = root_login and root_login[-1] != 'no' # Build message - message = m18n.n("migration_0007_general_warning") + message = m18n.n("migration_0007_general_disclaimer") - if port: + if custom_port: message += "\n\n" + m18n.n("migration_0007_port") - if root_user: + if root_login_enabled: message += "\n\n" + m18n.n("migration_0007_root") - if dsa: + if dsa_key_enabled: message += "\n\n" + m18n.n("migration_0007_dsa") - if port or root_user or dsa: - message += "\n\n" + m18n.n("migration_0007_risk") + if custom_port or root_login_enabled or dsa_key_enabled: + message += "\n\n" + m18n.n("migration_0007_warning") else: - message += "\n\n" + m18n.n("migration_0007_no_risk") + message += "\n\n" + m18n.n("migration_0007_no_warning") return message From 2aa0d2b55bb35c92cce3ba49d0cc217f8e765236 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 23:30:34 +0200 Subject: [PATCH 153/250] Spooky warning is spooky --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 54792a827..53250b0b1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -299,7 +299,7 @@ "migration_0007_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", "migration_0007_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;", "migration_0007_root": " - you will not be able to connect as root through SSH. Instead you should use the admin user ;", - "migration_0007_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a warning from your SSH client, and recheck the fingerprint of your server ;", + "migration_0007_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server ;", "migration_0007_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", "migration_0007_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;) ! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", "migrations_backward": "Migrating backward.", From e596758184204d095da220f5ad20147e6a06160d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 25 Oct 2018 23:34:39 +0200 Subject: [PATCH 154/250] Add a comment about /etc/ssh/sshd_config.to_restor --- src/yunohost/tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index a0549321a..678049900 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -442,6 +442,11 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, service_regen_conf(force=True) # Restore specific ssh conf + # c.f. the install script and in particular + # https://github.com/YunoHost/install_script/pull/50 + # The user can now choose during the install to keep + # the initial, existing sshd configuration + # instead of YunoHost's recommended conf bkp_sshd_conf = '/etc/ssh/sshd_config.to_restore' if os.path.exists(bkp_sshd_conf): os.rename(bkp_sshd_conf, '/etc/ssh/sshd_config') From 325678f541d40d833675823aa7de27bcad338bb9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 27 Nov 2018 23:55:15 +0100 Subject: [PATCH 155/250] More explicit name for setting --- data/hooks/conf_regen/03-ssh | 4 ++-- .../0006_ssh_conf_managed_by_yunohost_step1.py | 10 +++++----- .../0007_ssh_conf_managed_by_yunohost_step2.py | 4 ++-- src/yunohost/settings.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 2c9261193..37b92e3fe 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -13,8 +13,8 @@ do_pre_regen() { [[ -f /proc/net/if_inet6 ]] \ || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config - # Add DSA HostKey to let user remove it with migration 7 - if [[ "$(yunohost settings get 'service.ssh._deprecated_dsa_hostkey')" == "True" ]]; then + # Support legacy setting (this setting might be disabled by a user during a migration) + if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then sed -i '/HostKey \/etc\/ssh\/ssh_host_rsa_key/a HostKey /etc/ssh/ssh_host_dsa_key' sshd_config fi diff --git a/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py index c3a503492..751f56fac 100644 --- a/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py @@ -23,13 +23,13 @@ class MyMigration(Migration): This is the first step of a couple of migrations that ensure SSH conf is managed by YunoHost (even if the "from_script" flag is present, which was previously preventing it from being managed by YunoHost) - + The goal of this first (automatic) migration is to make sure that the sshd_config is managed by the regen-conf mechanism. If the from_script flag exists, then we keep the current SSH conf such that it will appear as "manually modified" to the regenconf. - + In step 2 (manual), the admin will be able to choose wether or not to actually use the recommended configuration, with an appropriate disclaimer. """ @@ -44,15 +44,15 @@ class MyMigration(Migration): dsa = True break if dsa: - settings_set("service.ssh._deprecated_dsa_hostkey", True) + settings_set("service.ssh.allow_deprecated_dsa_hostkey", True) # Create sshd_config.d dir if not os.path.exists(SSHD_CONF + '.d'): mkdir(SSHD_CONF + '.d', 0755, uid='root', gid='root') # Here, we make it so that /etc/ssh/sshd_config is managed - # by the regen conf (in particular in the case where the - # from_script flag is present - in which case it was *not* + # by the regen conf (in particular in the case where the + # from_script flag is present - in which case it was *not* # managed by the regenconf) # But because we can't be sure the user wants to use the # recommended conf, we backup then restore the /etc/ssh/sshd_config diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py index c6355ac61..20267d9e8 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py @@ -28,7 +28,7 @@ class MyMigration(Migration): """ def migrate(self): - settings_set("service.ssh._deprecated_dsa_hostkey", False) + settings_set("service.ssh.allow_deprecated_dsa_hostkey", False) service_regen_conf(names=['ssh'], force=True) def backward(self): @@ -44,7 +44,7 @@ class MyMigration(Migration): # (basically nothing shall change) ynh_hash = _get_conf_hashes('ssh').get(SSHD_CONF, None) current_hash = _calculate_hash(SSHD_CONF) - dsa = settings_get("service.ssh._deprecated_dsa_hostkey") + dsa = settings_get("service.ssh.allow_deprecated_dsa_hostkey") if ynh_hash == current_hash and not dsa: return "auto" diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 1539435c6..391893b4e 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -39,7 +39,7 @@ DEFAULTS = OrderedDict([ # -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._deprecated_dsa_hostkey", {"type": "bool", "default": False}), + ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}), ]) From 23893c43b3431b0fc5094f7b1a2521187243810a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 00:08:53 +0100 Subject: [PATCH 156/250] Increment migrations number --- locales/en.json | 20 +++++++++---------- ...007_ssh_conf_managed_by_yunohost_step1.py} | 4 ++-- ...008_ssh_conf_managed_by_yunohost_step2.py} | 14 ++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) rename src/yunohost/data_migrations/{0006_ssh_conf_managed_by_yunohost_step1.py => 0007_ssh_conf_managed_by_yunohost_step1.py} (96%) rename src/yunohost/data_migrations/{0007_ssh_conf_managed_by_yunohost_step2.py => 0008_ssh_conf_managed_by_yunohost_step2.py} (86%) diff --git a/locales/en.json b/locales/en.json index 53250b0b1..e657d38d1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -274,8 +274,8 @@ "migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6", "migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", - "migration_description_0006_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)", - "migration_description_0007_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)", + "migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)", + "migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)", "migration_0003_backward_impossible": "The stretch migration cannot be reverted.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_patching_sources_list": "Patching the sources.lists ...", @@ -294,14 +294,14 @@ "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migration_0006_done": "Your root password have been replaced by your admin password.", - "migration_0006_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.", - "migration_0006_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.", - "migration_0007_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", - "migration_0007_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;", - "migration_0007_root": " - you will not be able to connect as root through SSH. Instead you should use the admin user ;", - "migration_0007_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server ;", - "migration_0007_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", - "migration_0007_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;) ! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", + "migration_0007_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.", + "migration_0007_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.", + "migration_0008_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:", + "migration_0008_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;", + "migration_0008_root": " - you will not be able to connect as root through SSH. Instead you should use the admin user ;", + "migration_0008_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server ;", + "migration_0008_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", + "migration_0008_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;) ! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", diff --git a/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py similarity index 96% rename from src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py rename to src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py index 751f56fac..82d1fc634 100644 --- a/src/yunohost/data_migrations/0006_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py @@ -85,11 +85,11 @@ class MyMigration(Migration): # Restart ssh and backward if it fail if not _run_service_command('restart', 'ssh'): self.backward() - raise MoulinetteError(m18n.n("migration_0006_cancel")) + raise MoulinetteError(m18n.n("migration_0007_cancel")) def backward(self): # We don't backward completely but it should be enough copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) if not _run_service_command('restart', 'ssh'): - raise MoulinetteError(m18n.n("migration_0006_cannot_restart")) + raise MoulinetteError(m18n.n("migration_0007_cannot_restart")) diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py similarity index 86% rename from src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py rename to src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py index 20267d9e8..c53154192 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step2.py +++ b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py @@ -33,7 +33,7 @@ class MyMigration(Migration): def backward(self): - raise MoulinetteError(m18n.n("migration_0007_backward_impossible")) + raise MoulinetteError(m18n.n("migration_0008_backward_impossible")) @property def mode(self): @@ -77,20 +77,20 @@ class MyMigration(Migration): root_login_enabled = root_login and root_login[-1] != 'no' # Build message - message = m18n.n("migration_0007_general_disclaimer") + message = m18n.n("migration_0008_general_disclaimer") if custom_port: - message += "\n\n" + m18n.n("migration_0007_port") + message += "\n\n" + m18n.n("migration_0008_port") if root_login_enabled: - message += "\n\n" + m18n.n("migration_0007_root") + message += "\n\n" + m18n.n("migration_0008_root") if dsa_key_enabled: - message += "\n\n" + m18n.n("migration_0007_dsa") + message += "\n\n" + m18n.n("migration_0008_dsa") if custom_port or root_login_enabled or dsa_key_enabled: - message += "\n\n" + m18n.n("migration_0007_warning") + message += "\n\n" + m18n.n("migration_0008_warning") else: - message += "\n\n" + m18n.n("migration_0007_no_warning") + message += "\n\n" + m18n.n("migration_0008_no_warning") return message From fad4ff090a9f58ed2864097c1c49f192e67c9ba8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 00:26:25 +0100 Subject: [PATCH 157/250] Use templating for more robustness about which SSH keys are enabled --- data/hooks/conf_regen/03-ssh | 8 ++++++-- data/templates/ssh/sshd_config | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 37b92e3fe..74064a631 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -2,6 +2,8 @@ set -e +. /usr/share/yunohost/helpers.d/utils + do_pre_regen() { pending_dir=$1 @@ -14,11 +16,13 @@ do_pre_regen() { || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config # Support legacy setting (this setting might be disabled by a user during a migration) + ssh_keys=$(ls /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_key 2>/dev/null) if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then - sed -i '/HostKey \/etc\/ssh\/ssh_host_rsa_key/a HostKey /etc/ssh/ssh_host_dsa_key' sshd_config + ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" fi - install -D -m 644 sshd_config "${pending_dir}/etc/ssh/sshd_config" + export $ssh_keys + ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" fi } diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 66aacc5f0..36bd9167d 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -8,9 +8,9 @@ ListenAddress :: ListenAddress 0.0.0.0 Protocol 2 # HostKeys for protocol version 2 -HostKey /etc/ssh/ssh_host_rsa_key -HostKey /etc/ssh/ssh_host_ecdsa_key -HostKey /etc/ssh/ssh_host_ed25519_key +{% for key in ssh_keys %} +HostKey {{ key }} +{% endfor %} #Privilege Separation is turned on for security UsePrivilegeSeparation yes From 8cb029a55e471e1ece3a8a2d7bba00975a6f2d17 Mon Sep 17 00:00:00 2001 From: frju365 Date: Wed, 28 Nov 2018 01:21:28 +0100 Subject: [PATCH 158/250] Better Configuration of nginx (#564) * path-traversal * [fix] try a patch for path-traversal * Use more_set_headers insta --- data/templates/nginx/server.tpl.conf | 18 +++++++++--------- src/yunohost/certificate.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/data/templates/nginx/server.tpl.conf b/data/templates/nginx/server.tpl.conf index 464639952..ee20c29c9 100644 --- a/data/templates/nginx/server.tpl.conf +++ b/data/templates/nginx/server.tpl.conf @@ -11,7 +11,7 @@ server { return 301 https://$http_host$request_uri; } - location /.well-known/autoconfig/mail { + location /.well-known/autoconfig/mail/ { alias /var/www/.well-known/{{ domain }}/autoconfig/mail; } @@ -51,14 +51,14 @@ server { # Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners # https://wiki.mozilla.org/Security/Guidelines/Web_Security # https://observatory.mozilla.org/ - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; - add_header Content-Security-Policy "upgrade-insecure-requests"; - add_header Content-Security-Policy-Report-Only "default-src https: data: 'unsafe-inline' 'unsafe-eval'"; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Download-Options noopen; - add_header X-Permitted-Cross-Domain-Policies none; - add_header X-Frame-Options "SAMEORIGIN"; + more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload"; + more_set_headers "Content-Security-Policy : upgrade-insecure-requests"; + more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval'"; + more_set_headers "X-Content-Type-Options : nosniff"; + more_set_headers "X-XSS-Protection : 1; mode=block"; + more_set_headers "X-Download-Options : noopen"; + more_set_headers "X-Permitted-Cross-Domain-Policies : none"; + more_set_headers "X-Frame-Options : SAMEORIGIN"; {% if domain_cert_ca == "Let's Encrypt" %} # OCSP settings diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 049eeb0f4..801741b31 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -466,7 +466,7 @@ def _configure_for_acme_challenge(auth, domain): nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder nginx_configuration = ''' -location ^~ '/.well-known/acme-challenge' +location ^~ '/.well-known/acme-challenge/' { default_type "text/plain"; alias %s; From 546102547964697768bb6b77e31c2aae78fc6c61 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 16:16:24 +0100 Subject: [PATCH 159/250] tools_adminpw was still checking the password strength despite --force-password --- src/yunohost/tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 271947b3d..baa614fa5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -118,7 +118,7 @@ def tools_ldapinit(): return auth -def tools_adminpw(auth, new_password): +def tools_adminpw(auth, new_password, check_strength=True): """ Change admin password @@ -130,7 +130,8 @@ def tools_adminpw(auth, new_password): from yunohost.utils.password import assert_password_is_strong_enough import spwd - assert_password_is_strong_enough("admin", new_password) + if check_strength: + assert_password_is_strong_enough("admin", new_password) new_hash = _hash_user_password(new_password) @@ -416,7 +417,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, tools_maindomain(auth, domain) # Change LDAP admin password - tools_adminpw(auth, password) + tools_adminpw(auth, password, check_strength=not force_password) # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) From 3d81f032e9aba1d4b2155c8b74300b9bf0d307c0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 17:50:20 +0000 Subject: [PATCH 160/250] Fixes following tests (some sshd_config options do not exists or are deprecated) --- data/helpers.d/utils | 1 + data/hooks/conf_regen/03-ssh | 2 +- data/templates/ssh/sshd_config | 21 +++++++------------ ...0007_ssh_conf_managed_by_yunohost_step1.py | 2 +- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/data/helpers.d/utils b/data/helpers.d/utils index eef9f2a8e..b280c3b21 100644 --- a/data/helpers.d/utils +++ b/data/helpers.d/utils @@ -272,6 +272,7 @@ ynh_local_curl () { ynh_render_template() { local template_path=$1 local output_path=$2 + mkdir -p "$(dirname $output_path)" # Taken from https://stackoverflow.com/a/35009576 python2.7 -c 'import os, sys, jinja2; sys.stdout.write( jinja2.Template(sys.stdin.read() diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 74064a631..a9ed0ee48 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -21,7 +21,7 @@ do_pre_regen() { ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" fi - export $ssh_keys + export ssh_keys ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" fi } diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 36bd9167d..ed9a3136e 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -1,16 +1,14 @@ -# Package generated configuration file -# See the sshd_config(5) manpage for details +# This configuration has been automatically generated +# by YunoHost -# What ports, IPs and protocols we listen for +Protocol 2 Port 22 -# Use these options to restrict which interfaces/protocols sshd will bind to + ListenAddress :: ListenAddress 0.0.0.0 -Protocol 2 -# HostKeys for protocol version 2 -{% for key in ssh_keys %} -HostKey {{ key }} -{% endfor %} + +{% for key in ssh_keys.split() %} +HostKey {{ key }}{% endfor %} #Privilege Separation is turned on for security UsePrivilegeSeparation yes @@ -24,14 +22,11 @@ LoginGraceTime 120 PermitRootLogin no StrictModes yes -RSAAuthentication yes PubkeyAuthentication yes #AuthorizedKeysFile %h/.ssh/authorized_keys # Don't read the user's ~/.rhosts and ~/.shosts files IgnoreRhosts yes -# For this to work you will also need host keys in /etc/ssh_known_hosts -RhostsRSAAuthentication no # similar for protocol version 2 HostbasedAuthentication no # Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication @@ -92,5 +87,3 @@ Match User sftpusers AllowTcpForwarding no GatewayPorts no X11Forwarding no - -Include sshd_config.d/* diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py index 82d1fc634..95e67894c 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py @@ -65,7 +65,7 @@ class MyMigration(Migration): copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) # If we detect the conf as manually modified - ynh_hash = _get_conf_hashes('ssh')[SSHD_CONF] + ynh_hash = _get_conf_hashes('ssh').get(SSHD_CONF, None) current_hash = _calculate_hash(SSHD_CONF) if ynh_hash != current_hash: From f295c83fd3cd50f8c96e156e4a9267fc1f21be45 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 18:59:34 +0000 Subject: [PATCH 161/250] Order of keys matter, ed25519 is recommended --- bin/yunoprompt | 2 +- data/hooks/conf_regen/03-ssh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/yunoprompt b/bin/yunoprompt index bca5c2cb3..2b2a6cfb2 100755 --- a/bin/yunoprompt +++ b/bin/yunoprompt @@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address) # Fetch SSH fingerprints i=0 -for key in $(ls /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_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)) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index a9ed0ee48..e60b3022f 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -16,7 +16,7 @@ do_pre_regen() { || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config # Support legacy setting (this setting might be disabled by a user during a migration) - ssh_keys=$(ls /etc/ssh/ssh_host_{rsa,ecdsa,ed25519}_key 2>/dev/null) + ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" fi From 25efab7f2a91243ef55438fc76cd2deb13fec3dd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 19:45:19 +0000 Subject: [PATCH 162/250] We can't have include blocks in sshd_config :| --- .../0007_ssh_conf_managed_by_yunohost_step1.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py index 95e67894c..73cb162b6 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py @@ -64,24 +64,6 @@ class MyMigration(Migration): service_regen_conf(names=['ssh'], force=True) copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) - # If we detect the conf as manually modified - ynh_hash = _get_conf_hashes('ssh').get(SSHD_CONF, None) - current_hash = _calculate_hash(SSHD_CONF) - if ynh_hash != current_hash: - - # And if there's not already an "Include ssh_config.d/*" directive - include_rgx = r'^[ \t]*Include[ \t]+sshd_config\.d/\*[ \t]*(?:#.*)?$' - add_include = False - for line in open(SSHD_CONF): - if re.match(include_rgx, line) is not None: - add_include = True - break - - # We add an "Include sshd_config.d/*" directive - if add_include: - with open(SSHD_CONF, "a") as conf: - conf.write('Include sshd_config.d/*') - # Restart ssh and backward if it fail if not _run_service_command('restart', 'ssh'): self.backward() From 6a812190c5ffaae014229088b85217a064a379fa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 20:27:42 +0000 Subject: [PATCH 163/250] Enforce permissions for /etc/ssh/sshd_config --- data/hooks/conf_regen/03-ssh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index e60b3022f..dac21b19b 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -15,8 +15,9 @@ do_pre_regen() { [[ -f /proc/net/if_inet6 ]] \ || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config - # Support legacy setting (this setting might be disabled by a user during a migration) ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) + + # Support legacy setting (this setting might be disabled by a user during a migration) if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" fi @@ -27,12 +28,15 @@ do_pre_regen() { } do_post_regen() { - regen_conf_files=$1 - - if [[ ! -f /etc/yunohost/from_script ]]; then - [[ -z "$regen_conf_files" ]] \ - || sudo service ssh restart - fi + regen_conf_files=$1 + if [[ ! -f /etc/yunohost/from_script ]]; then + if [[ -n "$regen_conf_files" ]]; + then + sudo service ssh restart + chown root:root "/etc/ssh/sshd_config" + chmod 644 "/etc/ssh/sshd_config" + fi + fi } FORCE=${2:-0} From 4db65682eb468edc81ae043f2da03909437f557d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 20:42:39 +0000 Subject: [PATCH 164/250] Fix IPv6 handling in ssh regen conf script --- data/hooks/conf_regen/03-ssh | 5 +++-- data/templates/ssh/sshd_config | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index dac21b19b..271ad9bb8 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -11,9 +11,9 @@ do_pre_regen() { # Don't overwrite configuration if from_script if [[ ! -f /etc/yunohost/from_script ]]; then + # do not listen to IPv6 if unavailable - [[ -f /proc/net/if_inet6 ]] \ - || sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config + [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) @@ -23,6 +23,7 @@ do_pre_regen() { fi export ssh_keys + export ipv6_enabled ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" fi } diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index ed9a3136e..9d6c078b9 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -4,7 +4,7 @@ Protocol 2 Port 22 -ListenAddress :: +{% if ipv6_enabled == "true" %}ListenAddress ::{% endif %} ListenAddress 0.0.0.0 {% for key in ssh_keys.split() %} From 0576b17442282867a6b011a41cdaf7bcfafaaad3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 21:03:28 +0000 Subject: [PATCH 165/250] Simplify code / indentation levels --- data/hooks/conf_regen/03-ssh | 53 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 271ad9bb8..76fab7cd4 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -5,39 +5,44 @@ set -e . /usr/share/yunohost/helpers.d/utils do_pre_regen() { - pending_dir=$1 + pending_dir=$1 - cd /usr/share/yunohost/templates/ssh + # If the (legacy) 'from_script' flag is here, + # we won't touch anything in the ssh config. + [[ ! -f /etc/yunohost/from_script ]] || return - # Don't overwrite configuration if from_script - if [[ ! -f /etc/yunohost/from_script ]]; then + cd /usr/share/yunohost/templates/ssh - # do not listen to IPv6 if unavailable - [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false + # do not listen to IPv6 if unavailable + [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false - ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) + ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) - # Support legacy setting (this setting might be disabled by a user during a migration) - if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then - ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" - fi + # Support legacy setting (this setting might be disabled by a user during a migration) + if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then + ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" + fi - export ssh_keys - export ipv6_enabled - ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" - fi + export ssh_keys + export ipv6_enabled + ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config" } do_post_regen() { - regen_conf_files=$1 - if [[ ! -f /etc/yunohost/from_script ]]; then - if [[ -n "$regen_conf_files" ]]; - then - sudo service ssh restart - chown root:root "/etc/ssh/sshd_config" - chmod 644 "/etc/ssh/sshd_config" - fi - fi + regen_conf_files=$1 + + # If the (legacy) 'from_script' flag is here, + # we won't touch anything in the ssh config. + [[ ! -f /etc/yunohost/from_script ]] || return + + # If no file changed, there's nothing to do + [[ -n "$regen_conf_files" ]] || return + + # Enforce permissions for /etc/ssh/sshd_config + chown root:root "/etc/ssh/sshd_config" + chmod 644 "/etc/ssh/sshd_config" + + systemctl restart ssh } FORCE=${2:-0} From 90e542a9315115f1f7489bbf704433e722ee18dc Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 21:30:26 +0000 Subject: [PATCH 166/250] Allow root login on local networks --- data/templates/ssh/sshd_config | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 9d6c078b9..cfc101ffa 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -87,3 +87,11 @@ Match User sftpusers AllowTcpForwarding no GatewayPorts no X11Forwarding no + +# root login is allowed on local networks +# It's meant to be a backup solution in case LDAP is down and +# user admin can't be used... +# If the server is a VPS, it's expected that the owner of the +# server has access to a web console through which to log in. +Match Address 192.168.0.0/16,10.0.0.0/16 + PermitRootLogin yes From 69d0503ba56ec509a62d839f5d97c03ada67464b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 22:01:27 +0000 Subject: [PATCH 167/250] Forgot to get the 'value' key here.. --- 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 68e51056b..6e8f5ba0a 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -60,7 +60,7 @@ class PasswordValidator(object): # (or at least that's my understanding -- Alex) settings = json.load(open('/etc/yunohost/settings.json', "r")) setting_key = "security.password." + profile + ".strength" - self.validation_strength = int(settings[setting_key]) + self.validation_strength = int(settings[setting_key]["value"]) except Exception as e: # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 1 From 847d18293a6c375d062f342425a34d05c62bbd22 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Fri, 30 Nov 2018 15:47:42 +0100 Subject: [PATCH 168/250] [enh] Add other private ip network and link local --- data/templates/ssh/sshd_config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index cfc101ffa..360920751 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -93,5 +93,5 @@ Match User sftpusers # user admin can't be used... # If the server is a VPS, it's expected that the owner of the # server has access to a web console through which to log in. -Match Address 192.168.0.0/16,10.0.0.0/16 +Match Address 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,169.254.0.0/16,fe80::/10,fd00::/8 PermitRootLogin yes From e918836ab09ef72fca590a5a24279f82779e7c25 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 1 Dec 2018 16:21:31 +0100 Subject: [PATCH 169/250] Fix multiple value for getopts (#592) --- data/helpers.d/getopts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/helpers.d/getopts b/data/helpers.d/getopts index 1cc66da8a..6d600e207 100644 --- a/data/helpers.d/getopts +++ b/data/helpers.d/getopts @@ -129,6 +129,8 @@ ynh_handle_getopts_args () { shift_value=$(( shift_value - 1 )) fi + # Declare the content of option_var as a variable. + eval ${option_var}="" # Then read the array value per value for i in `seq 0 $(( ${#all_args[@]} - 1 ))` do @@ -140,8 +142,6 @@ ynh_handle_getopts_args () { break fi else - # Declare the content of option_var as a variable. - eval ${option_var}="" # Else, add this value to this option # Each value will be separated by ';' if [ -n "${!option_var}" ] From c9fec565b5455cf428f5e6eafc66331702bd0d07 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 1 Dec 2018 16:21:59 +0100 Subject: [PATCH 170/250] Log dyndns update only if we really update something (#591) --- src/yunohost/dyndns.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 3e040d682..dd652119f 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -206,9 +206,6 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key = keys[0] - operation_logger.related_to.append(('domain', domain)) - operation_logger.start() - # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. # The actual update will be done in next run. @@ -258,6 +255,8 @@ 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.start() logger.info("Updated needed, going on...") dns_conf = _build_dns_conf(domain) From 09fcea1177213fd3a415392afbb1d7101736057f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 1 Dec 2018 16:22:40 +0100 Subject: [PATCH 171/250] Add libpam-ldapd as dependency to be able to login through SSH with LDAP identities ? (#587) --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 1f4995195..9f72bf11a 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,7 @@ Depends: ${python:Depends}, ${misc:Depends} , dnsutils, bind9utils, unzip, git, curl, cron, wget, jq , ca-certificates, netcat-openbsd, iproute , mariadb-server, php-mysql | php-mysqlnd - , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd + , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd , postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils, postsrsd , dovecot-ldap, dovecot-lmtpd, dovecot-managesieved , dovecot-antispam, fail2ban From 41c1a9ce13634cff7c2c81540cbee1c2fed1cb10 Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sat, 1 Dec 2018 23:07:32 +0100 Subject: [PATCH 172/250] Do not use a separate ini file for php pools (#548) Have a look to https://github.com/YunoHost-Apps/nextcloud_ynh/issues/138 for more information. --- data/helpers.d/backend | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/data/helpers.d/backend b/data/helpers.d/backend index b36235b42..26e53ede9 100644 --- a/data/helpers.d/backend +++ b/data/helpers.d/backend @@ -222,11 +222,7 @@ ynh_add_fpm_config () { if [ -e "../conf/php-fpm.ini" ] then - finalphpini="$fpm_config_dir/conf.d/20-$app.ini" - ynh_backup_if_checksum_is_different "$finalphpini" - sudo cp ../conf/php-fpm.ini "$finalphpini" - sudo chown root: "$finalphpini" - ynh_store_file_checksum "$finalphpini" + echo "Please do not use a separate ini file, merge you directives in the pool file instead." &>2 fi sudo systemctl reload $fpm_service } From 640bc494cb11937bfb2f3901db203c7b84e449c8 Mon Sep 17 00:00:00 2001 From: "ljf (zamentur)" Date: Sun, 2 Dec 2018 02:43:39 +0100 Subject: [PATCH 173/250] [enh] Improve upnp support (#542) --- src/yunohost/firewall.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 7b1c72170..0e1cf6e76 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -343,7 +343,8 @@ def firewall_upnp(action='status', no_refresh=False): # Refresh port mapping using UPnP if not no_refresh: upnpc = miniupnpc.UPnP() - upnpc.discoverdelay = 3000 + upnpc.discoverdelay = 62000 + upnpc.localport = 1900 # Discover UPnP device(s) logger.debug('discovering UPnP devices...') From 02544f837c5a88d828ab829bc1b01e193766d469 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 16:16:24 +0100 Subject: [PATCH 174/250] tools_adminpw was still checking the password strength despite --force-password --- src/yunohost/tools.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 271947b3d..baa614fa5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -118,7 +118,7 @@ def tools_ldapinit(): return auth -def tools_adminpw(auth, new_password): +def tools_adminpw(auth, new_password, check_strength=True): """ Change admin password @@ -130,7 +130,8 @@ def tools_adminpw(auth, new_password): from yunohost.utils.password import assert_password_is_strong_enough import spwd - assert_password_is_strong_enough("admin", new_password) + if check_strength: + assert_password_is_strong_enough("admin", new_password) new_hash = _hash_user_password(new_password) @@ -416,7 +417,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, tools_maindomain(auth, domain) # Change LDAP admin password - tools_adminpw(auth, password) + tools_adminpw(auth, password, check_strength=not force_password) # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) From 207c2516b5943019eaf02d51a05ecf82afaefd2d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 28 Nov 2018 22:01:27 +0000 Subject: [PATCH 175/250] Forgot to get the 'value' key here.. --- 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 68e51056b..6e8f5ba0a 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -60,7 +60,7 @@ class PasswordValidator(object): # (or at least that's my understanding -- Alex) settings = json.load(open('/etc/yunohost/settings.json', "r")) setting_key = "security.password." + profile + ".strength" - self.validation_strength = int(settings[setting_key]) + self.validation_strength = int(settings[setting_key]["value"]) except Exception as e: # Fallback to default value if we can't fetch settings for some reason self.validation_strength = 1 From fe0c127aa8107e52bba890e1cb46abe3c7cf8690 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 1 Dec 2018 16:21:59 +0100 Subject: [PATCH 176/250] Log dyndns update only if we really update something (#591) --- src/yunohost/dyndns.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 3e040d682..dd652119f 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -206,9 +206,6 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, key = keys[0] - operation_logger.related_to.append(('domain', domain)) - operation_logger.start() - # This mean that hmac-md5 is used # (Re?)Trigger the migration to sha256 and return immediately. # The actual update will be done in next run. @@ -258,6 +255,8 @@ 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.start() logger.info("Updated needed, going on...") dns_conf = _build_dns_conf(domain) From 48e20ca9bf4d40cc683ab654035bf9a5f01b4804 Mon Sep 17 00:00:00 2001 From: frju365 Date: Sun, 2 Dec 2018 17:20:03 +0100 Subject: [PATCH 177/250] [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) * [fix] Regen nginx conf to be sure it integrates OCSP Stapling * Typo * Regen nginx each time we enable a new cert * add comment about the reason of the PR --- src/yunohost/certificate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 801741b31..8da6ab52a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -803,6 +803,11 @@ 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'): + # regen nginx conf to be sure it integrates OCSP Stapling + # (We don't do this yet if postinstall is not finished yet) + service_regen_conf(names=['nginx']) + _run_service_command("reload", "nginx") From 65dee220e308a2fad4a65235c6c1ffaebd3761bd Mon Sep 17 00:00:00 2001 From: frju365 Date: Sun, 2 Dec 2018 17:20:03 +0100 Subject: [PATCH 178/250] [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) * [fix] Regen nginx conf to be sure it integrates OCSP Stapling * Typo * Regen nginx each time we enable a new cert * add comment about the reason of the PR --- src/yunohost/certificate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 049eeb0f4..0c52f43b1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -803,6 +803,11 @@ 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'): + # regen nginx conf to be sure it integrates OCSP Stapling + # (We don't do this yet if postinstall is not finished yet) + service_regen_conf(names=['nginx']) + _run_service_command("reload", "nginx") From db93aa6d5cded943b3265b77fc8c87a98a137b06 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 2 Dec 2018 16:23:11 +0000 Subject: [PATCH 179/250] Update changelog for 3.3.2 --- debian/changelog | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/debian/changelog b/debian/changelog index ca4d8d928..084d7f096 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +yunohost (3.3.2) stable; urgency=low + + * [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) + * [fix] Broken new settings and options to control passwords checks / constrains (#589) + * [fix] Log dyndns update only if we really update something (#591) + + -- Alexandre Aubin Sun, 02 Dev 2018 17:23:00 +0000 + +yunohost (3.3.2) stable; urgency=low + + * [fix] Log dyndns update only if we really update something (#591) + * [fix] Broken new settings and options to control passwords checks / constrains (#589) + + -- Alexandre Aubin Sun, 02 Dev 2018 17:17:00 +0000 + yunohost (3.3.1) stable; urgency=low * [fix] Wait for dpkg lock to be free in apt helpers (#571) From 0ac908326a4a79aec6de9f37da8cf7fa9034fcf8 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 14:02:12 +0100 Subject: [PATCH 180/250] Return instead of break, otherwise warning is shown --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 22adb9b15..8b672d701 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -15,7 +15,7 @@ ynh_wait_dpkg_free() { # Sleep an exponential time at each round sleep $(( try * try )) else - break + return 0 fi done echo "apt still used, but timeout reached !" From 447372d07c88172309e122116b5f4086fba82a3e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 17:03:22 +0100 Subject: [PATCH 181/250] [enh] Clean + harden sshd config using Mozilla recommendation (#590) * Clean sshd_config + harden using Mozilla recommendation * Order of keys matter, ed25519 is recommended --- data/hooks/conf_regen/03-ssh | 8 ++- data/templates/ssh/sshd_config | 101 +++++++++++++-------------------- 2 files changed, 48 insertions(+), 61 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index 76fab7cd4..dafa4327e 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -12,10 +12,16 @@ do_pre_regen() { [[ ! -f /etc/yunohost/from_script ]] || return cd /usr/share/yunohost/templates/ssh - + # do not listen to IPv6 if unavailable [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false + # Support legacy setting (this setting might be disabled by a user during a migration) + ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) + if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then + ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)" + fi + ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null) # Support legacy setting (this setting might be disabled by a user during a migration) diff --git a/data/templates/ssh/sshd_config b/data/templates/ssh/sshd_config index 360920751..ed870e5dc 100644 --- a/data/templates/ssh/sshd_config +++ b/data/templates/ssh/sshd_config @@ -10,77 +10,58 @@ ListenAddress 0.0.0.0 {% for key in ssh_keys.split() %} HostKey {{ key }}{% endfor %} -#Privilege Separation is turned on for security -UsePrivilegeSeparation yes +# ############################################## +# Stuff recommended by Mozilla "modern" compat' +# https://infosec.mozilla.org/guidelines/openssh +# ############################################## -# Logging +# Keys, ciphers and MACS +KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 +Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr +MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com + +# Use kernel sandbox mechanisms where possible in unprivileged processes +UsePrivilegeSeparation sandbox + +# LogLevel VERBOSE logs user's key fingerprint on login. +# Needed to have a clear audit track of which key was using to log in. SyslogFacility AUTH -LogLevel INFO +LogLevel VERBOSE + +# ####################### +# Authentication settings +# ####################### + +# Comment from Mozilla about the motivation behind disabling root login +# +# Root login is not allowed for auditing reasons. This is because it's difficult to track which process belongs to which root user: +# +# On Linux, user sessions are tracking using a kernel-side session id, however, this session id is not recorded by OpenSSH. +# Additionally, only tools such as systemd and auditd record the process session id. +# On other OSes, the user session id is not necessarily recorded at all kernel-side. +# Using regular users in combination with /bin/su or /usr/bin/sudo ensure a clear audit track. -# Authentication: LoginGraceTime 120 PermitRootLogin no StrictModes yes - PubkeyAuthentication yes -#AuthorizedKeysFile %h/.ssh/authorized_keys - -# Don't read the user's ~/.rhosts and ~/.shosts files -IgnoreRhosts yes -# similar for protocol version 2 -HostbasedAuthentication no -# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication -#IgnoreUserKnownHosts yes - -# To enable empty passwords, change to yes (NOT RECOMMENDED) PermitEmptyPasswords no - -# Change to yes to enable challenge-response passwords (beware issues with -# some PAM modules and threads) ChallengeResponseAuthentication no - -# Change to no to disable tunnelled clear text passwords -#PasswordAuthentication yes - -# Kerberos options -#KerberosAuthentication no -#KerberosGetAFSToken no -#KerberosOrLocalPasswd yes -#KerberosTicketCleanup yes - -# GSSAPI options -#GSSAPIAuthentication no -#GSSAPICleanupCredentials yes - -X11Forwarding yes -X11DisplayOffset 10 -PrintMotd no -PrintLastLog yes -TCPKeepAlive yes -#UseLogin no - -# keep ssh sessions fresh -ClientAliveInterval 60 - -#MaxStartups 10:30:60 -Banner /etc/issue.net - -# Allow client to pass locale environment variables -AcceptEnv LANG LC_* - -Subsystem sftp internal-sftp - -# Set this to 'yes' to enable PAM authentication, account processing, -# and session processing. If this is enabled, PAM authentication will -# be allowed through the ChallengeResponseAuthentication and -# PasswordAuthentication. Depending on your PAM configuration, -# PAM authentication via ChallengeResponseAuthentication may bypass -# the setting of "PermitRootLogin without-password". -# If you just want the PAM account and session checks to run without -# PAM authentication, then enable this but set PasswordAuthentication -# and ChallengeResponseAuthentication to 'no'. UsePAM yes +# Change to no to disable tunnelled clear text passwords +# (i.e. everybody will need to authenticate using ssh keys) +#PasswordAuthentication yes + +# Post-login stuff +Banner /etc/issue.net +PrintMotd no +PrintLastLog yes +ClientAliveInterval 60 +AcceptEnv LANG LC_* + +# SFTP stuff +Subsystem sftp internal-sftp Match User sftpusers ForceCommand internal-sftp ChrootDirectory /home/%u From 4f05cd5b2b2f12fddf67712c3c263c89fbf4678a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 16:56:28 +0000 Subject: [PATCH 182/250] Uh for some reason we need to return *0* explicitly --- data/hooks/conf_regen/03-ssh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/hooks/conf_regen/03-ssh b/data/hooks/conf_regen/03-ssh index dafa4327e..34cb441b4 100755 --- a/data/hooks/conf_regen/03-ssh +++ b/data/hooks/conf_regen/03-ssh @@ -9,7 +9,7 @@ do_pre_regen() { # If the (legacy) 'from_script' flag is here, # we won't touch anything in the ssh config. - [[ ! -f /etc/yunohost/from_script ]] || return + [[ ! -f /etc/yunohost/from_script ]] || return 0 cd /usr/share/yunohost/templates/ssh @@ -39,10 +39,10 @@ do_post_regen() { # If the (legacy) 'from_script' flag is here, # we won't touch anything in the ssh config. - [[ ! -f /etc/yunohost/from_script ]] || return + [[ ! -f /etc/yunohost/from_script ]] || return 0 # If no file changed, there's nothing to do - [[ -n "$regen_conf_files" ]] || return + [[ -n "$regen_conf_files" ]] || return 0 # Enforce permissions for /etc/ssh/sshd_config chown root:root "/etc/ssh/sshd_config" From e871ff3b20307069ccc28eede7bb31e1f91561aa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 17:04:43 +0000 Subject: [PATCH 183/250] Semantics --- src/yunohost/tools.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 678049900..c5b35a6b5 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -441,15 +441,18 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, service_regen_conf(force=True) - # Restore specific ssh conf - # c.f. the install script and in particular + # Restore original ssh conf, as chosen by the + # admin during the initial install + # + # c.f. the install script and in particular # https://github.com/YunoHost/install_script/pull/50 # The user can now choose during the install to keep # the initial, existing sshd configuration # instead of YunoHost's recommended conf - bkp_sshd_conf = '/etc/ssh/sshd_config.to_restore' - if os.path.exists(bkp_sshd_conf): - os.rename(bkp_sshd_conf, '/etc/ssh/sshd_config') + # + 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') logger.success(m18n.n('yunohost_configured')) From 3949333599dfcad37799c83e6765b4f75a56bf02 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 17:56:01 +0000 Subject: [PATCH 184/250] We need to explicitly ask for the ssh conf to be generated --- src/yunohost/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index c5b35a6b5..6810e5397 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -453,6 +453,10 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, 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') + else: + # We need to explicitly ask the regen conf to regen ssh + # (by default, i.e. first argument = None, it won't because it's too touchy) + service_regen_conf(names=["ssh"], force=True) logger.success(m18n.n('yunohost_configured')) From 3a2464dfb46bc2d6cba7cbe3862a694b6cd5258c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:27:18 +0100 Subject: [PATCH 185/250] Skip migrations one at a time --- src/yunohost/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index baa614fa5..add114b99 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -938,6 +938,10 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai operation_logger.success() + # Skip migrations one at a time + if skip: + break + # special case where we want to go back from the start if target == 0: state["last_run_migration"] = None From 3ca8c089e116b0d6a5344ffdec6ff106ec25140c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:33:39 +0100 Subject: [PATCH 186/250] Have a function to initialize migrations --- src/yunohost/tools.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index add114b99..f6535d85c 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -432,7 +432,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, _install_appslist_fetch_cron() # Init migrations (skip them, no need to run them on a fresh system) - tools_migrations_migrate(skip=True, auto=True) + _skip_all_migrations() os.system('touch /etc/yunohost/installed') @@ -948,7 +948,6 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai write_to_json(MIGRATIONS_STATE_PATH, state) - def tools_migrations_state(): """ Show current migration state @@ -1048,6 +1047,25 @@ def _load_migration(migration_file): raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', number=number, name=name)) +def _skip_all_migrations(): + """ + Skip all pending migrations. + This is meant to be used during postinstall to + initialize the migration system. + """ + state = tools_migrations_state() + + # load all migrations + migrations = _get_migrations_list() + migrations = sorted(migrations, key=lambda x: x.number) + last_migration = migrations[-1] + + state["last_run_migration"] = { + "number": last_migration.number, + "name": last_migration.name + } + write_to_json(MIGRATIONS_STATE_PATH, state) + class Migration(object): From 791ac440375515b6ca4a1c5d53c2f1529bd9b47d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 22:10:06 +0100 Subject: [PATCH 187/250] Add success message after running migrations --- locales/en.json | 1 + src/yunohost/tools.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/locales/en.json b/locales/en.json index 6ce22ca80..3bb36ae9d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -305,6 +305,7 @@ "migrations_show_currently_running_migration": "Running migration {number} {name}...", "migrations_show_last_migration": "Last ran migration is {}", "migrations_skip_migration": "Skipping migration {number} {name}...", + "migrations_success": "Successfully ran migration {number} {name}!", "migrations_to_be_ran_manually": "Migration {number} {name} has to be ran manually. Please go to Tools > Migrations on the webadmin, or run `yunohost tools migrations migrate`.", "migrations_need_to_accept_disclaimer": "To run the migration {number} {name}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option --accept-disclaimer.", "monitor_disabled": "The server monitoring has been disabled", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index f6535d85c..fbadb3b20 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -924,6 +924,9 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai logger.error(msg, exc_info=1) operation_logger.error(msg) break + else: + logger.success(m18n.n('migrations_success', + number=migration.number, name=migration.name)) else: # if skip logger.warn(m18n.n('migrations_skip_migration', From 0fa374db40171f6412b2cc4f10d1bb2dd7ffc014 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 22:11:06 +0100 Subject: [PATCH 188/250] This message should be info --- 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 fbadb3b20..3bffe3606 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -903,7 +903,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai if not skip: - logger.warn(m18n.n('migrations_show_currently_running_migration', + logger.info(m18n.n('migrations_show_currently_running_migration', number=migration.number, name=migration.name)) try: From 783ecf8f4158876be6192abf576f9acbef379268 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:24:53 +0100 Subject: [PATCH 189/250] Manage migration of auto/manual and disclaimer on a per-migration basis --- src/yunohost/tools.py | 47 +++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 3bffe3606..d8cd53f56 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -872,31 +872,34 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai else: # can't happen, this case is handle before raise Exception() - # If we are migrating in "automatic mode" (i.e. from debian - # configure during an upgrade of the package) but we are asked to run - # migrations is to be ran manually by the user - manual_migrations = [m for m in migrations if m.mode == "manual"] - if not skip and auto and manual_migrations: - for m in manual_migrations: - logger.warn(m18n.n('migrations_to_be_ran_manually', - number=m.number, - name=m.name)) - return - - # If some migrations have disclaimers, require the --accept-disclaimer - # option - migrations_with_disclaimer = [m for m in migrations if m.disclaimer] - if not skip and not accept_disclaimer and migrations_with_disclaimer: - for m in migrations_with_disclaimer: - logger.warn(m18n.n('migrations_need_to_accept_disclaimer', - number=m.number, - name=m.name, - disclaimer=m.disclaimer)) - return - # effectively run selected migrations for migration in migrations: + if not skip: + # If we are migrating in "automatic mode" (i.e. from debian configure + # during an upgrade of the package) but we are asked to run 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', + number=migration.number, + name=migration.name)) + break + + # If some migrations have disclaimers, + if migration.disclaimer: + # require the --accept-disclaimer option. Otherwise, stop everything + # here and display the disclaimer + if not accept_disclaimer: + logger.warn(m18n.n('migrations_need_to_accept_disclaimer', + number=migration.number, + name=migration.name, + disclaimer=migration.disclaimer)) + break + # --accept-disclaimer will only work for the first migration + else: + accept_disclaimer = False + # Start register change on system operation_logger= OperationLogger('tools_migrations_migrate_' + mode) operation_logger.start() From df56cf90aa18235ee5103681dc76ff5dece841e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Fri, 7 Dec 2018 18:28:30 +0100 Subject: [PATCH 190/250] Ignore test directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6dd427aba..75f4ae6ea 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ pip-log.txt # moulinette lib src/yunohost/locales + +# Test +src/yunohost/tests/apps From 1ac97c46adc4518eefdf2a43b57c86e9626b087d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Dec 2018 19:12:47 +0100 Subject: [PATCH 191/250] Explicit root password change each time admin password is changed --- locales/en.json | 2 +- .../data_migrations/0006_sync_admin_and_root_passwords.py | 2 +- src/yunohost/tools.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6ce22ca80..6a2852f80 100644 --- a/locales/en.json +++ b/locales/en.json @@ -291,7 +291,6 @@ "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", - "migration_0006_done": "Your root password have been replaced by your admin password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", @@ -375,6 +374,7 @@ "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "root_password_desynchronized": "The admin password has been changed, but YunoHost was unable to propagate this on the root password !", + "root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.", "server_shutdown": "The server will shutdown", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_reboot": "The server will reboot", diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 366363f22..ee3aeefcb 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -23,7 +23,7 @@ class MyMigration(Migration): new_hash = self._get_admin_hash() self._replace_root_hash(new_hash) - logger.info(m18n.n("migration_0006_done")) + logger.info(m18n.n("root_password_replaced_by_admin_password")) def backward(self): pass diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index baa614fa5..fea1f8398 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -155,6 +155,8 @@ def tools_adminpw(auth, new_password, check_strength=True): except IOError as e: 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')) From 03b1a2867ce4f8c0cce772c951c2f8f89acfa635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Fri, 7 Dec 2018 16:35:01 +0100 Subject: [PATCH 192/250] Fix tests --- locales/en.json | 2 + src/yunohost/settings.py | 7 ++- src/yunohost/tests/test_backuprestore.py | 68 ++++++++++-------------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6ce22ca80..980dfb071 100644 --- a/locales/en.json +++ b/locales/en.json @@ -194,6 +194,8 @@ "global_settings_setting_example_enum": "Example enum option", "global_settings_setting_example_int": "Example int option", "global_settings_setting_example_string": "Example string option", + "global_settings_setting_security_password_admin_strength": "Admin password strength", + "global_settings_setting_security_password_user_strength": "User password strength", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index d2526316e..ee2f35785 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -95,7 +95,12 @@ def settings_set(key, value): elif key_type == "int": if not isinstance(value, int) or isinstance(value, bool): if isinstance(value, str): - value=int(value) + try: + value=int(value) + except: + raise MoulinetteError(errno.EINVAL, m18n.n( + 'global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type)) else: raise MoulinetteError(errno.EINVAL, m18n.n( 'global_settings_bad_type_for_setting', setting=key, diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 1071c1642..c60dcb517 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -195,7 +195,7 @@ def add_archive_system_from_2p4(): def test_backup_only_ldap(): # Create the backup - backup_create(ignore_system=False, ignore_apps=True, system=["conf_ldap"]) + backup_create(system=["conf_ldap"]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -212,7 +212,7 @@ def test_backup_system_part_that_does_not_exists(mocker): # Create the backup with pytest.raises(MoulinetteError): - backup_create(ignore_system=False, ignore_apps=True, system=["yolol"]) + backup_create(system=["yolol"]) m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") m18n.n.assert_any_call('backup_nothings_done') @@ -224,7 +224,7 @@ def test_backup_system_part_that_does_not_exists(mocker): def test_backup_and_restore_all_sys(): # Create the backup - backup_create(ignore_system=False, ignore_apps=True) + backup_create(system=[]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -241,7 +241,7 @@ def test_backup_and_restore_all_sys(): # Restore the backup backup_restore(auth, name=archives[0], force=True, - ignore_system=False, ignore_apps=True) + system=[]) # Check ssowat conf is back assert os.path.exists("/etc/ssowat/conf.json") @@ -255,21 +255,19 @@ def test_backup_and_restore_all_sys(): def test_restore_system_from_Ynh2p4(monkeypatch, mocker): # Backup current system - backup_create(ignore_system=False, ignore_apps=True) + backup_create(system=[]) archives = backup_list()["archives"] assert len(archives) == 2 # Restore system archive from 2.4 try: backup_restore(auth, name=backup_list()["archives"][1], - ignore_system=False, - ignore_apps=True, + system=[], force=True) finally: # Restore system as it was backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=False, - ignore_apps=True, + system=[], force=True) ############################################################################### @@ -293,7 +291,7 @@ def test_backup_script_failure_handling(monkeypatch, mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app') @@ -313,7 +311,7 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call('not_enough_disk_space', path=ANY) @@ -325,7 +323,7 @@ def test_backup_app_not_installed(mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_create(ignore_system=True, ignore_apps=False, apps=["wordpress"]) + backup_create(system=None, apps=["wordpress"]) m18n.n.assert_any_call("unbackup_app", app="wordpress") m18n.n.assert_any_call('backup_nothings_done') @@ -341,7 +339,7 @@ def test_backup_app_with_no_backup_script(mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app") m18n.n.assert_any_call('backup_nothings_done') @@ -359,7 +357,7 @@ def test_backup_app_with_no_restore_script(mocker): # Backuping an app with no restore script will only display a warning to the # user... - backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"]) + backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call("backup_with_no_restore_script_for_app", app="backup_recommended_app") @@ -368,7 +366,7 @@ def test_backup_app_with_no_restore_script(mocker): def test_backup_with_different_output_directory(): # Create the backup - backup_create(ignore_system=False, ignore_apps=True, system=["conf_ssh"], + backup_create(system=["conf_ssh"], output_directory="/opt/test_backup_output_directory", name="backup") @@ -385,7 +383,7 @@ def test_backup_with_different_output_directory(): @pytest.mark.clean_opt_dir def test_backup_with_no_compress(): # Create the backup - backup_create(ignore_system=False, ignore_apps=True, system=["conf_nginx"], + backup_create(system=["conf_nginx"], output_directory="/opt/test_backup_output_directory", no_compress=True, name="backup") @@ -400,9 +398,7 @@ def test_backup_with_no_compress(): @pytest.mark.with_wordpress_archive_from_2p4 def test_restore_app_wordpress_from_Ynh2p4(): - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) @@ -420,9 +416,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") with pytest.raises(MoulinetteError): - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) m18n.n.assert_any_call('restore_app_failed', app='wordpress') @@ -443,9 +437,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): assert not _is_installed("wordpress") with pytest.raises(MoulinetteError): - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) m18n.n.assert_any_call('restore_not_enough_disk_space', @@ -464,9 +456,7 @@ def test_restore_app_not_in_backup(mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["yoloswag"]) m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag") @@ -479,18 +469,14 @@ def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) assert _is_installed("wordpress") mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_restore(auth, name=backup_list()["archives"][0], - ignore_system=True, - ignore_apps=False, + backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) m18n.n.assert_any_call('restore_already_installed_app', app="wordpress") @@ -520,7 +506,7 @@ def test_backup_and_restore_with_ynh_restore(): def _test_backup_and_restore_app(app): # Create a backup of this app - backup_create(ignore_system=True, ignore_apps=False, apps=[app]) + backup_create(system=None, apps=[app]) archives = backup_list()["archives"] assert len(archives) == 1 @@ -535,8 +521,8 @@ def _test_backup_and_restore_app(app): assert not app_is_installed(app) # Restore the app - backup_restore(auth, name=archives[0], ignore_system=True, - ignore_apps=False, apps=[app]) + backup_restore(auth, system=None, name=archives[0], + apps=[app]) assert app_is_installed(app) @@ -554,8 +540,7 @@ def test_restore_archive_with_no_json(mocker): mocker.spy(m18n, "n") with pytest.raises(MoulinetteError): - backup_restore(auth, name="badbackup", force=True, - ignore_system=False, ignore_apps=False) + backup_restore(auth, name="badbackup", force=True) m18n.n.assert_any_call('backup_invalid_archive') @@ -565,9 +550,10 @@ def test_backup_binds_are_readonly(monkeypatch): self.manager = backup_manager self._organize_files() + confssh = os.path.join(self.work_dir, "conf/ssh") output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh, - shell=True) + shell=True, env={'LANG' : 'en_US.UTF-8'}) assert "Read-only file system" in output @@ -580,4 +566,4 @@ def test_backup_binds_are_readonly(monkeypatch): custom_mount_and_backup) # Create the backup - backup_create(ignore_system=False, ignore_apps=True) + backup_create(system=[]) From 1f13676d06717dbaa38530df0e4430d476be6f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Fri, 7 Dec 2018 23:14:57 +0100 Subject: [PATCH 193/250] Fix LDAP authenticator after backup --- src/yunohost/tests/test_changeurl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index 737b68a6d..36d8028c9 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -19,8 +19,10 @@ maindomain = _get_maindomain() def setup_function(function): - pass - + # For some reason the nginx reload can take some time to propagate + time.sleep(1) + global auth + auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) def teardown_function(function): app_remove(auth, "change_url_app") From f93aa40c2e2ed2a071ce2556bfac08ea5712c959 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 8 Dec 2018 00:18:02 +0100 Subject: [PATCH 194/250] Use 127.0.0.1 + domain header for local HTTP requests --- src/yunohost/tests/test_changeurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index 36d8028c9..13f8aca5d 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -40,7 +40,7 @@ def check_changeurl_app(path): assert appmap[maindomain][path + "/"]["id"] == "change_url_app" - r = requests.get("https://%s%s/" % (maindomain, path), verify=False) + r = requests.get("https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False) assert r.status_code == 200 From c2631d74cde608e3a060637391d34bf4ac05e411 Mon Sep 17 00:00:00 2001 From: nqb Date: Sat, 8 Dec 2018 04:39:30 +0100 Subject: [PATCH 195/250] fix change quotes around CAA value for letsencrypt.org --- src/yunohost/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index dd2eda4a3..7b387618a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -340,7 +340,7 @@ def _build_dns_conf(domain, ttl=3600): {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} ], "extra": [ - {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org'", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, ], } """ @@ -397,7 +397,7 @@ def _build_dns_conf(domain, ttl=3600): # Extra extra = [ - ["@", ttl, "CAA", "128 issue 'letsencrypt.org'"] + ["@", ttl, "CAA", '128 issue "letsencrypt.org"'] ] return { From 830341a687e2e0aeac3304f8508c20fc1b47f9c4 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 19:27:44 +0100 Subject: [PATCH 196/250] [enh] Display date correctly --- data/actionsmap/yunohost.yml | 5 ----- debian/control | 2 +- src/yunohost/app.py | 14 ++++++-------- src/yunohost/log.py | 13 ++++++++++--- src/yunohost/monitor.py | 2 +- src/yunohost/service.py | 17 ++++++++--------- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index a6bc8ec3e..ba5f1d505 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -470,11 +470,6 @@ app: listlists: action_help: List registered application lists api: GET /appslists - arguments: - -H: - full: --human-readable - help: Return the lastUpdate with a human readable date - action: store_true ### app_removelist() removelist: diff --git a/debian/control b/debian/control index 9f72bf11a..f0423c4a3 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Package: yunohost Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) - , python-psutil, python-requests, python-dnspython, python-openssl + , python-psutil, python-requests, python-dnspython, python-openssl, python-tz , python-apt, python-miniupnpc, python-dbus, python-jinja2 , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget, jq diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 66d83db9c..d5121a418 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -37,6 +37,7 @@ import pwd import grp from collections import OrderedDict import datetime +import pytz from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError @@ -67,7 +68,7 @@ re_app_instance_name = re.compile( ) -def app_listlists(human_readable=False): +def app_listlists(): """ List fetched lists @@ -84,13 +85,10 @@ def app_listlists(human_readable=False): # Get the list appslist_list = _read_appslist_list() - # Human readable date - if human_readable: - for app in appslist_list: - now_for_humans = datetime.datetime.fromtimestamp( - appslist_list[app].get("lastUpdate")) - appslist_list[app]["lastUpdate"] = now_for_humans.strftime( - '%Y-%m-%d %H:%M:%S') + for app in appslist_list: + last_update = datetime.datetime.utcfromtimestamp( + appslist_list[app].get("lastUpdate")) + appslist_list[app]["lastUpdate"] = last_update.replace(tzinfo=pytz.utc) return appslist_list diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c105b8279..c75300d3d 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -28,6 +28,7 @@ import os import yaml import errno import collections +import pytz from datetime import datetime from logging import FileHandler, getLogger, Formatter @@ -100,7 +101,7 @@ def log_list(category=[], limit=None): except ValueError: pass else: - entry["started_at"] = log_datetime + entry["started_at"] = log_datetime.replace(tzinfo=pytz.utc) result[category].append(entry) @@ -182,6 +183,10 @@ def log_display(path, number=50, share=False): metadata = yaml.safe_load(md_file) infos['metadata_path'] = md_path infos['metadata'] = metadata + if 'started_at' in infos['metadata']: + infos['metadata']['started_at'] = infos['metadata']['started_at'].replace(tzinfo=pytz.utc) + if 'ended_at' in infos['metadata']: + infos['metadata']['ended_at'] = infos['metadata']['ended_at'].replace(tzinfo=pytz.utc) if 'log_path' in metadata: log_path = metadata['log_path'] except yaml.YAMLError: @@ -316,7 +321,8 @@ class OperationLogger(object): """ if self.started_at is None: - self.started_at = datetime.now() + self.started_at = datetime.utcnow() + self.started_at = self.started_at.replace(tzinfo=pytz.utc) self.flush() self._register_log() @@ -408,7 +414,8 @@ class OperationLogger(object): return if error is not None and not isinstance(error, basestring): error = str(error) - self.ended_at = datetime.now() + self.ended_at = datetime.utcnow() + self.ended_at = self.ended_at.replace(tzinfo=pytz.utc) self._error = error self._success = error is None if self.logger is not None: diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index fc10a4fbc..cad467ed5 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -283,7 +283,7 @@ def monitor_system(units=None, human_readable=False): elif u == 'process': result[u] = json.loads(glances.getProcessCount()) elif u == 'uptime': - result[u] = (str(datetime.now() - datetime.fromtimestamp(psutil.boot_time())).split('.')[0]) + result[u] = (str(datetime.utcnow() - datetime.utcfromtimestamp(psutil.boot_time())).split('.')[0]) elif u == 'infos': result[u] = json.loads(glances.getSystem()) else: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 5b7680a80..73383cb44 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -31,6 +31,7 @@ import subprocess import errno import shutil import hashlib +import pytz from difflib import unified_diff from datetime import datetime @@ -248,10 +249,7 @@ def service_status(names=[]): 'status': "unknown", 'loaded': "unknown", 'active': "unknown", - 'active_at': { - "timestamp": "unknown", - "human": "unknown", - }, + 'active_at': "unknown", 'description': "Error: failed to get information for this service, it doesn't exists for systemd", 'service_file_path': "unknown", } @@ -273,13 +271,14 @@ def service_status(names=[]): 'status': str(status.get("SubState", "unknown")), 'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")), 'active': str(status.get("ActiveState", "unknown")), - 'active_at': { - "timestamp": str(status.get("ActiveEnterTimestamp", "unknown")), - "human": datetime.fromtimestamp(status["ActiveEnterTimestamp"] / 1000000).strftime("%F %X") if "ActiveEnterTimestamp" in status else "unknown", - }, 'description': description, 'service_file_path': str(status.get("FragmentPath", "unknown")), } + if "ActiveEnterTimestamp" in status: + result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000) + result[name]['active_at'] = result[name]['active_at'].replace(tzinfo=pytz.utc) + else: + result[name]['active_at'] = "unknown" if len(names) == 1: return result[names[0]] @@ -293,7 +292,7 @@ def _get_service_information_from_systemd(service): d = dbus.SystemBus() - systemd = d.get_object('org.freedesktop.systemd1','/org/freedesktop/systemd1') + systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager') try: From c417c628948e1bbe928fd213e13fec0d2d554ce8 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 19:35:38 +0100 Subject: [PATCH 197/250] [fix] Bad comment --- src/yunohost/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d5121a418..e107d7ac9 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -72,9 +72,6 @@ def app_listlists(): """ List fetched lists - Keyword argument: - human_readable -- Show human readable dates - """ # Migrate appslist system if needed From 7754f2722a9eb3b1aace286fb6ccdc9f97c9983b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 3 Dec 2018 14:02:12 +0100 Subject: [PATCH 198/250] Return instead of break, otherwise warning is shown --- data/helpers.d/package | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/helpers.d/package b/data/helpers.d/package index 22adb9b15..8b672d701 100644 --- a/data/helpers.d/package +++ b/data/helpers.d/package @@ -15,7 +15,7 @@ ynh_wait_dpkg_free() { # Sleep an exponential time at each round sleep $(( try * try )) else - break + return 0 fi done echo "apt still used, but timeout reached !" From cd3ed6af6fa6e590ef3736ed61c7d3962740a9ef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 7 Dec 2018 19:12:47 +0100 Subject: [PATCH 199/250] Explicit root password change each time admin password is changed --- locales/en.json | 2 +- .../data_migrations/0006_sync_admin_and_root_passwords.py | 2 +- src/yunohost/tools.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6ce22ca80..6a2852f80 100644 --- a/locales/en.json +++ b/locales/en.json @@ -291,7 +291,6 @@ "migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...", "migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", - "migration_0006_done": "Your root password have been replaced by your admin password.", "migrations_backward": "Migrating backward.", "migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s", @@ -375,6 +374,7 @@ "restore_running_hooks": "Running restoration hooks...", "restore_system_part_failed": "Unable to restore the '{part:s}' system part", "root_password_desynchronized": "The admin password has been changed, but YunoHost was unable to propagate this on the root password !", + "root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.", "server_shutdown": "The server will shutdown", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_reboot": "The server will reboot", diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 366363f22..ee3aeefcb 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -23,7 +23,7 @@ class MyMigration(Migration): new_hash = self._get_admin_hash() self._replace_root_hash(new_hash) - logger.info(m18n.n("migration_0006_done")) + logger.info(m18n.n("root_password_replaced_by_admin_password")) def backward(self): pass diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index baa614fa5..fea1f8398 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -155,6 +155,8 @@ def tools_adminpw(auth, new_password, check_strength=True): except IOError as e: 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')) From ccd9c1631ea30f4314681662e5ab91f842548f62 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:27:18 +0100 Subject: [PATCH 200/250] Skip migrations one at a time --- src/yunohost/tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index fea1f8398..fce2f1569 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -940,6 +940,10 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai operation_logger.success() + # Skip migrations one at a time + if skip: + break + # special case where we want to go back from the start if target == 0: state["last_run_migration"] = None From 8c3905c5d3de183227d9836c753b7fbdf64c261d Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:33:39 +0100 Subject: [PATCH 201/250] Have a function to initialize migrations --- src/yunohost/tools.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index fce2f1569..ebb9516f1 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -434,7 +434,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, _install_appslist_fetch_cron() # Init migrations (skip them, no need to run them on a fresh system) - tools_migrations_migrate(skip=True, auto=True) + _skip_all_migrations() os.system('touch /etc/yunohost/installed') @@ -950,7 +950,6 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai write_to_json(MIGRATIONS_STATE_PATH, state) - def tools_migrations_state(): """ Show current migration state @@ -1050,6 +1049,25 @@ def _load_migration(migration_file): raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', number=number, name=name)) +def _skip_all_migrations(): + """ + Skip all pending migrations. + This is meant to be used during postinstall to + initialize the migration system. + """ + state = tools_migrations_state() + + # load all migrations + migrations = _get_migrations_list() + migrations = sorted(migrations, key=lambda x: x.number) + last_migration = migrations[-1] + + state["last_run_migration"] = { + "number": last_migration.number, + "name": last_migration.name + } + write_to_json(MIGRATIONS_STATE_PATH, state) + class Migration(object): From fee5b1efa108e705451d7eeb05ba28715ccc79d1 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 22:10:06 +0100 Subject: [PATCH 202/250] Add success message after running migrations --- locales/en.json | 1 + src/yunohost/tools.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/locales/en.json b/locales/en.json index 6a2852f80..bc90c93a6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -304,6 +304,7 @@ "migrations_show_currently_running_migration": "Running migration {number} {name}...", "migrations_show_last_migration": "Last ran migration is {}", "migrations_skip_migration": "Skipping migration {number} {name}...", + "migrations_success": "Successfully ran migration {number} {name}!", "migrations_to_be_ran_manually": "Migration {number} {name} has to be ran manually. Please go to Tools > Migrations on the webadmin, or run `yunohost tools migrations migrate`.", "migrations_need_to_accept_disclaimer": "To run the migration {number} {name}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option --accept-disclaimer.", "monitor_disabled": "The server monitoring has been disabled", diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index ebb9516f1..42e5cd690 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -926,6 +926,9 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai logger.error(msg, exc_info=1) operation_logger.error(msg) break + else: + logger.success(m18n.n('migrations_success', + number=migration.number, name=migration.name)) else: # if skip logger.warn(m18n.n('migrations_skip_migration', From 3d5cb7e3d17ca8e9d4e3cecba1b017323bb21dc4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 22:11:06 +0100 Subject: [PATCH 203/250] This message should be info --- 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 42e5cd690..63863a57f 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -905,7 +905,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai if not skip: - logger.warn(m18n.n('migrations_show_currently_running_migration', + logger.info(m18n.n('migrations_show_currently_running_migration', number=migration.number, name=migration.name)) try: From 34c3968501bf4131e4472c45a4dd337c5f77e5d7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 6 Dec 2018 18:24:53 +0100 Subject: [PATCH 204/250] Manage migration of auto/manual and disclaimer on a per-migration basis --- src/yunohost/tools.py | 47 +++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 63863a57f..78e641189 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -874,31 +874,34 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai else: # can't happen, this case is handle before raise Exception() - # If we are migrating in "automatic mode" (i.e. from debian - # configure during an upgrade of the package) but we are asked to run - # migrations is to be ran manually by the user - manual_migrations = [m for m in migrations if m.mode == "manual"] - if not skip and auto and manual_migrations: - for m in manual_migrations: - logger.warn(m18n.n('migrations_to_be_ran_manually', - number=m.number, - name=m.name)) - return - - # If some migrations have disclaimers, require the --accept-disclaimer - # option - migrations_with_disclaimer = [m for m in migrations if m.disclaimer] - if not skip and not accept_disclaimer and migrations_with_disclaimer: - for m in migrations_with_disclaimer: - logger.warn(m18n.n('migrations_need_to_accept_disclaimer', - number=m.number, - name=m.name, - disclaimer=m.disclaimer)) - return - # effectively run selected migrations for migration in migrations: + if not skip: + # If we are migrating in "automatic mode" (i.e. from debian configure + # during an upgrade of the package) but we are asked to run 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', + number=migration.number, + name=migration.name)) + break + + # If some migrations have disclaimers, + if migration.disclaimer: + # require the --accept-disclaimer option. Otherwise, stop everything + # here and display the disclaimer + if not accept_disclaimer: + logger.warn(m18n.n('migrations_need_to_accept_disclaimer', + number=migration.number, + name=migration.name, + disclaimer=migration.disclaimer)) + break + # --accept-disclaimer will only work for the first migration + else: + accept_disclaimer = False + # Start register change on system operation_logger= OperationLogger('tools_migrations_migrate_' + mode) operation_logger.start() From b71ee3a9a0d89fd6405c78308c74c2856cc01d05 Mon Sep 17 00:00:00 2001 From: nqb Date: Sat, 8 Dec 2018 04:39:30 +0100 Subject: [PATCH 205/250] fix change quotes around CAA value for letsencrypt.org --- src/yunohost/domain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index dd2eda4a3..7b387618a 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -340,7 +340,7 @@ def _build_dns_conf(domain, ttl=3600): {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} ], "extra": [ - {"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org'", "ttl": 3600}, + {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600}, ], } """ @@ -397,7 +397,7 @@ def _build_dns_conf(domain, ttl=3600): # Extra extra = [ - ["@", ttl, "CAA", "128 issue 'letsencrypt.org'"] + ["@", ttl, "CAA", '128 issue "letsencrypt.org"'] ] return { From bda028f2b25928840b5c9d9fb4b38456451ff67b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 9 Dec 2018 19:59:05 +0000 Subject: [PATCH 206/250] Update changelog for 3.3.3 --- debian/changelog | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/debian/changelog b/debian/changelog index 084d7f096..3abd967e1 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,17 +1,19 @@ +yunohost (3.3.3) stable; urgency=low + + * [fix] ynh_wait_dpkg_free displaying a warning despite everything being okay (#593) + * [fix] Quotes for recommended CAA DNS record (#596) + * [fix] Manual migration and disclaimer behaviors (#594) + * [fix] Explicit root password change each time admin password is changed + + -- Alexandre Aubin Sun, 09 Dec 2018 20:58:00 +0000 + yunohost (3.3.2) stable; urgency=low * [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) * [fix] Broken new settings and options to control passwords checks / constrains (#589) * [fix] Log dyndns update only if we really update something (#591) - -- Alexandre Aubin Sun, 02 Dev 2018 17:23:00 +0000 - -yunohost (3.3.2) stable; urgency=low - - * [fix] Log dyndns update only if we really update something (#591) - * [fix] Broken new settings and options to control passwords checks / constrains (#589) - - -- Alexandre Aubin Sun, 02 Dev 2018 17:17:00 +0000 + -- Alexandre Aubin Sun, 02 Dec 2018 17:23:00 +0000 yunohost (3.3.1) stable; urgency=low From 60843edc0f6bd6d59bb785f620c95958d71f0108 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 21:08:36 +0100 Subject: [PATCH 207/250] [enh] Use aware UTC date in backup settings and app --- src/yunohost/app.py | 3 +-- src/yunohost/backup.py | 6 ++++-- src/yunohost/settings.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e107d7ac9..0664a24b2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1762,8 +1762,7 @@ def _get_app_status(app_id, format_date=False): if not v: status[f] = '-' else: - status[f] = time.strftime(m18n.n('format_datetime_short'), - time.gmtime(v)) + status[f] = datetime.utcfromtimestamp(v).replace(tzinfo=pytz.utc) return status diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 6ef9a0554..492687eaf 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -33,6 +33,8 @@ import shutil import subprocess import csv import tempfile +import pytz +from datetime import datetime from glob import glob from collections import OrderedDict @@ -2266,8 +2268,8 @@ def backup_info(name, with_details=False, human_readable=False): result = { 'path': archive_file, - 'created_at': time.strftime(m18n.n('format_datetime_short'), - time.gmtime(info['created_at'])), + 'created_at': datetime.utcfromtimestamp(info['created_at']) + .replace(tzinfo=pytz.utc), 'description': info['description'], 'size': size, } diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index d2526316e..3227ad3e3 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -154,7 +154,7 @@ 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.now().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(): From e43bcf253cef4137120e0fc8faa498ebc015a020 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 21:24:59 +0100 Subject: [PATCH 208/250] [fix] Avoid -1 remaining_days --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8da6ab52a..9975a349f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -154,7 +154,7 @@ def _certificate_install_selfsigned(domain_list, force=False): args={'force': force}) # Paths of files and folder we'll need - date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % ( CERT_FOLDER, domain, date_tag) @@ -587,7 +587,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): logger.debug("Saving the key and signed certificate...") # Create corresponding directory - date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") if staging: folder_flag = "staging" @@ -674,7 +674,7 @@ def _get_status(domain): cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") - days_remaining = (valid_up_to - datetime.now()).days + days_remaining = (valid_up_to - datetime.utcnow()).days if cert_issuer == _name_self_CA(): CA_type = { @@ -816,7 +816,7 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(CERT_FOLDER, domain) - date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag) shutil.copytree(cert_folder_domain, backup_folder) From fa5a226339964d3ee0489ebfaecee83b67a0e82a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 9 Dec 2018 21:26:37 +0100 Subject: [PATCH 209/250] Add post_cert_update hook each time certificate is updated (#586) --- src/yunohost/certificate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8da6ab52a..adb546329 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -810,6 +810,9 @@ def _enable_certificate(domain, new_cert_folder): _run_service_command("reload", "nginx") + from yunohost.hook import hook_callback + hook_callback('post_cert_update', args=[domain]) + def _backup_current_cert(domain): logger.debug("Backuping existing certificate for domain %s", domain) From 08818757cc356d5fd3da9d70a4cd49c66934759d Mon Sep 17 00:00:00 2001 From: Maniack Crudelis Date: Sun, 9 Dec 2018 21:27:43 +0100 Subject: [PATCH 210/250] [enh] Do not fail on backup and restore if some missing files are not mandatory (#576) --- data/helpers.d/filesystem | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index badc0e997..b5ae36a75 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -10,12 +10,13 @@ CAN_BIND=${CAN_BIND:-1} # # If DEST is ended by a slash it complete this path with the basename of SRC. # -# usage: ynh_backup src [dest [is_big [arg]]] +# usage: ynh_backup src [dest [is_big [not_mandatory [arg]]]] # | arg: src - file or directory to bind or symlink or copy. it shouldn't be in # the backup dir. # | arg: dest - destination file or directory inside the # backup dir # | arg: is_big - 1 to indicate data are big (mail, video, image ...) +# | arg: not_mandatory - 1 to indicate that if the file is missing, the backup can ignore it. # | arg: arg - Deprecated arg # # example: @@ -46,6 +47,7 @@ ynh_backup() { local SRC_PATH="$1" local DEST_PATH="${2:-}" local IS_BIG="${3:-0}" + local NOT_MANDATORY="${4:-0}" BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0} # If backing up core only (used by ynh_backup_before_upgrade), @@ -60,15 +62,17 @@ ynh_backup() { # ============================================================================== # Be sure the source path is not empty [[ -e "${SRC_PATH}" ]] || { - echo "!!! Source path '${SRC_PATH}' does not exist !!!" >&2 - - # This is a temporary fix for fail2ban config files missing after the migration to stretch. - if echo "${SRC_PATH}" | grep --quiet "/etc/fail2ban" + if [ "$NOT_MANDATORY" == "0" ] then - touch "${SRC_PATH}" - echo "The missing file will be replaced by a dummy one for the backup !!!" >&2 + # This is a temporary fix for fail2ban config files missing after the migration to stretch. + if echo "${SRC_PATH}" | grep --quiet "/etc/fail2ban" + then + touch "${SRC_PATH}" + echo "The missing file will be replaced by a dummy one for the backup !!!" >&2 + else + return 1 else - return 1 + return 0 fi } @@ -176,12 +180,13 @@ with open(sys.argv[1], 'r') as backup_file: # Use the registered path in backup_list by ynh_backup to restore the file at # the good place. # -# usage: ynh_restore_file ORIGIN_PATH [ DEST_PATH ] +# usage: ynh_restore_file ORIGIN_PATH [ DEST_PATH [NOT_MANDATORY]] # | arg: ORIGIN_PATH - Path where was located the file or the directory before # to be backuped or relative path to $YNH_CWD where it is located in the backup archive # | arg: DEST_PATH - Path where restore the file or the dir, if unspecified, # the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in # the archive, the destination will be searched into backup.csv +# | arg: NOT_MANDATORY - 1 to indicate that if the file is missing, the restore process can ignore it. # # 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. @@ -201,10 +206,16 @@ ynh_restore_file () { local ARCHIVE_PATH="$YNH_CWD${ORIGIN_PATH}" # Default value for DEST_PATH = /$ORIGIN_PATH local DEST_PATH="${2:-$ORIGIN_PATH}" + local NOT_MANDATORY="${3:-0}" # If ARCHIVE_PATH doesn't exist, search for a corresponding path in CSV if [ ! -d "$ARCHIVE_PATH" ] && [ ! -f "$ARCHIVE_PATH" ] && [ ! -L "$ARCHIVE_PATH" ]; then - ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" + if [ "$NOT_MANDATORY" == "0" ] + then + ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" + else + return 0 + fi fi # Move the old directory if it already exists From db1b00e706a9c4f1ea16a95b6ca3c8184f831789 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 23:48:56 +0100 Subject: [PATCH 211/250] [enh] Use UTC naive format and manage it in moulinette --- src/yunohost/app.py | 6 ++---- src/yunohost/backup.py | 6 ++---- src/yunohost/log.py | 8 +++----- src/yunohost/monitor.py | 2 +- src/yunohost/service.py | 4 +--- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0664a24b2..79af7faf2 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -37,7 +37,6 @@ import pwd import grp from collections import OrderedDict import datetime -import pytz from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError @@ -83,9 +82,8 @@ def app_listlists(): appslist_list = _read_appslist_list() for app in appslist_list: - last_update = datetime.datetime.utcfromtimestamp( + appslist_list[app]["lastUpdate"] = datetime.datetime.utcfromtimestamp( appslist_list[app].get("lastUpdate")) - appslist_list[app]["lastUpdate"] = last_update.replace(tzinfo=pytz.utc) return appslist_list @@ -1762,7 +1760,7 @@ def _get_app_status(app_id, format_date=False): if not v: status[f] = '-' else: - status[f] = datetime.utcfromtimestamp(v).replace(tzinfo=pytz.utc) + status[f] = datetime.utcfromtimestamp(v) return status diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 492687eaf..528b2e24f 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -33,7 +33,6 @@ import shutil import subprocess import csv import tempfile -import pytz from datetime import datetime from glob import glob from collections import OrderedDict @@ -301,7 +300,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') + return time.strftime('%Y%m%d-%H%M%S', time.gmtime()) def _init_work_dir(self): """Initialize preparation directory @@ -2268,8 +2267,7 @@ def backup_info(name, with_details=False, human_readable=False): result = { 'path': archive_file, - 'created_at': datetime.utcfromtimestamp(info['created_at']) - .replace(tzinfo=pytz.utc), + 'created_at': datetime.utcfromtimestamp(info['created_at']), 'description': info['description'], 'size': size, } diff --git a/src/yunohost/log.py b/src/yunohost/log.py index c75300d3d..af19090c8 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -101,7 +101,7 @@ def log_list(category=[], limit=None): except ValueError: pass else: - entry["started_at"] = log_datetime.replace(tzinfo=pytz.utc) + entry["started_at"] = log_datetime result[category].append(entry) @@ -321,8 +321,7 @@ class OperationLogger(object): """ if self.started_at is None: - self.started_at = datetime.utcnow() - self.started_at = self.started_at.replace(tzinfo=pytz.utc) + self.started_at = datetime.now(tz=pytz.utc) self.flush() self._register_log() @@ -414,8 +413,7 @@ class OperationLogger(object): return if error is not None and not isinstance(error, basestring): error = str(error) - self.ended_at = datetime.utcnow() - self.ended_at = self.ended_at.replace(tzinfo=pytz.utc) + self.ended_at = datetime.now(tz=pytz.utc) self._error = error self._success = error is None if self.logger is not None: diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index cad467ed5..fc10a4fbc 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -283,7 +283,7 @@ def monitor_system(units=None, human_readable=False): elif u == 'process': result[u] = json.loads(glances.getProcessCount()) elif u == 'uptime': - result[u] = (str(datetime.utcnow() - datetime.utcfromtimestamp(psutil.boot_time())).split('.')[0]) + result[u] = (str(datetime.now() - datetime.fromtimestamp(psutil.boot_time())).split('.')[0]) elif u == 'infos': result[u] = json.loads(glances.getSystem()) else: diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 73383cb44..fd75d0235 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -31,7 +31,6 @@ import subprocess import errno import shutil import hashlib -import pytz from difflib import unified_diff from datetime import datetime @@ -276,7 +275,6 @@ def service_status(names=[]): } if "ActiveEnterTimestamp" in status: result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000) - result[name]['active_at'] = result[name]['active_at'].replace(tzinfo=pytz.utc) else: result[name]['active_at'] = "unknown" @@ -926,7 +924,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): """ if save: backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format( - system_conf.lstrip('/'), time.strftime("%Y%m%d.%H%M%S"))) + 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): From cafd0dd884d9a10ea761efdef7bb1a1938082092 Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 23:55:51 +0100 Subject: [PATCH 212/250] [enh] Remove pytz --- debian/control | 2 +- src/yunohost/log.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/debian/control b/debian/control index f0423c4a3..9f72bf11a 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Package: yunohost Architecture: all Depends: ${python:Depends}, ${misc:Depends} , moulinette (>= 2.7.1), ssowat (>= 2.7.1) - , python-psutil, python-requests, python-dnspython, python-openssl, python-tz + , python-psutil, python-requests, python-dnspython, python-openssl , python-apt, python-miniupnpc, python-dbus, python-jinja2 , glances , dnsutils, bind9utils, unzip, git, curl, cron, wget, jq diff --git a/src/yunohost/log.py b/src/yunohost/log.py index af19090c8..f11c587d1 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -28,7 +28,6 @@ import os import yaml import errno import collections -import pytz from datetime import datetime from logging import FileHandler, getLogger, Formatter @@ -184,9 +183,9 @@ def log_display(path, number=50, share=False): infos['metadata_path'] = md_path infos['metadata'] = metadata if 'started_at' in infos['metadata']: - infos['metadata']['started_at'] = infos['metadata']['started_at'].replace(tzinfo=pytz.utc) + infos['metadata']['started_at'] = infos['metadata']['started_at'] if 'ended_at' in infos['metadata']: - infos['metadata']['ended_at'] = infos['metadata']['ended_at'].replace(tzinfo=pytz.utc) + infos['metadata']['ended_at'] = infos['metadata']['ended_at'] if 'log_path' in metadata: log_path = metadata['log_path'] except yaml.YAMLError: @@ -321,7 +320,7 @@ class OperationLogger(object): """ if self.started_at is None: - self.started_at = datetime.now(tz=pytz.utc) + self.started_at = datetime.utcnow() self.flush() self._register_log() @@ -413,7 +412,7 @@ class OperationLogger(object): return if error is not None and not isinstance(error, basestring): error = str(error) - self.ended_at = datetime.now(tz=pytz.utc) + self.ended_at = datetime.utcnow() self._error = error self._success = error is None if self.logger is not None: From 7732136b7508a3056ae79a1745312f604098463a Mon Sep 17 00:00:00 2001 From: ljf Date: Sun, 9 Dec 2018 23:58:06 +0100 Subject: [PATCH 213/250] [enh] Remove unneeded code --- src/yunohost/log.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index f11c587d1..80a7a134f 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -182,10 +182,6 @@ def log_display(path, number=50, share=False): metadata = yaml.safe_load(md_file) infos['metadata_path'] = md_path infos['metadata'] = metadata - if 'started_at' in infos['metadata']: - infos['metadata']['started_at'] = infos['metadata']['started_at'] - if 'ended_at' in infos['metadata']: - infos['metadata']['ended_at'] = infos['metadata']['ended_at'] if 'log_path' in metadata: log_path = metadata['log_path'] except yaml.YAMLError: From 76be566607405618ae535e7c8884125403044cf7 Mon Sep 17 00:00:00 2001 From: ljf Date: Mon, 10 Dec 2018 01:19:14 +0100 Subject: [PATCH 214/250] [fix] Bad time convesion in a debug message --- 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 528b2e24f..33fbccb7f 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -860,7 +860,7 @@ class RestoreManager(): raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: logger.debug("restoring from backup '%s' created on %s", self.name, - time.ctime(self.info['created_at'])) + datetime.utcfromtimestamp(self.info['created_at'])) def _postinstall_if_needed(self): """ From 96a6e370a37b0c44d46df430dd740aac67cac2b4 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Dec 2018 02:53:27 +0100 Subject: [PATCH 215/250] [tests] Fix some issues with maindomain changing sometimes --- src/yunohost/tests/test_backuprestore.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index c60dcb517..6dfbff9b7 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -15,7 +15,7 @@ from yunohost.domain import _get_maindomain from moulinette.core import MoulinetteError # Get main domain -maindomain = _get_maindomain() +maindomain = "" # Instantiate LDAP Authenticator AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') @@ -24,6 +24,9 @@ auth = None def setup_function(function): + global maindomain + maindomain = _get_maindomain() + print "" global auth From d06d1cdee4b5cb62c1ec9906c972fab89b16e802 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Dec 2018 03:33:40 +0100 Subject: [PATCH 216/250] Explicit that we don't backup/restore apps --- src/yunohost/tests/test_backuprestore.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index 6dfbff9b7..c9d4c6b15 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -198,7 +198,7 @@ def add_archive_system_from_2p4(): def test_backup_only_ldap(): # Create the backup - backup_create(system=["conf_ldap"]) + backup_create(system=["conf_ldap"], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -215,7 +215,7 @@ def test_backup_system_part_that_does_not_exists(mocker): # Create the backup with pytest.raises(MoulinetteError): - backup_create(system=["yolol"]) + backup_create(system=["yolol"], apps=None) m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") m18n.n.assert_any_call('backup_nothings_done') @@ -227,7 +227,7 @@ def test_backup_system_part_that_does_not_exists(mocker): def test_backup_and_restore_all_sys(): # Create the backup - backup_create(system=[]) + backup_create(system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 1 @@ -244,7 +244,7 @@ def test_backup_and_restore_all_sys(): # Restore the backup backup_restore(auth, name=archives[0], force=True, - system=[]) + system=[], apps=None) # Check ssowat conf is back assert os.path.exists("/etc/ssowat/conf.json") @@ -258,7 +258,7 @@ def test_backup_and_restore_all_sys(): def test_restore_system_from_Ynh2p4(monkeypatch, mocker): # Backup current system - backup_create(system=[]) + backup_create(system=[], apps=None) archives = backup_list()["archives"] assert len(archives) == 2 @@ -266,11 +266,13 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker): try: backup_restore(auth, name=backup_list()["archives"][1], system=[], + apps=None, force=True) finally: # Restore system as it was backup_restore(auth, name=backup_list()["archives"][0], system=[], + apps=None, force=True) ############################################################################### @@ -369,7 +371,7 @@ def test_backup_app_with_no_restore_script(mocker): def test_backup_with_different_output_directory(): # Create the backup - backup_create(system=["conf_ssh"], + backup_create(system=["conf_ssh"], apps=None, output_directory="/opt/test_backup_output_directory", name="backup") @@ -386,7 +388,7 @@ def test_backup_with_different_output_directory(): @pytest.mark.clean_opt_dir def test_backup_with_no_compress(): # Create the backup - backup_create(system=["conf_nginx"], + backup_create(system=["conf_nginx"], apps=None, output_directory="/opt/test_backup_output_directory", no_compress=True, name="backup") From 4d4b14b82c8eb19d5dba375379fccaa28f7de5e6 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 10 Dec 2018 16:23:30 +0100 Subject: [PATCH 217/250] Change message about conf now being managed by service -> yunohost (#597) --- locales/en.json | 2 +- src/yunohost/service.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 78df35c33..819a28008 100644 --- a/locales/en.json +++ b/locales/en.json @@ -403,7 +403,7 @@ "service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", "service_conf_file_removed": "The configuration file '{conf}' has been removed", "service_conf_file_updated": "The configuration file '{conf}' has been updated", - "service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.", + "service_conf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost.", "service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'", "service_conf_updated": "The configuration has been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 9ab301933..47d53e0ac 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -508,8 +508,8 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, # we assume that it is safe to regen it, since the file is backuped # anyway (by default in _regen), as long as we warn the user # appropriately. - logger.info(m18n.n('service_conf_new_managed_file', - conf=system_path, service=service)) + logger.info(m18n.n('service_conf_now_managed_by_yunohost', + conf=system_path)) regenerated = _regen(system_path, pending_path) conf_status = 'new' elif force: From fe15e600fd2b0f2b413c19ab92a7c4edf0cb1898 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Dec 2018 02:02:11 +0000 Subject: [PATCH 218/250] Small UX improvement about link to log sharing in webadmin --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 819a28008..98ae9a170 100644 --- a/locales/en.json +++ b/locales/en.json @@ -211,7 +211,7 @@ "log_category_404": "The log category '{category}' does not exist", "log_link_to_log": "Full log of this operation: '{desc}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", - "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation", + "log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please provide the full log of this operation by clicking here", "log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please share the full log of this operation using the command 'yunohost log display {name} --share'", "log_category_404": "The log category '{category}' does not exist", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'", From c97582938760f43559f15383f0c483563fa09fbb Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Dec 2018 15:54:50 +0100 Subject: [PATCH 219/250] [fix] Missing 'fi' as found by M. Martin --- data/helpers.d/filesystem | 1 + 1 file changed, 1 insertion(+) diff --git a/data/helpers.d/filesystem b/data/helpers.d/filesystem index b5ae36a75..5b55b752f 100644 --- a/data/helpers.d/filesystem +++ b/data/helpers.d/filesystem @@ -71,6 +71,7 @@ ynh_backup() { echo "The missing file will be replaced by a dummy one for the backup !!!" >&2 else return 1 + fi else return 0 fi From 777108d84416fb3a4a90fb06c831ebbb763669c9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 11 Dec 2018 18:39:15 +0000 Subject: [PATCH 220/250] Ask confirmation before installing low-quality, experimental or third party apps --- data/actionsmap/yunohost.yml | 4 ++++ locales/en.json | 4 ++++ src/yunohost/app.py | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index ba5f1d505..79cc7cdaa 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -557,6 +557,10 @@ app: full: --no-remove-on-failure help: Debug option to avoid removing the app on a failed installation action: store_true + -f: + full: --force + help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party) + action: store_true ### app_remove() TODO: Write help remove: diff --git a/locales/en.json b/locales/en.json index 98ae9a170..8e4e18497 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,4 +1,5 @@ { + "aborting": "Aborting.", "action_invalid": "Invalid action '{action:s}'", "admin_password": "Administration password", "admin_password_change_failed": "Unable to change password", @@ -132,6 +133,9 @@ "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", + "confirm_app_install_warning": "Warning : this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway ? [{answers:s}] ", + "confirm_app_install_danger": "WARNING ! This application is still experimental (if not explicitly not working) and it is likely to break your system ! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", + "confirm_app_install_thirdparty": "WARNING ! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e26700c49..e9a4b67f1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -689,7 +689,7 @@ def app_upgrade(auth, app=[], url=None, file=None): @is_unit_operation() -def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False): +def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False, force=False): """ Install apps @@ -698,7 +698,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on label -- Custom name for the app args -- Serialize arguments for app installation no_remove_on_failure -- Debug option to avoid removing the app on a failed installation - + force -- Do not ask for confirmation when installing experimental / low-quality apps """ from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger @@ -718,9 +718,38 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on }, } - if app in app_list(raw=True) or ('@' in app) or ('http://' in app) or ('https://' in app): + 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': + return + + answer = msignals.prompt(m18n.n('confirm_app_install_'+confirm, + answers='Y/N')) + if answer.upper() != "Y": + raise MoulinetteError(errno.EINVAL, m18n.n("aborting")) + + + raw_app_list = app_list(raw=True) + if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app): + if app in raw_app_list: + state = raw_app_list[app].get("state", "notworking") + level = raw_app_list[app].get("level", None) + confirm = "danger" + if state in ["working", "validated"]: + if isinstance(level, int) and level >= 3: + confirm = None + elif isinstance(level, int) and level > 0: + confirm = "warning" + else: + confirm = "thirdparty" + + confirm_install(confirm) + manifest, extracted_app_folder = _fetch_app_from_git(app) elif os.path.exists(app): + confirm_install("thirdparty") manifest, extracted_app_folder = _extract_app_from_file(app) else: raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) From d7f25bec2ab05faa6f69a9a37f75b570eb7430f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Tille?= Date: Tue, 11 Dec 2018 20:18:23 +0100 Subject: [PATCH 221/250] Remove hack for authenticator It's solved in the moulinette with #185 --- src/yunohost/tests/test_changeurl.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index 13f8aca5d..eb10cd604 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -19,10 +19,8 @@ maindomain = _get_maindomain() def setup_function(function): - # For some reason the nginx reload can take some time to propagate - time.sleep(1) - global auth - auth = init_authenticator(AUTH_IDENTIFIER, AUTH_PARAMETERS) + pass + def teardown_function(function): app_remove(auth, "change_url_app") From 3749bc9790e16db46956e938761b4b9715d124a5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 18:31:47 +0000 Subject: [PATCH 222/250] Missing locale key for new setting about DSA hostkey --- locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/locales/en.json b/locales/en.json index e54e3c168..f1db76e07 100644 --- a/locales/en.json +++ b/locales/en.json @@ -197,6 +197,7 @@ "global_settings_setting_security_password_admin_strength": "Admin password strength", "global_settings_setting_security_password_user_strength": "User password strength", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", + "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).", From 0179bf2005da83483dc15426f72fa0f2a182a60b Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 18:32:55 +0000 Subject: [PATCH 223/250] Fix misc stuff about utcfromtimestamp usage (tests were broken) --- src/yunohost/app.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 79af7faf2..296f21361 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -36,7 +36,7 @@ import glob import pwd import grp from collections import OrderedDict -import datetime +from datetime import datetime from moulinette import msignals, m18n, msettings from moulinette.core import MoulinetteError @@ -81,9 +81,11 @@ def app_listlists(): # Get the list appslist_list = _read_appslist_list() - for app in appslist_list: - appslist_list[app]["lastUpdate"] = datetime.datetime.utcfromtimestamp( - appslist_list[app].get("lastUpdate")) + # Convert 'lastUpdate' timestamp to datetime + for name, infos in appslist_list.items(): + if infos["lastUpdate"] is None: + infos["lastUpdate"] = 0 + infos["lastUpdate"] = datetime.utcfromtimestamp(infos["lastUpdate"]) return appslist_list From f2e184d8b29ae2e3cdbef1f9d69becaf983db647 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Fri, 31 Aug 2018 18:51:16 +0200 Subject: [PATCH 224/250] moulinette MoulinetteError oneline --- src/yunohost/app.py | 36 +++++++++---------- src/yunohost/backup.py | 14 ++++---- src/yunohost/certificate.py | 2 +- .../0002_migrate_to_tsig_sha256.py | 2 +- .../0003_migrate_to_stretch.py | 10 +++--- .../0005_postgresql_9p4_to_9p6.py | 4 +-- src/yunohost/domain.py | 12 +++---- src/yunohost/dyndns.py | 6 ++-- src/yunohost/firewall.py | 8 ++--- src/yunohost/hook.py | 4 +-- src/yunohost/monitor.py | 18 +++++----- src/yunohost/service.py | 12 +++---- src/yunohost/ssh.py | 4 +-- src/yunohost/tools.py | 14 ++++---- src/yunohost/user.py | 14 ++++---- 15 files changed, 80 insertions(+), 80 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 296f21361..4637b36e3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -212,7 +212,7 @@ def app_removelist(operation_logger, name): # Make sure we know this appslist if name not in appslists.keys(): - raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name)) + raise MoulinetteError('appslist_unknown', appslist=name) operation_logger.start() @@ -459,7 +459,7 @@ def app_change_url(operation_logger, auth, app, domain, path): m18n.n('app_not_installed', app=app)) if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): - raise MoulinetteError(errno.EINVAL, m18n.n("app_change_no_change_url_script", app_name=app)) + raise MoulinetteError("app_change_no_change_url_script", app_name=app) old_domain = app_setting(app, "domain") old_path = app_setting(app, "path") @@ -471,7 +471,7 @@ def app_change_url(operation_logger, auth, app, domain, path): path = normalize_url_path(path) if (domain, path) == (old_domain, old_path): - raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path)) + raise MoulinetteError("app_change_url_identical_domains", domain=domain, path=path) # WARNING / FIXME : checkurl will modify the settings # (this is a non intuitive behavior that should be changed) @@ -547,7 +547,7 @@ def app_change_url(operation_logger, auth, app, domain, path): stderr=subprocess.STDOUT, shell=True).rstrip() - raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors)) + raise MoulinetteError("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors) logger.success(m18n.n("app_change_url_success", app=app, domain=domain, path=path)) @@ -573,7 +573,7 @@ def app_upgrade(auth, app=[], url=None, file=None): try: app_list() except MoulinetteError: - raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) + raise MoulinetteError('app_no_upgrade') upgraded_apps = [] @@ -684,7 +684,7 @@ def app_upgrade(auth, app=[], url=None, file=None): operation_logger.success() if not upgraded_apps: - raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) + raise MoulinetteError('app_no_upgrade') app_ssowatconf(auth) @@ -730,12 +730,12 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on elif os.path.exists(app): manifest, extracted_app_folder = _extract_app_from_file(app) else: - raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) + raise MoulinetteError('app_unknown') status['remote'] = manifest.get('remote', {}) # Check ID if 'id' not in manifest or '__' in manifest['id']: - raise MoulinetteError(errno.EINVAL, m18n.n('app_id_invalid')) + raise MoulinetteError('app_id_invalid') app_id = manifest['id'] @@ -1139,7 +1139,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): domain = app_domain operation_logger.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + raise MoulinetteError('domain_unknown') operation_logger.start() if '/' in app_map(raw=True)[domain]: @@ -1262,7 +1262,7 @@ def app_register_url(auth, app, domain, path): app_label=app_label, )) - raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps))) + raise MoulinetteError('app_location_unavailable', apps="\n".join(apps)) app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path) @@ -1300,7 +1300,7 @@ def app_checkurl(auth, url, app=None): apps_map = app_map(raw=True) if domain not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + raise MoulinetteError('domain_unknown') if domain in apps_map: # Loop through apps @@ -1349,10 +1349,10 @@ def app_initdb(user, password=None, db=None, sql=None): mysql_root_pwd = open('/etc/yunohost/mysql').read().rstrip() mysql_command = 'mysql -u root -p%s -e "CREATE DATABASE %s ; GRANT ALL PRIVILEGES ON %s.* TO \'%s\'@localhost IDENTIFIED BY \'%s\';"' % (mysql_root_pwd, db, db, user, password) if os.system(mysql_command) != 0: - raise MoulinetteError(errno.EIO, m18n.n('mysql_db_creation_failed')) + raise MoulinetteError('mysql_db_creation_failed') if sql is not None: if os.system('mysql -u %s -p%s %s < %s' % (user, password, db, sql)) != 0: - raise MoulinetteError(errno.EIO, m18n.n('mysql_db_init_failed')) + raise MoulinetteError('mysql_db_init_failed') if return_pwd: return password @@ -1738,7 +1738,7 @@ def _get_app_status(app_id, format_date=False): """ app_setting_path = APPS_SETTING_PATH + app_id if not os.path.isdir(app_setting_path): - raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) + raise MoulinetteError('app_unknown') status = {} try: @@ -1803,7 +1803,7 @@ def _extract_app_from_file(path, remove=False): extract_result = 1 if extract_result != 0: - raise MoulinetteError(errno.EINVAL, m18n.n('app_extraction_failed')) + raise MoulinetteError('app_extraction_failed') try: extracted_app_folder = APP_TMP_FOLDER @@ -1814,7 +1814,7 @@ def _extract_app_from_file(path, remove=False): manifest = json.loads(str(json_manifest.read())) manifest['lastUpdate'] = int(time.time()) except IOError: - raise MoulinetteError(errno.EIO, m18n.n('app_install_files_invalid')) + raise MoulinetteError('app_install_files_invalid') except ValueError as e: raise MoulinetteError(errno.EINVAL, m18n.n('app_manifest_invalid', error=e.strerror)) @@ -1933,7 +1933,7 @@ def _fetch_app_from_git(app): app_info['manifest']['lastUpdate'] = app_info['lastUpdate'] manifest = app_info['manifest'] else: - raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) + raise MoulinetteError('app_unknown') if 'git' not in app_info: raise MoulinetteError(errno.EINVAL, @@ -2312,7 +2312,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): app_label=app_label, )) - raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps))) + raise MoulinetteError('app_location_unavailable', apps="\n".join(apps)) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 33fbccb7f..d8197fed4 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -507,7 +507,7 @@ class BackupManager(): if not successfull_apps and not successfull_system: filesystem.rm(self.work_dir, True, True) - raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) + raise MoulinetteError('backup_nothings_done') # Add unlisted files from backup tmp dir self._add_to_list_to_backup('backup.csv') @@ -857,7 +857,7 @@ class RestoreManager(): self.info["system"] = self.info["hooks"] except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) - raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) + raise MoulinetteError('backup_invalid_archive') else: logger.debug("restoring from backup '%s' created on %s", self.name, datetime.utcfromtimestamp(self.info['created_at'])) @@ -1442,7 +1442,7 @@ class BackupMethod(object): @property def method_name(self): """Return the string name of a BackupMethod (eg "tar" or "copy")""" - raise MoulinetteError(errno.EINVAL, m18n.n('backup_abstract_method')) + raise MoulinetteError('backup_abstract_method') @property def name(self): @@ -2132,7 +2132,7 @@ def backup_restore(auth, name, system=[], apps=[], force=False): if i == 'y' or i == 'Y': force = True if not force: - raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed')) + raise MoulinetteError('restore_failed') # TODO Partial app restore could not work if ldap is not restored before # TODO repair mysql if broken and it's a complete restore @@ -2159,7 +2159,7 @@ def backup_restore(auth, name, system=[], apps=[], force=False): if restore_manager.success: logger.success(m18n.n('restore_complete')) else: - raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) + raise MoulinetteError('restore_nothings_done') return restore_manager.targets.results @@ -2240,7 +2240,7 @@ def backup_info(name, with_details=False, human_readable=False): except KeyError: logger.debug("unable to retrieve '%s' inside the archive", info_file, exc_info=1) - raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) + raise MoulinetteError('backup_invalid_archive') else: shutil.move(os.path.join(info_dir, 'info.json'), info_file) finally: @@ -2253,7 +2253,7 @@ 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 MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) + raise MoulinetteError('backup_invalid_archive') # Retrieve backup size size = info.get('size', 0) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5c3bf8c9e..59b7a72c3 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -581,7 +581,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text except requests.exceptions.Timeout as e: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert')) + raise MoulinetteError('certmanager_couldnt_fetch_intermediate_cert') # Now save the key and signed certificate logger.debug("Saving the key and signed certificate...") diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 5cbc4494f..aad6771be 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -52,7 +52,7 @@ class MyMigration(Migration): 'public_key_sha512': base64.b64encode(public_key_sha512), }, timeout=30) except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + raise MoulinetteError('no_internet_connection') if r.status_code != 201: try: diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 26f91ae0b..474a26db5 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -30,7 +30,7 @@ class MyMigration(Migration): def backward(self): - raise MoulinetteError(m18n.n("migration_0003_backward_impossible")) + raise MoulinetteError("migration_0003_backward_impossible") def migrate(self): @@ -57,7 +57,7 @@ class MyMigration(Migration): self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") if self.debian_major_version() == 8: - raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)) + raise MoulinetteError("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile) # Specific upgrade for fail2ban... logger.info(m18n.n("migration_0003_fail2ban_upgrade")) @@ -107,11 +107,11 @@ class MyMigration(Migration): # would still be in 2.x... if not self.debian_major_version() == 8 \ and not self.yunohost_major_version() == 2: - raise MoulinetteError(m18n.n("migration_0003_not_jessie")) + raise MoulinetteError("migration_0003_not_jessie") # Have > 1 Go free space on /var/ ? if free_space_in_directory("/var/") / (1024**3) < 1.0: - raise MoulinetteError(m18n.n("migration_0003_not_enough_free_space")) + raise MoulinetteError("migration_0003_not_enough_free_space") # Check system is up to date # (but we don't if 'stretch' is already in the sources.list ... @@ -120,7 +120,7 @@ class MyMigration(Migration): self.apt_update() apt_list_upgradable = check_output("apt list --upgradable -a") if "upgradable" in apt_list_upgradable: - raise MoulinetteError(m18n.n("migration_0003_system_not_fully_up_to_date")) + raise MoulinetteError("migration_0003_system_not_fully_up_to_date") @property def disclaimer(self): diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index 871edcd19..f03a93ef9 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -20,10 +20,10 @@ class MyMigration(Migration): return if not self.package_is_installed("postgresql-9.6"): - raise MoulinetteError(m18n.n("migration_0005_postgresql_96_not_installed")) + raise MoulinetteError("migration_0005_postgresql_96_not_installed") if not space_used_by_directory("/var/lib/postgresql/9.4") > free_space_in_directory("/var/lib/postgresql"): - raise MoulinetteError(m18n.n("migration_0005_not_enough_space", path="/var/lib/postgresql/")) + raise MoulinetteError("migration_0005_not_enough_space", path="/var/lib/postgresql/") subprocess.check_call("service postgresql stop", shell=True) subprocess.check_call("pg_dropcluster --stop 9.6 main", shell=True) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 7b387618a..21a7992ae 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -78,7 +78,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False): try: auth.validate_uniqueness({'virtualdomain': domain}) except MoulinetteError: - raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) + raise MoulinetteError('domain_exists') operation_logger.start() @@ -110,7 +110,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False): } if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): - raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed')) + raise MoulinetteError('domain_creation_failed') # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): @@ -147,11 +147,11 @@ def domain_remove(operation_logger, auth, domain, force=False): from yunohost.app import app_ssowatconf if not force and domain not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + raise MoulinetteError('domain_unknown') # Check domain is not the main domain if domain == _get_maindomain(): - raise MoulinetteError(errno.EINVAL, m18n.n('domain_cannot_remove_main')) + raise MoulinetteError('domain_cannot_remove_main') # Check if apps are installed on the domain for app in os.listdir('/etc/yunohost/apps/'): @@ -169,7 +169,7 @@ def domain_remove(operation_logger, auth, domain, force=False): if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: - raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed')) + raise MoulinetteError('domain_deletion_failed') service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) @@ -246,7 +246,7 @@ def _get_conflicting_apps(auth, domain, path): # Abort if domain is unknown if domain not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + raise MoulinetteError('domain_unknown') # This import cannot be put on top of file because it would create a # recursive import... diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index dd652119f..8bf2d254f 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -161,7 +161,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom try: r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) except requests.ConnectionError: - raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + raise MoulinetteError('no_internet_connection') if r.status_code != 201: try: error = json.loads(r.text)['error'] @@ -202,7 +202,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) if not keys: - raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) + raise MoulinetteError('dyndns_key_not_found') key = keys[0] @@ -329,7 +329,7 @@ def dyndns_removecron(): try: os.remove("/etc/cron.d/yunohost-dyndns") except: - raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed')) + raise MoulinetteError('dyndns_cron_remove_failed') logger.success(m18n.n('dyndns_cron_removed')) diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 0e1cf6e76..878083bc7 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -268,7 +268,7 @@ def firewall_reload(skip_upnp=False): reloaded = True if not reloaded: - raise MoulinetteError(errno.ESRCH, m18n.n('firewall_reload_failed')) + raise MoulinetteError('firewall_reload_failed') hook_callback('post_iptable_rules', args=[upnp, os.path.exists("/proc/net/if_inet6")]) @@ -338,7 +338,7 @@ def firewall_upnp(action='status', no_refresh=False): if action == 'status': no_refresh = True else: - raise MoulinetteError(errno.EINVAL, m18n.n('action_invalid', action=action)) + raise MoulinetteError('action_invalid', action=action) # Refresh port mapping using UPnP if not no_refresh: @@ -407,7 +407,7 @@ def firewall_upnp(action='status', no_refresh=False): firewall_reload(skip_upnp=True) if action == 'enable' and not enabled: - raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) + raise MoulinetteError('upnp_port_open_failed') return {'enabled': enabled} @@ -419,7 +419,7 @@ def firewall_stop(): """ if os.system("iptables -w -P INPUT ACCEPT") != 0: - raise MoulinetteError(errno.ESRCH, m18n.n('iptables_unavailable')) + raise MoulinetteError('iptables_unavailable') os.system("iptables -w -F") os.system("iptables -w -X") diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 46c25d50d..b7457878e 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -112,7 +112,7 @@ def hook_info(action, name): }) if not hooks: - raise MoulinetteError(errno.EINVAL, m18n.n('hook_name_unknown', name=name)) + raise MoulinetteError('hook_name_unknown', name=name) return { 'action': action, 'name': name, @@ -174,7 +174,7 @@ def hook_list(action, list_by='name', show_info=False): # Add only the name d.add(name) else: - raise MoulinetteError(errno.EINVAL, m18n.n('hook_list_by_invalid')) + raise MoulinetteError('hook_list_by_invalid') def _append_folder(d, folder): # Iterate over and add hook from a folder diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index fc10a4fbc..b01e36238 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -83,7 +83,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): result_dname = dn if len(devices) == 0: if mountpoint is not None: - raise MoulinetteError(errno.ENODEV, m18n.n('mountpoint_unknown')) + raise MoulinetteError('mountpoint_unknown') return result # Retrieve monitoring for unit(s) @@ -141,7 +141,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): for dname in devices_names: _set(dname, 'not-available') else: - raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) + raise MoulinetteError('unit_unknown', unit=u) if result_dname is not None: return result[result_dname] @@ -237,7 +237,7 @@ def monitor_network(units=None, human_readable=False): 'gateway': gateway, } else: - raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) + raise MoulinetteError('unit_unknown', unit=u) if len(units) == 1: return result[units[0]] @@ -287,7 +287,7 @@ def monitor_system(units=None, human_readable=False): elif u == 'infos': result[u] = json.loads(glances.getSystem()) else: - raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) + raise MoulinetteError('unit_unknown', unit=u) if len(units) == 1 and type(result[units[0]]) is not str: return result[units[0]] @@ -303,7 +303,7 @@ def monitor_update_stats(period): """ if period not in ['day', 'week', 'month']: - raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid')) + raise MoulinetteError('monitor_period_invalid') stats = _retrieve_stats(period) if not stats: @@ -321,7 +321,7 @@ def monitor_update_stats(period): else: monitor = _monitor_all(p, 0) if not monitor: - raise MoulinetteError(errno.ENODATA, m18n.n('monitor_stats_no_update')) + raise MoulinetteError('monitor_stats_no_update') stats['timestamp'].append(time.time()) @@ -386,7 +386,7 @@ def monitor_show_stats(period, date=None): """ if period not in ['day', 'week', 'month']: - raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid')) + raise MoulinetteError('monitor_period_invalid') result = _retrieve_stats(period, date) if result is False: @@ -470,8 +470,8 @@ def _get_glances_api(): from yunohost.service import service_status if service_status('glances')['status'] != 'running': - raise MoulinetteError(errno.EPERM, m18n.n('monitor_not_enabled')) - raise MoulinetteError(errno.EIO, m18n.n('monitor_glances_con_failed')) + raise MoulinetteError('monitor_not_enabled') + raise MoulinetteError('monitor_glances_con_failed') def _extract_inet(string, skip_netmask=False, skip_loopback=True): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 83c36d0f8..2c85b7047 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -85,7 +85,7 @@ def service_add(name, status=None, log=None, runlevel=None, need_lock=False, des _save_services(services) except: # we'll get a logger.warning with more details in _save_services - raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', service=name)) + raise MoulinetteError('service_add_failed', service=name) logger.success(m18n.n('service_added', service=name)) @@ -103,13 +103,13 @@ def service_remove(name): try: del services[name] except KeyError: - raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name)) + raise MoulinetteError('service_unknown', service=name) try: _save_services(services) except: # we'll get a logger.warning with more details in _save_services - raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', service=name)) + raise MoulinetteError('service_remove_failed', service=name) logger.success(m18n.n('service_removed', service=name)) @@ -320,10 +320,10 @@ def service_log(name, number=50): services = _get_services() if name not in services.keys(): - raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name)) + raise MoulinetteError('service_unknown', service=name) if 'log' not in services[name]: - raise MoulinetteError(errno.EPERM, m18n.n('service_no_log', service=name)) + raise MoulinetteError('service_no_log', service=name) log_list = services[name]['log'] @@ -609,7 +609,7 @@ def _run_service_command(action, service): """ services = _get_services() if service not in services.keys(): - raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service)) + raise MoulinetteError('service_unknown', service=service) possible_actions = ['start', 'stop', 'restart', 'reload', 'enable', 'disable'] if action not in possible_actions: diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index 5ddebfc2f..41ac64293 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -23,7 +23,7 @@ def user_ssh_allow(auth, username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(auth, username): - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + raise MoulinetteError('user_unknown', user=username) auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'}) @@ -42,7 +42,7 @@ def user_ssh_disallow(auth, username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(auth, username): - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + raise MoulinetteError('user_unknown', user=username) auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'}) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 397e51eb2..5f82c467b 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -112,7 +112,7 @@ def tools_ldapinit(): pwd.getpwnam("admin") except KeyError: logger.error(m18n.n('ldap_init_failed_to_create_admin')) - raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed')) + raise MoulinetteError('installation_failed') logger.success(m18n.n('ldap_initialized')) return auth @@ -176,7 +176,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None): # Check domain exists if new_domain not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) + raise MoulinetteError('domain_unknown') operation_logger.related_to.append(('domain', new_domain)) operation_logger.start() @@ -199,7 +199,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None): _set_maindomain(new_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) - raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed')) + raise MoulinetteError('maindomain_change_failed') _set_hostname(new_domain) @@ -248,7 +248,7 @@ def _set_hostname(hostname, pretty_hostname=None): if p.returncode != 0: logger.warning(command) logger.warning(out) - raise MoulinetteError(errno.EIO, m18n.n('domain_hostname_failed')) + raise MoulinetteError('domain_hostname_failed') else: logger.debug(out) @@ -483,7 +483,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): # Update APT cache logger.debug(m18n.n('updating_apt_cache')) if not cache.update(): - raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) + raise MoulinetteError('update_cache_failed') cache.open(None) cache.upgrade(True) @@ -807,7 +807,7 @@ def tools_migrations_list(pending=False, done=False): # Check for option conflict if pending and done: - raise MoulinetteError(errno.EINVAL, m18n.n("migrations_list_conflict_pending_done")) + raise MoulinetteError("migrations_list_conflict_pending_done") # Get all migrations migrations = _get_migrations_list() @@ -864,7 +864,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai # validate input, target must be "0" or a valid number elif target != 0 and target not in all_migration_numbers: - raise MoulinetteError(errno.EINVAL, m18n.n('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers)))) + raise MoulinetteError('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers))) logger.debug(m18n.n('migrations_current_target', target)) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index bb39dd58e..d074c81c1 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -130,7 +130,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # 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 MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) + raise MoulinetteError('system_username_exists') main_domain = _get_maindomain() aliases = [ @@ -226,7 +226,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas return {'fullname': fullname, 'username': username, 'mail': mail} - raise MoulinetteError(169, m18n.n('user_creation_failed')) + raise MoulinetteError('user_creation_failed') @is_unit_operation([('username', 'user')]) @@ -257,7 +257,7 @@ def user_delete(operation_logger, auth, username, purge=False): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: - raise MoulinetteError(169, m18n.n('user_deletion_failed')) + raise MoulinetteError('user_deletion_failed') app_ssowatconf(auth) @@ -296,7 +296,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, # Populate user informations result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + raise MoulinetteError('user_unknown', user=username) user = result[0] # Get modifications from arguments @@ -389,7 +389,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, app_ssowatconf(auth) return user_info(auth, username) else: - raise MoulinetteError(169, m18n.n('user_update_failed')) + raise MoulinetteError('user_update_failed') def user_info(auth, username): @@ -414,7 +414,7 @@ def user_info(auth, username): if result: user = result[0] else: - raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) + raise MoulinetteError('user_unknown', user=username) result_dict = { 'username': user['uid'][0], @@ -470,7 +470,7 @@ def user_info(auth, username): if result: return result_dict else: - raise MoulinetteError(167, m18n.n('user_info_failed')) + raise MoulinetteError('user_info_failed') # # SSH subcategory From 4bc3427e3efecf7f788f1c90f457696d898ab2ce Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Wed, 3 Oct 2018 15:32:04 +0200 Subject: [PATCH 225/250] moulinette MoulinetteError multilignes" sed -i "N; s@MoulinetteError(.*m18n\.n(\n* *\(.*\)))@MoulinetteError(\1)@g" --- src/yunohost/app.py | 81 +++++++------------ src/yunohost/backup.py | 70 ++++++---------- src/yunohost/certificate.py | 51 ++++-------- .../0002_migrate_to_tsig_sha256.py | 4 +- src/yunohost/domain.py | 3 +- src/yunohost/dyndns.py | 12 +-- src/yunohost/hook.py | 6 +- src/yunohost/log.py | 3 +- src/yunohost/monitor.py | 6 +- src/yunohost/settings.py | 6 +- src/yunohost/tools.py | 7 +- src/yunohost/user.py | 12 +-- 12 files changed, 89 insertions(+), 172 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 4637b36e3..92aae5336 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -125,14 +125,12 @@ def app_fetchlist(url=None, name=None): appslists_to_be_fetched = [name] operation_logger.success() else: - raise MoulinetteError(errno.EINVAL, - m18n.n('custom_appslist_name_required')) + raise MoulinetteError('custom_appslist_name_required') # If a name is given, look for an appslist with that name and fetch it elif name is not None: if name not in appslists.keys(): - raise MoulinetteError(errno.EINVAL, - m18n.n('appslist_unknown', appslist=name)) + raise MoulinetteError('appslist_unknown', appslist=name) else: appslists_to_be_fetched = [name] @@ -343,8 +341,7 @@ def app_info(app, show_status=False, raw=False): """ if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app) app_setting_path = APPS_SETTING_PATH + app @@ -402,8 +399,7 @@ def app_map(app=None, raw=False, user=None): if app is not None: if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app) apps = [app, ] else: apps = os.listdir(APPS_SETTING_PATH) @@ -455,8 +451,7 @@ def app_change_url(operation_logger, auth, app, domain, path): installed = _is_installed(app) if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app) if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): raise MoulinetteError("app_change_no_change_url_script", app_name=app) @@ -593,8 +588,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name)) installed = _is_installed(app_instance_name) if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app_instance_name)) + raise MoulinetteError('app_not_installed', app=app_instance_name) if app_instance_name in upgraded_apps: continue @@ -746,8 +740,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on 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 MoulinetteError(errno.EEXIST, - m18n.n('app_already_installed', app=app_id)) + raise MoulinetteError('app_already_installed', app=app_id) # Change app_id to the forked app id app_instance_name = app_id + '__' + str(instance_number) @@ -888,8 +881,7 @@ def app_remove(operation_logger, auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app) operation_logger.start() @@ -1152,8 +1144,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) + raise MoulinetteError('ssowat_persistent_conf_read_error', error=e.strerror) except IOError: ssowat_conf = {} @@ -1166,8 +1157,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError(errno.EPERM, - m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + raise MoulinetteError('ssowat_persistent_conf_write_error', error=e.strerror) os.system('chmod 644 /etc/ssowat/conf.json.persistent') @@ -1219,8 +1209,7 @@ def app_checkport(port): if tools_port_available(port): logger.success(m18n.n('port_available', port=int(port))) else: - raise MoulinetteError(errno.EINVAL, - m18n.n('port_unavailable', port=int(port))) + raise MoulinetteError('port_unavailable', port=int(port)) def app_register_url(auth, app, domain, path): @@ -1247,8 +1236,7 @@ def app_register_url(auth, app, domain, path): if installed: settings = _get_app_settings(app) if "path" in settings.keys() and "domain" in settings.keys(): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_already_installed_cant_change_url')) + raise MoulinetteError('app_already_installed_cant_change_url') # Check the url is available conflicts = _get_conflicting_apps(auth, domain, path) @@ -1458,8 +1446,7 @@ def app_ssowatconf(auth): def app_change_label(auth, app, new_label): installed = _is_installed(app) if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app) app_setting(app, "label", value=new_label) @@ -1640,8 +1627,7 @@ def app_config_apply(app, args): installed = _is_installed(app) if not installed: - raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app=app)) + raise MoulinetteError('app_not_installed', app=app_id) config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') @@ -1699,8 +1685,7 @@ def _get_app_settings(app_id): """ if not _is_installed(app_id): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app=app_id)) + raise MoulinetteError('app_not_installed', app=app_id) try: with open(os.path.join( APPS_SETTING_PATH, app_id, 'settings.yml')) as f: @@ -1816,8 +1801,7 @@ def _extract_app_from_file(path, remove=False): except IOError: raise MoulinetteError('app_install_files_invalid') except ValueError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_manifest_invalid', error=e.strerror)) + raise MoulinetteError('app_manifest_invalid', error=e.strerror) logger.debug(m18n.n('done')) @@ -1885,8 +1869,7 @@ def _fetch_app_from_git(app): 'wget', '-qO', app_tmp_archive, tarball_url]) except subprocess.CalledProcessError: logger.exception('unable to download %s', tarball_url) - raise MoulinetteError(errno.EIO, - m18n.n('app_sources_fetch_failed')) + raise MoulinetteError('app_sources_fetch_failed') else: manifest, extracted_app_folder = _extract_app_from_file( app_tmp_archive, remove=True) @@ -1909,11 +1892,9 @@ def _fetch_app_from_git(app): with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: - raise MoulinetteError(errno.EIO, - m18n.n('app_sources_fetch_failed')) + raise MoulinetteError('app_sources_fetch_failed') except ValueError as e: - raise MoulinetteError(errno.EIO, - m18n.n('app_manifest_invalid', error=e.strerror)) + raise MoulinetteError('app_manifest_invalid', error=e.strerror) else: logger.debug(m18n.n('done')) @@ -1936,8 +1917,7 @@ def _fetch_app_from_git(app): raise MoulinetteError('app_unknown') if 'git' not in app_info: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_unsupported_remote_type')) + raise MoulinetteError('app_unsupported_remote_type') url = app_info['git']['url'] if 'github.com' in url: @@ -1949,8 +1929,7 @@ def _fetch_app_from_git(app): 'wget', '-qO', app_tmp_archive, tarball_url]) except subprocess.CalledProcessError: logger.exception('unable to download %s', tarball_url) - raise MoulinetteError(errno.EIO, - m18n.n('app_sources_fetch_failed')) + raise MoulinetteError('app_sources_fetch_failed') else: manifest, extracted_app_folder = _extract_app_from_file( app_tmp_archive, remove=True) @@ -1966,11 +1945,9 @@ def _fetch_app_from_git(app): with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: - raise MoulinetteError(errno.EIO, - m18n.n('app_sources_fetch_failed')) + raise MoulinetteError('app_sources_fetch_failed') except ValueError as e: - raise MoulinetteError(errno.EIO, - m18n.n('app_manifest_invalid', error=e.strerror)) + raise MoulinetteError('app_manifest_invalid', error=e.strerror) else: logger.debug(m18n.n('done')) @@ -2237,8 +2214,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): # Validate argument value if (arg_value is None or arg_value == '') \ and not arg.get('optional', False): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_required', name=arg_name)) + raise MoulinetteError('app_argument_required', name=arg_name) elif arg_value is None: args_dict[arg_name] = '' continue @@ -2462,8 +2438,7 @@ def _read_appslist_list(): try: appslists = json.loads(appslists_json) except ValueError: - raise MoulinetteError(errno.EBADR, - m18n.n('appslist_corrupted_json', filename=APPSLISTS_JSON)) + raise MoulinetteError('appslist_corrupted_json', filename=APPSLISTS_JSON) return appslists @@ -2493,15 +2468,13 @@ def _register_new_appslist(url, name): # Check if name conflicts with an existing list if name in appslist_list: - raise MoulinetteError(errno.EEXIST, - m18n.n('appslist_name_already_tracked', name=name)) + raise MoulinetteError('appslist_name_already_tracked', name=name) # Check if url conflicts with an existing list known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()] if url in known_appslist_urls: - raise MoulinetteError(errno.EEXIST, - m18n.n('appslist_url_already_tracked', url=url)) + raise MoulinetteError('appslist_url_already_tracked', url=url) logger.debug("Registering new appslist %s at %s" % (name, url)) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index d8197fed4..31acca188 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -327,8 +327,7 @@ class BackupManager(): logger.debug("temporary directory for backup '%s' already exists", self.work_dir) # FIXME May be we should clean the workdir here - raise MoulinetteError( - errno.EIO, m18n.n('backup_output_directory_not_empty')) + raise MoulinetteError('backup_output_directory_not_empty') ########################################################################### # Backup target management # @@ -880,8 +879,7 @@ class RestoreManager(): logger.debug("unable to retrieve current_host from the backup", exc_info=1) # FIXME include the current_host by default ? - raise MoulinetteError(errno.EIO, - m18n.n('backup_invalid_archive')) + raise MoulinetteError('backup_invalid_archive') logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) @@ -1010,8 +1008,7 @@ class RestoreManager(): subprocess.call(['rmdir', self.work_dir]) logger.debug("Unmount dir: {}".format(self.work_dir)) else: - raise MoulinetteError(errno.EIO, - m18n.n('restore_removing_tmp_dir_failed')) + raise MoulinetteError('restore_removing_tmp_dir_failed') elif os.path.isdir(self.work_dir): logger.debug("temporary restore directory '%s' already exists", self.work_dir) @@ -1019,8 +1016,7 @@ class RestoreManager(): if ret == 0: logger.debug("Delete dir: {}".format(self.work_dir)) else: - raise MoulinetteError(errno.EIO, - m18n.n('restore_removing_tmp_dir_failed')) + raise MoulinetteError('restore_removing_tmp_dir_failed') filesystem.mkdir(self.work_dir, parents=True) @@ -1524,8 +1520,7 @@ class BackupMethod(object): """ if self.need_mount(): if self._recursive_umount(self.work_dir) > 0: - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_cleaning_failed')) + raise MoulinetteError('backup_cleaning_failed') if self.manager.is_tmp_work_dir: filesystem.rm(self.work_dir, True, True) @@ -1567,8 +1562,7 @@ class BackupMethod(object): if free_space < backup_size: logger.debug('Not enough space at %s (free: %s / needed: %d)', self.repo, free_space, backup_size) - raise MoulinetteError(errno.EIO, m18n.n( - 'not_enough_disk_space', path=self.repo)) + raise MoulinetteError('not_enough_disk_space', path=self.repo) def _organize_files(self): """ @@ -1656,12 +1650,10 @@ class BackupMethod(object): i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', answers='y/N', size=str(size))) except NotImplemented: - raise MoulinetteError(errno.EIO, - m18n.n('backup_unable_to_organize_files')) + raise MoulinetteError('backup_unable_to_organize_files') else: if i != 'y' and i != 'Y': - raise MoulinetteError(errno.EIO, - m18n.n('backup_unable_to_organize_files')) + raise MoulinetteError('backup_unable_to_organize_files') # Copy unbinded path logger.debug(m18n.n('backup_copying_to_organize_the_archive', @@ -1751,8 +1743,7 @@ class CopyBackupMethod(BackupMethod): super(CopyBackupMethod, self).mount() if not os.path.isdir(self.repo): - raise MoulinetteError(errno.EIO, - m18n.n('backup_no_uncompress_archive_dir')) + raise MoulinetteError('backup_no_uncompress_archive_dir') filesystem.mkdir(self.work_dir, parent=True) ret = subprocess.call(["mount", "-r", "--rbind", self.repo, @@ -1763,8 +1754,7 @@ class CopyBackupMethod(BackupMethod): logger.warning(m18n.n("bind_mouting_disable")) subprocess.call(["mountpoint", "-q", dest, "&&", "umount", "-R", dest]) - raise MoulinetteError(errno.EIO, - m18n.n('backup_cant_mount_uncompress_archive')) + raise MoulinetteError('backup_cant_mount_uncompress_archive') class TarBackupMethod(BackupMethod): @@ -1809,8 +1799,7 @@ class TarBackupMethod(BackupMethod): except: logger.debug("unable to open '%s' for writing", self._archive_file, exc_info=1) - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_open_failed')) + raise MoulinetteError('backup_archive_open_failed') # Add files to the archive try: @@ -1821,8 +1810,7 @@ class TarBackupMethod(BackupMethod): tar.close() except IOError: logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) - raise MoulinetteError(errno.EIO, - m18n.n('backup_creation_failed')) + raise MoulinetteError('backup_creation_failed') # Move info file shutil.copy(os.path.join(self.work_dir, 'info.json'), @@ -1850,8 +1838,7 @@ class TarBackupMethod(BackupMethod): except: logger.debug("cannot open backup archive '%s'", self._archive_file, exc_info=1) - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_open_failed')) + raise MoulinetteError('backup_archive_open_failed') tar.close() # Mount the tarball @@ -1912,12 +1899,10 @@ class BorgBackupMethod(BackupMethod): super(CopyBackupMethod, self).backup() # TODO run borg create command - raise MoulinetteError( - errno.EIO, m18n.n('backup_borg_not_implemented')) + raise MoulinetteError('backup_borg_not_implemented') def mount(self, mnt_path): - raise MoulinetteError( - errno.EIO, m18n.n('backup_borg_not_implemented')) + raise MoulinetteError('backup_borg_not_implemented') class CustomBackupMethod(BackupMethod): @@ -1963,8 +1948,7 @@ class CustomBackupMethod(BackupMethod): ret = hook_callback('backup_method', [self.method], args=self._get_args('backup')) if ret['failed']: - raise MoulinetteError(errno.EIO, - m18n.n('backup_custom_backup_error')) + raise MoulinetteError('backup_custom_backup_error') def mount(self, restore_manager): """ @@ -1977,8 +1961,7 @@ class CustomBackupMethod(BackupMethod): ret = hook_callback('backup_method', [self.method], args=self._get_args('mount')) if ret['failed']: - raise MoulinetteError(errno.EIO, - m18n.n('backup_custom_mount_error')) + raise MoulinetteError('backup_custom_mount_error') def _get_args(self, action): """Return the arguments to give to the custom script""" @@ -2014,8 +1997,7 @@ 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 MoulinetteError(errno.EINVAL, - m18n.n('backup_archive_name_exists')) + raise MoulinetteError('backup_archive_name_exists') # Validate output_directory option if output_directory: @@ -2025,17 +2007,14 @@ def backup_create(name=None, description=None, methods=[], if output_directory.startswith(ARCHIVES_PATH) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_forbidden')) + raise MoulinetteError('backup_output_directory_forbidden') # Check that output directory is empty if os.path.isdir(output_directory) and no_compress and \ os.listdir(output_directory): - raise MoulinetteError(errno.EIO, - m18n.n('backup_output_directory_not_empty')) + raise MoulinetteError('backup_output_directory_not_empty') elif no_compress: - raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_required')) + raise MoulinetteError('backup_output_directory_required') # Define methods (retro-compat) if not methods: @@ -2217,8 +2196,7 @@ def backup_info(name, with_details=False, human_readable=False): # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_name_unknown', name=name)) + raise MoulinetteError('backup_archive_name_unknown', name=name) # If symlink, retrieve the real path if os.path.islink(archive_file): @@ -2292,8 +2270,8 @@ def backup_delete(name): """ if name not in backup_list()["archives"]: - raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', - name=name)) + raise MoulinetteError('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 59b7a72c3..6c9b890ca 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -172,8 +172,7 @@ def _certificate_install_selfsigned(domain_list, force=False): status = _get_status(domain) if status["summary"]["code"] in ('good', 'great'): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_attempt_to_replace_valid_cert', domain=domain)) + raise MoulinetteError('certmanager_attempt_to_replace_valid_cert', domain=domain) operation_logger.start() @@ -203,8 +202,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if p.returncode != 0: logger.warning(out) - raise MoulinetteError( - errno.EIO, m18n.n('domain_cert_gen_failed')) + raise MoulinetteError('domain_cert_gen_failed') else: logger.debug(out) @@ -262,14 +260,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F for domain in domain_list: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_unknown', domain=domain)) + raise MoulinetteError('certmanager_domain_unknown', domain=domain) # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_cert_not_selfsigned', domain=domain)) + raise MoulinetteError('certmanager_domain_cert_not_selfsigned', domain=domain) if staging: logger.warning( @@ -349,25 +345,21 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Is it in Yunohost dmomain list? if domain not in yunohost.domain.domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_unknown', domain=domain)) + raise MoulinetteError('certmanager_domain_unknown', domain=domain) status = _get_status(domain) # Does it expire soon? if status["validity"] > VALIDITY_LIMIT and not force: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_attempt_to_renew_valid_cert', domain=domain)) + raise MoulinetteError('certmanager_attempt_to_renew_valid_cert', domain=domain) # Does it have a Let's Encrypt cert? if status["CA_type"]["code"] != "lets-encrypt": - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + raise MoulinetteError('certmanager_attempt_to_renew_nonLE_cert', domain=domain) # Check ACME challenge configured for given domain if not _check_acme_challenge_configuration(domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_acme_not_configured_for_domain', domain=domain)) + raise MoulinetteError('certmanager_acme_not_configured_for_domain', domain=domain) if staging: logger.warning( @@ -563,19 +555,16 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): CA=certification_authority) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_hit_rate_limit', domain=domain)) + raise MoulinetteError('certmanager_hit_rate_limit', domain=domain) else: logger.error(str(e)) _display_debug_information(domain) - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_cert_signing_failed')) + raise MoulinetteError('certmanager_cert_signing_failed') except Exception as e: logger.error(str(e)) - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_cert_signing_failed')) + raise MoulinetteError('certmanager_cert_signing_failed') import requests # lazy loading this module for performance reasons try: @@ -624,8 +613,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): status_summary = _get_status(domain)["summary"] if status_summary["code"] != "great": - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + raise MoulinetteError('certmanager_certificate_fetching_or_enabling_failed', domain=domain) def _prepare_certificate_signing_request(domain, key_file, output_folder): @@ -658,8 +646,7 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_no_cert_file', domain=domain, file=cert_file)) + raise MoulinetteError('certmanager_no_cert_file', domain=domain, file=cert_file) from OpenSSL import crypto # lazy loading this module for performance reasons try: @@ -668,8 +655,7 @@ def _get_status(domain): except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) + raise MoulinetteError('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception) cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN @@ -830,13 +816,11 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + raise MoulinetteError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_http_not_working', domain=domain)) + raise MoulinetteError('certmanager_domain_http_not_working', domain=domain) def _get_dns_ip(domain): @@ -845,8 +829,7 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_error_no_A_record', domain=domain)) + raise MoulinetteError('certmanager_error_no_A_record', domain=domain) return str(answers[0]) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index aad6771be..188c1dffc 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -70,8 +70,8 @@ class MyMigration(Migration): # Migration didn't succeed, so we rollback and raise an exception os.system("mv /etc/yunohost/dyndns/*+165* /tmp") - raise MoulinetteError(m18n.n('migrate_tsig_failed', domain=domain, - error_code=str(r.status_code), error=error)) + raise MoulinetteError('migrate_tsig_failed', domain=domain, + error_code=str(r.status_code), error=error) # remove old certificates os.system("mv /etc/yunohost/dyndns/*+157* /tmp") diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 21a7992ae..98ebf3239 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -162,8 +162,7 @@ def domain_remove(operation_logger, auth, domain, force=False): continue else: if app_domain == domain: - raise MoulinetteError(errno.EPERM, - m18n.n('domain_uninstall_app_first')) + raise MoulinetteError('domain_uninstall_app_first') operation_logger.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 8bf2d254f..09f672662 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -136,8 +136,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom # Verify if domain is available if not _dyndns_available(subscribe_host, domain): - raise MoulinetteError(errno.ENOENT, - m18n.n('dyndns_unavailable', domain=domain)) + raise MoulinetteError('dyndns_unavailable', domain=domain) operation_logger.start() @@ -167,8 +166,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom error = json.loads(r.text)['error'] except: error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text) - raise MoulinetteError(errno.EPERM, - m18n.n('dyndns_registration_failed', error=error)) + raise MoulinetteError('dyndns_registration_failed', error=error) logger.success(m18n.n('dyndns_registered')) @@ -302,8 +300,7 @@ 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 MoulinetteError(errno.EPERM, - m18n.n('dyndns_ip_update_failed')) + raise MoulinetteError('dyndns_ip_update_failed') logger.success(m18n.n('dyndns_ip_updated')) @@ -359,5 +356,4 @@ def _guess_current_dyndns_domain(dyn_host): else: return (_domain, path) - raise MoulinetteError(errno.EINVAL, - m18n.n('dyndns_no_domain_registered')) + raise MoulinetteError('dyndns_no_domain_registered') diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index b7457878e..302ffe302 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -384,14 +384,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, # Check and return process' return code if returncode is None: if raise_on_error: - raise MoulinetteError( - errno.EIO, m18n.n('hook_exec_not_terminated', path=path)) + raise MoulinetteError('hook_exec_not_terminated', path=path) else: logger.error(m18n.n('hook_exec_not_terminated', path=path)) return 1 elif raise_on_error and returncode != 0: - raise MoulinetteError( - errno.EIO, m18n.n('hook_exec_failed', path=path)) + raise MoulinetteError('hook_exec_failed', path=path) return returncode diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 80a7a134f..d31231c72 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -148,8 +148,7 @@ def log_display(path, number=50, share=False): log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): - raise MoulinetteError(errno.EINVAL, - m18n.n('log_does_exists', log=path)) + raise MoulinetteError('log_does_exists', log=path) infos = {} diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index b01e36238..0839c6ae7 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -390,11 +390,9 @@ def monitor_show_stats(period, date=None): result = _retrieve_stats(period, date) if result is False: - raise MoulinetteError(errno.ENOENT, - m18n.n('monitor_stats_file_not_found')) + raise MoulinetteError('monitor_stats_file_not_found') elif result is None: - raise MoulinetteError(errno.EINVAL, - m18n.n('monitor_stats_period_unavailable')) + raise MoulinetteError('monitor_stats_period_unavailable') return result diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index fcb08485e..d49c1af35 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -83,8 +83,7 @@ def settings_set(key, value): settings = _get_settings() if key not in settings: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_key_doesnt_exists', settings_key=key)) + raise MoulinetteError('global_settings_key_doesnt_exists', settings_key=key) key_type = settings[key]["type"] @@ -138,8 +137,7 @@ def settings_reset(key): settings = _get_settings() if key not in settings: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_key_doesnt_exists', settings_key=key)) + raise MoulinetteError('global_settings_key_doesnt_exists', settings_key=key) settings[key]["value"] = settings[key]["default"] _save_settings(settings) diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 5f82c467b..1c7581365 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -406,8 +406,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, if p.returncode != 0: logger.warning(out) - raise MoulinetteError(errno.EPERM, - m18n.n('yunohost_ca_creation_failed')) + raise MoulinetteError('yunohost_ca_creation_failed') else: logger.debug(out) @@ -1070,8 +1069,8 @@ def _load_migration(migration_file): import traceback traceback.print_exc() - raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', - number=number, name=name)) + raise MoulinetteError('migrations_error_failed_to_load_migration', + number=number, name=name) def _skip_all_migrations(): """ diff --git a/src/yunohost/user.py b/src/yunohost/user.py index d074c81c1..c6752d2dc 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -188,8 +188,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error', error=e.strerror)) + raise MoulinetteError('ssowat_persistent_conf_read_error', error=e.strerror) except IOError: ssowat_conf = {} @@ -199,8 +198,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError(errno.EPERM, - m18n.n('ssowat_persistent_conf_write_error', error=e.strerror)) + raise MoulinetteError('ssowat_persistent_conf_write_error', error=e.strerror) if auth.add('uid=%s,ou=users' % username, attr_dict): # Invalidate passwd to take user creation into account @@ -355,8 +353,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) else: - raise MoulinetteError(errno.EINVAL, - m18n.n('mail_alias_remove_failed', mail=mail)) + raise MoulinetteError('mail_alias_remove_failed', mail=mail) new_attr_dict['mail'] = user['mail'] if add_mailforward: @@ -375,8 +372,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) else: - raise MoulinetteError(errno.EINVAL, - m18n.n('mail_forward_remove_failed', mail=mail)) + raise MoulinetteError('mail_forward_remove_failed', mail=mail) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: From 0468d6b900acd199053ab977e06716030e0f2bb7 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Fri, 9 Nov 2018 17:50:39 +0100 Subject: [PATCH 226/250] sed 3 lines deletion of line breaks to be able to use "sed" : sed -i "N; s@MoulinetteError(.*m18n\.n(\n* *@MoulinetteError(m18n.n(@g" --- src/yunohost/backup.py | 13 +++++-------- src/yunohost/certificate.py | 3 +-- src/yunohost/dyndns.py | 8 +++----- src/yunohost/hook.py | 3 +-- src/yunohost/service.py | 5 ++--- src/yunohost/settings.py | 28 +++++++++++----------------- src/yunohost/user.py | 9 +++------ 7 files changed, 26 insertions(+), 43 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 31acca188..9893def33 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1083,14 +1083,12 @@ class RestoreManager(): return True elif free_space > needed_space: # TODO Add --force options to avoid the error raising - raise MoulinetteError(errno.EIO, - m18n.n('restore_may_be_not_enough_disk_space', + raise MoulinetteError(m18n.n('restore_may_be_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin)) else: - raise MoulinetteError(errno.EIO, - m18n.n('restore_not_enough_disk_space', + raise MoulinetteError(m18n.n('restore_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin)) @@ -1143,7 +1141,7 @@ class RestoreManager(): newlines.append(row) except (IOError, OSError, csv.Error) as e: - raise MoulinetteError(errno.EIO,m18n.n('error_reading_file', + raise MoulinetteError(m18n.n('error_reading_file', file=backup_csv, error=str(e))) @@ -2204,9 +2202,8 @@ def backup_info(name, with_details=False, human_readable=False): # Raise exception if link is broken (e.g. on unmounted external storage) if not os.path.exists(archive_file): - raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_broken_link', - path=archive_file)) + raise MoulinetteError('backup_archive_broken_link', + path=archive_file) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 6c9b890ca..61eeca319 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -476,8 +476,7 @@ location ^~ '/.well-known/acme-challenge/' contents = f.read() if '/.well-known/acme-challenge' in contents: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_conflicting_nginx_file', filepath=path)) + raise MoulinetteError('certmanager_conflicting_nginx_file', filepath=path) # Write the conf if os.path.exists(nginx_conf_file): diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 09f672662..d9643054e 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -106,9 +106,8 @@ def _dyndns_available(provider, domain): expected_status_code=None) except MoulinetteError as e: logger.error(str(e)) - raise MoulinetteError(errno.EIO, - m18n.n('dyndns_could_not_check_available', - domain=domain, provider=provider)) + raise MoulinetteError('dyndns_could_not_check_available', + domain=domain, provider=provider) return r == u"Domain %s is available" % domain @@ -130,8 +129,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): - raise MoulinetteError(errno.ENOENT, - m18n.n('dyndns_domain_not_provided', + raise MoulinetteError(m18n.n('dyndns_domain_not_provided', domain=domain, provider=subscribe_host)) # Verify if domain is available diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 302ffe302..fef5199d9 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -255,8 +255,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, try: hl = hooks_names[n] except KeyError: - raise MoulinetteError(errno.EINVAL, - m18n.n('hook_name_unknown', n)) + raise MoulinetteError('hook_name_unknown', n) # Iterate over hooks with this name for h in hl: # Update hooks dict diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 2c85b7047..14d16a73d 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -431,9 +431,8 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, names = pre_result['succeed'].keys() if not names: - raise MoulinetteError(errno.EIO, - m18n.n('service_regenconf_failed', - services=', '.join(pre_result['failed']))) + raise MoulinetteError('service_regenconf_failed', + services=', '.join(pre_result['failed'])) # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index d49c1af35..ca9a98561 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -54,7 +54,7 @@ def settings_get(key, full=False): settings = _get_settings() if key not in settings: - raise MoulinetteError(errno.EINVAL, m18n.n( + raise MoulinetteError(m18n.n( 'global_settings_key_doesnt_exists', settings_key=key)) if full: @@ -89,9 +89,8 @@ def settings_set(key, value): if key_type == "bool": if not isinstance(value, bool): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type)) + raise MoulinetteError('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): @@ -102,24 +101,21 @@ def settings_set(key, value): 'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)) else: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_bad_type_for_setting', setting=key, + raise MoulinetteError(m18n.n('global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)) elif key_type == "string": if not isinstance(value, basestring): - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_bad_type_for_setting', setting=key, + raise MoulinetteError(m18n.n('global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)) elif key_type == "enum": if value not in settings[key]["choices"]: - raise MoulinetteError(errno.EINVAL, m18n.n( + raise MoulinetteError(m18n.n( 'global_settings_bad_choice_for_enum', setting=key, received_type=type(value).__name__, expected_type=", ".join(settings[key]["choices"]))) else: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'global_settings_unknown_type', setting=key, - unknown_type=key_type)) + raise MoulinetteError('global_settings_unknown_type', setting=key, + unknown_type=key_type) settings[key]["value"] = value @@ -213,7 +209,7 @@ def _get_settings(): setting_key=key)) unknown_settings[key] = value except Exception as e: - raise MoulinetteError(errno.EIO, m18n.n('global_settings_cant_open_settings', reason=e), + raise MoulinetteError(m18n.n('global_settings_cant_open_settings', reason=e), exc_info=1) if unknown_settings: @@ -235,14 +231,12 @@ def _save_settings(settings, location=SETTINGS_PATH): try: result = json.dumps(settings_without_description, indent=4) except Exception as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('global_settings_cant_serialize_settings', reason=e), + raise MoulinetteError(m18n.n('global_settings_cant_serialize_settings', reason=e), exc_info=1) try: with open(location, "w") as settings_fd: settings_fd.write(result) except Exception as e: - raise MoulinetteError(errno.EIO, - m18n.n('global_settings_cant_write_settings', reason=e), + raise MoulinetteError(m18n.n('global_settings_cant_write_settings', reason=e), exc_info=1) diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c6752d2dc..78bfbf047 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -145,8 +145,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, - m18n.n('mail_domain_unknown', + raise MoulinetteError(m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) operation_logger.start() @@ -325,8 +324,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, ] auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise MoulinetteError(errno.EINVAL, - m18n.n('mail_domain_unknown', + raise MoulinetteError(m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) if mail in aliases: raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) @@ -340,8 +338,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, for mail in add_mailalias: auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise MoulinetteError(errno.EINVAL, - m18n.n('mail_domain_unknown', + raise MoulinetteError(m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] From fab662f4ef8d4f039ec2ca3c42f6707d419854c3 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Fri, 9 Nov 2018 19:07:23 +0100 Subject: [PATCH 227/250] The rest The ones that sed didn't find. Modified by hand. --- src/yunohost/app.py | 52 ++++++++++----------------------- src/yunohost/backup.py | 4 +-- src/yunohost/certificate.py | 3 +- src/yunohost/domain.py | 6 ++-- src/yunohost/dyndns.py | 4 +-- src/yunohost/hook.py | 2 +- src/yunohost/log.py | 2 +- src/yunohost/service.py | 23 ++++----------- src/yunohost/tools.py | 16 ++++------ src/yunohost/user.py | 3 +- src/yunohost/utils/yunopaste.py | 9 ++---- 11 files changed, 36 insertions(+), 88 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 92aae5336..5e7860992 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -184,9 +184,7 @@ def app_fetchlist(url=None, name=None): with open(list_file, "w") as f: f.write(appslist) except Exception as e: - raise MoulinetteError(errno.EIO, - "Error while writing appslist %s: %s" % - (name, str(e))) + raise MoulinetteError("Error while writing appslist %s: %s" % (name, str(e))) now = int(time.time()) appslists[name]["lastUpdate"] = now @@ -843,9 +841,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg - raise MoulinetteError(errno.EINTR, msg) + raise MoulinetteError(msg) msg = error_msg - raise MoulinetteError(errno.EIO, msg) + raise MoulinetteError(msg) # Clean hooks and add new ones hook_remove(app_instance_name) @@ -1135,10 +1133,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): operation_logger.start() if '/' in app_map(raw=True)[domain]: - raise MoulinetteError(errno.EEXIST, - m18n.n('app_make_default_location_already_used', - app=app, domain=app_domain, - other_app=app_map(raw=True)[domain]["/"]["id"])) + raise MoulinetteError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"]) try: with open('/etc/ssowat/conf.json.persistent') as json_conf: @@ -1298,14 +1293,10 @@ def app_checkurl(auth, url, app=None): installed = True continue if path == p: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_location_already_used', - app=a["id"], path=path)) + raise MoulinetteError('app_location_already_used', app=a["id"], path=path) # can't install "/a/b/" if "/a/" exists elif path.startswith(p) or p.startswith(path): - raise MoulinetteError(errno.EPERM, - m18n.n('app_location_install_failed', - other_path=p, other_app=a['id'])) + raise MoulinetteError('app_location_install_failed', other_path=p, other_app=a['id')) if app is not None and not installed: app_setting(app, 'domain', value=domain) @@ -1482,7 +1473,7 @@ def app_action_run(app, action, args=None): actions = {x["id"]: x for x in actions} if action not in actions: - raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) + raise MoulinetteError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) action_declaration = actions[action] @@ -2066,7 +2057,7 @@ def _check_manifest_requirements(manifest, app_instance_name): yunohost_req = requirements.get('yunohost', None) if (not yunohost_req or not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'): - raise MoulinetteError(errno.EINVAL, '{0}{1}'.format( + raise MoulinetteError('{0}{1}'.format( m18n.g('colon', m18n.n('app_incompatible'), app=app_instance_name), m18n.n('app_package_need_update', app=app_instance_name))) elif not requirements: @@ -2079,9 +2070,7 @@ def _check_manifest_requirements(manifest, app_instance_name): versions = packages.get_installed_version( *requirements.keys(), strict=True, as_dict=True) except packages.PackageException as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_requirements_failed', - error=str(e), app=app_instance_name)) + raise MoulinetteError('app_requirements_failed', error=str(e), app=app_instance_name) # Iterate over requirements for pkgname, spec in requirements.items(): @@ -2221,28 +2210,20 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): # Validate argument choice if arg_choices and arg_value not in arg_choices: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_choice_invalid', - name=arg_name, choices=', '.join(arg_choices))) + raise MoulinetteError('app_argument_choice_invalid', name=arg_name, choices=', '.join(arg_choices)) # Validate argument type if arg_type == 'domain': if arg_value not in domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=m18n.n('domain_unknown'))) + raise MoulinetteError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown')) elif arg_type == 'user': try: user_info(auth, arg_value) except MoulinetteError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=e.strerror)) + raise MoulinetteError('app_argument_invalid', name=arg_name, error=e.strerror) elif arg_type == 'app': if not _is_installed(arg_value): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_invalid', - name=arg_name, error=m18n.n('app_unknown'))) + raise MoulinetteError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown')) elif arg_type == 'boolean': if isinstance(arg_value, bool): arg_value = 1 if arg_value else 0 @@ -2252,9 +2233,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): elif str(arg_value).lower() in ["0", "no", "n"]: arg_value = 0 else: - raise MoulinetteError(errno.EINVAL, - m18n.n('app_argument_choice_invalid', - name=arg_name, choices='yes, no, y, n, 1, 0')) + raise MoulinetteError('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0') elif arg_type == 'password': from yunohost.utils.password import assert_password_is_strong_enough assert_password_is_strong_enough('user', arg_value) @@ -2453,8 +2432,7 @@ def _write_appslist_list(appslist_lists): with open(APPSLISTS_JSON, "w") as f: json.dump(appslist_lists, f) except Exception as e: - raise MoulinetteError(errno.EIO, - "Error while writing list of appslist %s: %s" % + raise MoulinetteError("Error while writing list of appslist %s: %s" % (APPSLISTS_JSON, str(e))) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 9893def33..84d89dbca 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2295,9 +2295,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 MoulinetteError(errno.EINVAL, - m18n.n('backup_output_symlink_dir_broken', - path=ARCHIVES_PATH)) + raise MoulinetteError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH) os.mkdir(ARCHIVES_PATH, 0750) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 61eeca319..e75aebcc7 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -106,8 +106,7 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: # Is it in Yunohost domain list? if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n( - 'certmanager_domain_unknown', domain=domain)) + raise MoulinetteError('certmanager_domain_unknown', domain=domain) certificates = {} diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 98ebf3239..7be214914 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -87,16 +87,14 @@ def domain_add(operation_logger, auth, domain, dyndns=False): # Do not allow to subscribe to multiple dyndns domains... if os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise MoulinetteError(errno.EPERM, - m18n.n('domain_dyndns_already_subscribed')) + raise MoulinetteError('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 MoulinetteError(errno.EINVAL, - m18n.n('domain_dyndns_root_unknown')) + raise MoulinetteError('domain_dyndns_root_unknown') # Actually subscribe dyndns_subscribe(domain=domain) diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index d9643054e..cda55f5e0 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -77,9 +77,7 @@ def _dyndns_provides(provider, domain): dyndomains = download_json('https://%s/domains' % provider, timeout=30) except MoulinetteError as e: logger.error(str(e)) - raise MoulinetteError(errno.EIO, - m18n.n('dyndns_could_not_check_provide', - domain=domain, provider=provider)) + raise MoulinetteError('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:]) diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index fef5199d9..665d2fe40 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -318,7 +318,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, if path[0] != '/': path = os.path.realpath(path) if not os.path.isfile(path): - raise MoulinetteError(errno.EIO, m18n.g('file_not_exist', path=path)) + raise MoulinetteError('file_not_exist', path=path) # Construct command variables cmd_args = '' diff --git a/src/yunohost/log.py b/src/yunohost/log.py index d31231c72..3df630b14 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -188,7 +188,7 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): logger.warning(error) else: - raise MoulinetteError(errno.EINVAL, error) + raise MoulinetteError(error) # Display logs if exist if os.path.exists(log_path): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 14d16a73d..140c107be 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -130,10 +130,7 @@ def service_start(names): logger.success(m18n.n('service_started', service=name)) else: if service_status(name)['status'] != 'running': - raise MoulinetteError(errno.EPERM, - m18n.n('service_start_failed', - service=name, - logs=_get_journalctl_logs(name))) + raise MoulinetteError('service_start_failed', service=name, logs=_get_journalctl_logs(name)) logger.debug(m18n.n('service_already_started', service=name)) @@ -152,10 +149,7 @@ def service_stop(names): logger.success(m18n.n('service_stopped', service=name)) else: if service_status(name)['status'] != 'inactive': - raise MoulinetteError(errno.EPERM, - m18n.n('service_stop_failed', - service=name, - logs=_get_journalctl_logs(name))) + raise MoulinetteError('service_stop_failed', service=name, logs=_get_journalctl_logs(name)) logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() @@ -174,10 +168,7 @@ def service_enable(operation_logger, names): if _run_service_command('enable', name): logger.success(m18n.n('service_enabled', service=name)) else: - raise MoulinetteError(errno.EPERM, - m18n.n('service_enable_failed', - service=name, - logs=_get_journalctl_logs(name))) + raise MoulinetteError('service_enable_failed', service=name, logs=_get_journalctl_logs(name)) def service_disable(names): @@ -194,10 +185,7 @@ def service_disable(names): if _run_service_command('disable', name): logger.success(m18n.n('service_disabled', service=name)) else: - raise MoulinetteError(errno.EPERM, - m18n.n('service_disable_failed', - service=name, - logs=_get_journalctl_logs(name))) + raise MoulinetteError('service_disable_failed', service=name, logs=_get_journalctl_logs(name)) def service_status(names=[]): @@ -220,8 +208,7 @@ def service_status(names=[]): for name in names: if check_names and name not in services.keys(): - raise MoulinetteError(errno.EINVAL, - m18n.n('service_unknown', service=name)) + raise MoulinetteError('service_unknown', service=name) # this "service" isn't a service actually so we skip it # diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 1c7581365..8e7da9939 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -139,8 +139,7 @@ def tools_adminpw(auth, new_password, check_strength=True): auth.update("cn=admin", { "userPassword": new_hash, }) except: logger.exception('unable to change admin password') - raise MoulinetteError(errno.EPERM, - m18n.n('admin_password_change_failed')) + raise MoulinetteError('admin_password_change_failed') else: # Write as root password try: @@ -289,8 +288,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): - raise MoulinetteError(errno.EPERM, - m18n.n('yunohost_already_installed')) + raise MoulinetteError('yunohost_already_installed') # Check password if not force_password: @@ -319,9 +317,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, dyndns = True # If not, abort the postinstall else: - raise MoulinetteError(errno.EEXIST, - m18n.n('dyndns_unavailable', - domain=domain)) + raise MoulinetteError('dyndns_unavailable', domain=domain) else: dyndns = False else: @@ -364,8 +360,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError(errno.EINVAL, - m18n.n('ssowat_persistent_conf_read_error', error=str(e))) + raise MoulinetteError('ssowat_persistent_conf_read_error', error=str(e)) except IOError: ssowat_conf = {} @@ -378,8 +373,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError(errno.EPERM, - m18n.n('ssowat_persistent_conf_write_error', error=str(e))) + raise MoulinetteError('ssowat_persistent_conf_write_error', error=str(e)) os.system('chmod 644 /etc/ssowat/conf.json.persistent') diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 78bfbf047..c8aee153f 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -71,8 +71,7 @@ def user_list(auth, fields=None): if attr in keys: attrs.append(attr) else: - raise MoulinetteError(errno.EINVAL, - m18n.n('field_invalid', attr)) + raise MoulinetteError('field_invalid', attrz1) else: attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 2b53062d1..475f29f79 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -13,17 +13,14 @@ def yunopaste(data): try: r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: - raise MoulinetteError(errno.EIO, - "Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) + raise MoulinetteError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) if r.status_code != 200: - raise MoulinetteError(errno.EIO, - "Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) + raise MoulinetteError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) try: url = json.loads(r.text)["key"] except: - raise MoulinetteError(errno.EIO, - "Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) + raise MoulinetteError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) return "%s/raw/%s" % (paste_server, url) From 9eec09da81e6927e03f459b8c9b7f58f07a182f5 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Sat, 17 Nov 2018 14:45:24 +0100 Subject: [PATCH 228/250] - "m18n.n" --- src/yunohost/backup.py | 14 +++----------- src/yunohost/settings.py | 25 ++++++++++--------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 84d89dbca..08544a1a6 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -1083,15 +1083,9 @@ class RestoreManager(): return True elif free_space > needed_space: # TODO Add --force options to avoid the error raising - raise MoulinetteError(m18n.n('restore_may_be_not_enough_disk_space', - free_space=free_space, - needed_space=needed_space, - margin=margin)) + raise MoulinetteError('restore_may_be_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin) else: - raise MoulinetteError(m18n.n('restore_not_enough_disk_space', - free_space=free_space, - needed_space=needed_space, - margin=margin)) + raise MoulinetteError('restore_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin) ########################################################################### # "Actual restore" (reverse step of the backup collect part) # @@ -1141,9 +1135,7 @@ class RestoreManager(): newlines.append(row) except (IOError, OSError, csv.Error) as e: - raise MoulinetteError(m18n.n('error_reading_file', - file=backup_csv, - error=str(e))) + raise MoulinetteError('error_reading_file', file=backup_csv, error=str(e)) if not contains_php5: return diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index ca9a98561..5d875afb9 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -54,8 +54,7 @@ def settings_get(key, full=False): settings = _get_settings() if key not in settings: - raise MoulinetteError(m18n.n( - 'global_settings_key_doesnt_exists', settings_key=key)) + raise MoulinetteError('global_settings_key_doesnt_exists', settings_key=key) if full: return settings[key] @@ -101,18 +100,17 @@ def settings_set(key, value): 'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)) else: - raise MoulinetteError(m18n.n('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type)) + raise MoulinetteError('global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type) elif key_type == "string": if not isinstance(value, basestring): - raise MoulinetteError(m18n.n('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type)) + raise MoulinetteError('global_settings_bad_type_for_setting', setting=key, + received_type=type(value).__name__, expected_type=key_type) elif key_type == "enum": if value not in settings[key]["choices"]: - raise MoulinetteError(m18n.n( - 'global_settings_bad_choice_for_enum', setting=key, + raise MoulinetteError('global_settings_bad_choice_for_enum', setting=key, received_type=type(value).__name__, - expected_type=", ".join(settings[key]["choices"]))) + expected_type=", ".join(settings[key]["choices"])) else: raise MoulinetteError('global_settings_unknown_type', setting=key, unknown_type=key_type) @@ -209,8 +207,7 @@ def _get_settings(): setting_key=key)) unknown_settings[key] = value except Exception as e: - raise MoulinetteError(m18n.n('global_settings_cant_open_settings', reason=e), - exc_info=1) + raise MoulinetteError('global_settings_cant_open_settings', reason=e, exc_info=1) if unknown_settings: try: @@ -231,12 +228,10 @@ def _save_settings(settings, location=SETTINGS_PATH): try: result = json.dumps(settings_without_description, indent=4) except Exception as e: - raise MoulinetteError(m18n.n('global_settings_cant_serialize_settings', reason=e), - exc_info=1) + raise MoulinetteError('global_settings_cant_serialize_settings', reason=e, exc_info=1) try: with open(location, "w") as settings_fd: settings_fd.write(result) except Exception as e: - raise MoulinetteError(m18n.n('global_settings_cant_write_settings', reason=e), - exc_info=1) + raise MoulinetteError('global_settings_cant_write_settings', reason=e, exc_info=1) From 410e757732b9ce46c58774c05a87b06f19ba1d1a Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Fri, 30 Nov 2018 17:28:15 +0100 Subject: [PATCH 229/250] corrections --- src/yunohost/app.py | 4 ++-- src/yunohost/dyndns.py | 3 +-- src/yunohost/user.py | 9 ++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 5e7860992..96c235835 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1296,7 +1296,7 @@ def app_checkurl(auth, url, app=None): raise MoulinetteError('app_location_already_used', app=a["id"], path=path) # can't install "/a/b/" if "/a/" exists elif path.startswith(p) or p.startswith(path): - raise MoulinetteError('app_location_install_failed', other_path=p, other_app=a['id')) + raise MoulinetteError('app_location_install_failed', other_path=p, other_app=a['id']) if app is not None and not installed: app_setting(app, 'domain', value=domain) @@ -1618,7 +1618,7 @@ def app_config_apply(app, args): installed = _is_installed(app) if not installed: - raise MoulinetteError('app_not_installed', app=app_id) + raise MoulinetteError('app_not_installed', app=app) config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index cda55f5e0..93f3a7a38 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -127,8 +127,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): - raise MoulinetteError(m18n.n('dyndns_domain_not_provided', - domain=domain, provider=subscribe_host)) + raise MoulinetteError('dyndns_domain_not_provided', domain=domain, provider=subscribe_host) # Verify if domain is available if not _dyndns_available(subscribe_host, domain): diff --git a/src/yunohost/user.py b/src/yunohost/user.py index c8aee153f..67cfe6cf1 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -71,7 +71,7 @@ def user_list(auth, fields=None): if attr in keys: attrs.append(attr) else: - raise MoulinetteError('field_invalid', attrz1) + raise MoulinetteError('field_invalid', attr) else: attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] @@ -140,7 +140,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas ] if mail in aliases: - raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) + raise MoulinetteError('mail_unavailable') # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: @@ -326,7 +326,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, raise MoulinetteError(m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) if mail in aliases: - raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) + raise MoulinetteError('mail_unavailable') del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] @@ -337,8 +337,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, for mail in add_mailalias: auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise MoulinetteError(m18n.n('mail_domain_unknown', - domain=mail[mail.find('@') + 1:])) + raise MoulinetteError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] From c4b77b2894e47847f59bf4bf058690b1cb6b24f4 Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Sat, 1 Dec 2018 00:12:16 +0100 Subject: [PATCH 230/250] add YunohostError --- src/yunohost/utils/error.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/yunohost/utils/error.py diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py new file mode 100644 index 000000000..a2b6f5055 --- /dev/null +++ b/src/yunohost/utils/error.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2018 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +from moulinette.core import MoulinetteError + +class YunohostError(MoulinetteError): + """Yunohost base exception""" + def __init__(self, key, *args, **kwargs): + msg = m18n.n(key, *args, **kwargs) + super(MoulinetteError, self).__init__(msg) + From 4ba309a2217389622f0362f5b10e550e2180b88e Mon Sep 17 00:00:00 2001 From: Irina LAMBLA Date: Fri, 7 Dec 2018 23:56:57 +0100 Subject: [PATCH 231/250] modif MoulinetteError to YunohostError --- src/yunohost/app.py | 132 +++++++++--------- src/yunohost/backup.py | 82 +++++------ src/yunohost/certificate.py | 42 +++--- .../0002_migrate_to_tsig_sha256.py | 8 +- .../0003_migrate_to_stretch.py | 12 +- .../0005_postgresql_9p4_to_9p6.py | 6 +- .../0006_sync_admin_and_root_passwords.py | 2 +- src/yunohost/domain.py | 22 +-- src/yunohost/dyndns.py | 26 ++-- src/yunohost/firewall.py | 10 +- src/yunohost/hook.py | 16 +-- src/yunohost/log.py | 6 +- src/yunohost/monitor.py | 26 ++-- src/yunohost/service.py | 26 ++-- src/yunohost/settings.py | 24 ++-- src/yunohost/ssh.py | 6 +- src/yunohost/tests/test_appslist.py | 12 +- src/yunohost/tests/test_appurl.py | 8 +- src/yunohost/tests/test_backuprestore.py | 23 +-- src/yunohost/tests/test_changeurl.py | 4 +- src/yunohost/tests/test_settings.py | 28 ++-- src/yunohost/tools.py | 38 ++--- src/yunohost/user.py | 36 ++--- src/yunohost/utils/error.py | 1 + src/yunohost/utils/password.py | 4 +- src/yunohost/utils/yunopaste.py | 8 +- 26 files changed, 305 insertions(+), 303 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 96c235835..f4e48de45 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -39,7 +39,7 @@ from collections import OrderedDict from datetime import datetime from moulinette import msignals, m18n, msettings -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_json @@ -125,12 +125,12 @@ def app_fetchlist(url=None, name=None): appslists_to_be_fetched = [name] operation_logger.success() else: - raise MoulinetteError('custom_appslist_name_required') + raise YunohostError('custom_appslist_name_required') # If a name is given, look for an appslist with that name and fetch it elif name is not None: if name not in appslists.keys(): - raise MoulinetteError('appslist_unknown', appslist=name) + raise YunohostError('appslist_unknown', appslist=name) else: appslists_to_be_fetched = [name] @@ -184,7 +184,7 @@ def app_fetchlist(url=None, name=None): with open(list_file, "w") as f: f.write(appslist) except Exception as e: - raise MoulinetteError("Error while writing appslist %s: %s" % (name, str(e))) + raise YunohostError("Error while writing appslist %s: %s" % (name, str(e))) now = int(time.time()) appslists[name]["lastUpdate"] = now @@ -208,7 +208,7 @@ def app_removelist(operation_logger, name): # Make sure we know this appslist if name not in appslists.keys(): - raise MoulinetteError('appslist_unknown', appslist=name) + raise YunohostError('appslist_unknown', appslist=name) operation_logger.start() @@ -339,7 +339,7 @@ def app_info(app, show_status=False, raw=False): """ if not _is_installed(app): - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) app_setting_path = APPS_SETTING_PATH + app @@ -397,7 +397,7 @@ def app_map(app=None, raw=False, user=None): if app is not None: if not _is_installed(app): - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) apps = [app, ] else: apps = os.listdir(APPS_SETTING_PATH) @@ -449,10 +449,10 @@ def app_change_url(operation_logger, auth, app, domain, path): installed = _is_installed(app) if not installed: - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): - raise MoulinetteError("app_change_no_change_url_script", app_name=app) + raise YunohostError("app_change_no_change_url_script", app_name=app) old_domain = app_setting(app, "domain") old_path = app_setting(app, "path") @@ -464,7 +464,7 @@ def app_change_url(operation_logger, auth, app, domain, path): path = normalize_url_path(path) if (domain, path) == (old_domain, old_path): - raise MoulinetteError("app_change_url_identical_domains", domain=domain, path=path) + raise YunohostError("app_change_url_identical_domains", domain=domain, path=path) # WARNING / FIXME : checkurl will modify the settings # (this is a non intuitive behavior that should be changed) @@ -540,7 +540,7 @@ def app_change_url(operation_logger, auth, app, domain, path): stderr=subprocess.STDOUT, shell=True).rstrip() - raise MoulinetteError("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)) @@ -565,8 +565,8 @@ def app_upgrade(auth, app=[], url=None, file=None): try: app_list() - except MoulinetteError: - raise MoulinetteError('app_no_upgrade') + except YunohostError: + raise YunohostError('app_no_upgrade') upgraded_apps = [] @@ -586,7 +586,7 @@ def app_upgrade(auth, app=[], url=None, file=None): logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name)) installed = _is_installed(app_instance_name) if not installed: - raise MoulinetteError('app_not_installed', app=app_instance_name) + raise YunohostError('app_not_installed', app=app_instance_name) if app_instance_name in upgraded_apps: continue @@ -676,7 +676,7 @@ def app_upgrade(auth, app=[], url=None, file=None): operation_logger.success() if not upgraded_apps: - raise MoulinetteError('app_no_upgrade') + raise YunohostError('app_no_upgrade') app_ssowatconf(auth) @@ -722,12 +722,12 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on elif os.path.exists(app): manifest, extracted_app_folder = _extract_app_from_file(app) else: - raise MoulinetteError('app_unknown') + raise YunohostError('app_unknown') status['remote'] = manifest.get('remote', {}) # Check ID if 'id' not in manifest or '__' in manifest['id']: - raise MoulinetteError('app_id_invalid') + raise YunohostError('app_id_invalid') app_id = manifest['id'] @@ -738,7 +738,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on 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 MoulinetteError('app_already_installed', app=app_id) + raise YunohostError('app_already_installed', app=app_id) # Change app_id to the forked app id app_instance_name = app_id + '__' + str(instance_number) @@ -841,9 +841,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg - raise MoulinetteError(msg) + raise YunohostError(msg) msg = error_msg - raise MoulinetteError(msg) + raise YunohostError(msg) # Clean hooks and add new ones hook_remove(app_instance_name) @@ -879,7 +879,7 @@ def app_remove(operation_logger, auth, app): """ from yunohost.hook import hook_exec, hook_remove, hook_callback if not _is_installed(app): - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) operation_logger.start() @@ -969,7 +969,7 @@ def app_addaccess(auth, apps, users=[]): if allowed_user not in allowed_users: try: user_info(auth, allowed_user) - except MoulinetteError: + except YunohostError: logger.warning(m18n.n('user_unknown', user=allowed_user)) continue allowed_users.add(allowed_user) @@ -1129,17 +1129,17 @@ def app_makedefault(operation_logger, auth, app, domain=None): domain = app_domain operation_logger.related_to.append(('domain',domain)) elif domain not in domain_list(auth)['domains']: - raise MoulinetteError('domain_unknown') + raise YunohostError('domain_unknown') operation_logger.start() if '/' in app_map(raw=True)[domain]: - raise MoulinetteError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"]) + raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"]) try: with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError('ssowat_persistent_conf_read_error', error=e.strerror) + raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror) except IOError: ssowat_conf = {} @@ -1152,7 +1152,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError('ssowat_persistent_conf_write_error', error=e.strerror) + raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror) os.system('chmod 644 /etc/ssowat/conf.json.persistent') @@ -1204,7 +1204,7 @@ def app_checkport(port): if tools_port_available(port): logger.success(m18n.n('port_available', port=int(port))) else: - raise MoulinetteError('port_unavailable', port=int(port)) + raise YunohostError('port_unavailable', port=int(port)) def app_register_url(auth, app, domain, path): @@ -1231,7 +1231,7 @@ def app_register_url(auth, app, domain, path): if installed: settings = _get_app_settings(app) if "path" in settings.keys() and "domain" in settings.keys(): - raise MoulinetteError('app_already_installed_cant_change_url') + raise YunohostError('app_already_installed_cant_change_url') # Check the url is available conflicts = _get_conflicting_apps(auth, domain, path) @@ -1245,7 +1245,7 @@ def app_register_url(auth, app, domain, path): app_label=app_label, )) - raise MoulinetteError('app_location_unavailable', apps="\n".join(apps)) + raise YunohostError('app_location_unavailable', apps="\n".join(apps)) app_setting(app, 'domain', value=domain) app_setting(app, 'path', value=path) @@ -1283,7 +1283,7 @@ def app_checkurl(auth, url, app=None): apps_map = app_map(raw=True) if domain not in domain_list(auth)['domains']: - raise MoulinetteError('domain_unknown') + raise YunohostError('domain_unknown') if domain in apps_map: # Loop through apps @@ -1293,10 +1293,10 @@ def app_checkurl(auth, url, app=None): installed = True continue if path == p: - raise MoulinetteError('app_location_already_used', app=a["id"], path=path) + raise YunohostError('app_location_already_used', app=a["id"], path=path) # can't install "/a/b/" if "/a/" exists elif path.startswith(p) or p.startswith(path): - raise MoulinetteError('app_location_install_failed', other_path=p, other_app=a['id']) + raise YunohostError('app_location_install_failed', other_path=p, other_app=a['id']) if app is not None and not installed: app_setting(app, 'domain', value=domain) @@ -1328,10 +1328,10 @@ def app_initdb(user, password=None, db=None, sql=None): mysql_root_pwd = open('/etc/yunohost/mysql').read().rstrip() mysql_command = 'mysql -u root -p%s -e "CREATE DATABASE %s ; GRANT ALL PRIVILEGES ON %s.* TO \'%s\'@localhost IDENTIFIED BY \'%s\';"' % (mysql_root_pwd, db, db, user, password) if os.system(mysql_command) != 0: - raise MoulinetteError('mysql_db_creation_failed') + raise YunohostError('mysql_db_creation_failed') if sql is not None: if os.system('mysql -u %s -p%s %s < %s' % (user, password, db, sql)) != 0: - raise MoulinetteError('mysql_db_init_failed') + raise YunohostError('mysql_db_init_failed') if return_pwd: return password @@ -1437,7 +1437,7 @@ def app_ssowatconf(auth): def app_change_label(auth, app, new_label): installed = _is_installed(app) if not installed: - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) app_setting(app, "label", value=new_label) @@ -1473,7 +1473,7 @@ def app_action_run(app, action, args=None): actions = {x["id"]: x for x in actions} if action not in actions: - raise MoulinetteError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) + raise YunohostError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys()))) action_declaration = actions[action] @@ -1511,7 +1511,7 @@ def app_action_run(app, action, args=None): ) if retcode not in action_declaration.get("accepted_return_codes", [0]): - raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode)) + raise YunohostError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode)) os.remove(path) @@ -1618,7 +1618,7 @@ def app_config_apply(app, args): installed = _is_installed(app) if not installed: - raise MoulinetteError('app_not_installed', app=app) + raise YunohostError('app_not_installed', app=app) config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') @@ -1676,7 +1676,7 @@ def _get_app_settings(app_id): """ if not _is_installed(app_id): - raise MoulinetteError('app_not_installed', app=app_id) + raise YunohostError('app_not_installed', app=app_id) try: with open(os.path.join( APPS_SETTING_PATH, app_id, 'settings.yml')) as f: @@ -1714,7 +1714,7 @@ def _get_app_status(app_id, format_date=False): """ app_setting_path = APPS_SETTING_PATH + app_id if not os.path.isdir(app_setting_path): - raise MoulinetteError('app_unknown') + raise YunohostError('app_unknown') status = {} try: @@ -1779,7 +1779,7 @@ def _extract_app_from_file(path, remove=False): extract_result = 1 if extract_result != 0: - raise MoulinetteError('app_extraction_failed') + raise YunohostError('app_extraction_failed') try: extracted_app_folder = APP_TMP_FOLDER @@ -1790,9 +1790,9 @@ def _extract_app_from_file(path, remove=False): manifest = json.loads(str(json_manifest.read())) manifest['lastUpdate'] = int(time.time()) except IOError: - raise MoulinetteError('app_install_files_invalid') + raise YunohostError('app_install_files_invalid') except ValueError as e: - raise MoulinetteError('app_manifest_invalid', error=e.strerror) + raise YunohostError('app_manifest_invalid', error=e.strerror) logger.debug(m18n.n('done')) @@ -1860,7 +1860,7 @@ def _fetch_app_from_git(app): 'wget', '-qO', app_tmp_archive, tarball_url]) except subprocess.CalledProcessError: logger.exception('unable to download %s', tarball_url) - raise MoulinetteError('app_sources_fetch_failed') + raise YunohostError('app_sources_fetch_failed') else: manifest, extracted_app_folder = _extract_app_from_file( app_tmp_archive, remove=True) @@ -1883,9 +1883,9 @@ def _fetch_app_from_git(app): with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: - raise MoulinetteError('app_sources_fetch_failed') + raise YunohostError('app_sources_fetch_failed') except ValueError as e: - raise MoulinetteError('app_manifest_invalid', error=e.strerror) + raise YunohostError('app_manifest_invalid', error=e.strerror) else: logger.debug(m18n.n('done')) @@ -1905,10 +1905,10 @@ def _fetch_app_from_git(app): app_info['manifest']['lastUpdate'] = app_info['lastUpdate'] manifest = app_info['manifest'] else: - raise MoulinetteError('app_unknown') + raise YunohostError('app_unknown') if 'git' not in app_info: - raise MoulinetteError('app_unsupported_remote_type') + raise YunohostError('app_unsupported_remote_type') url = app_info['git']['url'] if 'github.com' in url: @@ -1920,7 +1920,7 @@ def _fetch_app_from_git(app): 'wget', '-qO', app_tmp_archive, tarball_url]) except subprocess.CalledProcessError: logger.exception('unable to download %s', tarball_url) - raise MoulinetteError('app_sources_fetch_failed') + raise YunohostError('app_sources_fetch_failed') else: manifest, extracted_app_folder = _extract_app_from_file( app_tmp_archive, remove=True) @@ -1936,9 +1936,9 @@ def _fetch_app_from_git(app): with open(extracted_app_folder + '/manifest.json') as f: manifest = json.loads(str(f.read())) except subprocess.CalledProcessError: - raise MoulinetteError('app_sources_fetch_failed') + raise YunohostError('app_sources_fetch_failed') except ValueError as e: - raise MoulinetteError('app_manifest_invalid', error=e.strerror) + raise YunohostError('app_manifest_invalid', error=e.strerror) else: logger.debug(m18n.n('done')) @@ -2057,7 +2057,7 @@ def _check_manifest_requirements(manifest, app_instance_name): yunohost_req = requirements.get('yunohost', None) if (not yunohost_req or not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'): - raise MoulinetteError('{0}{1}'.format( + raise YunohostError('{0}{1}'.format( m18n.g('colon', m18n.n('app_incompatible'), app=app_instance_name), m18n.n('app_package_need_update', app=app_instance_name))) elif not requirements: @@ -2070,13 +2070,13 @@ def _check_manifest_requirements(manifest, app_instance_name): versions = packages.get_installed_version( *requirements.keys(), strict=True, as_dict=True) except packages.PackageException as e: - raise MoulinetteError('app_requirements_failed', error=str(e), app=app_instance_name) + raise YunohostError('app_requirements_failed', error=str(e), app=app_instance_name) # Iterate over requirements for pkgname, spec in requirements.items(): version = versions[pkgname] if version not in packages.SpecifierSet(spec): - raise MoulinetteError( + raise YunohostError( errno.EINVAL, m18n.n('app_requirements_unmeet', pkgname=pkgname, version=version, spec=spec, app=app_instance_name)) @@ -2203,27 +2203,27 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): # Validate argument value if (arg_value is None or arg_value == '') \ and not arg.get('optional', False): - raise MoulinetteError('app_argument_required', name=arg_name) + raise YunohostError('app_argument_required', name=arg_name) elif arg_value is None: args_dict[arg_name] = '' continue # Validate argument choice if arg_choices and arg_value not in arg_choices: - raise MoulinetteError('app_argument_choice_invalid', name=arg_name, choices=', '.join(arg_choices)) + raise YunohostError('app_argument_choice_invalid', name=arg_name, choices=', '.join(arg_choices)) # Validate argument type if arg_type == 'domain': if arg_value not in domain_list(auth)['domains']: - raise MoulinetteError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown')) + raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown')) elif arg_type == 'user': try: user_info(auth, arg_value) - except MoulinetteError as e: - raise MoulinetteError('app_argument_invalid', name=arg_name, error=e.strerror) + except YunohostError as e: + raise YunohostError('app_argument_invalid', name=arg_name, error=e.strerror) elif arg_type == 'app': if not _is_installed(arg_value): - raise MoulinetteError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown')) + raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown')) elif arg_type == 'boolean': if isinstance(arg_value, bool): arg_value = 1 if arg_value else 0 @@ -2233,7 +2233,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): elif str(arg_value).lower() in ["0", "no", "n"]: arg_value = 0 else: - raise MoulinetteError('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0') + raise YunohostError('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0') elif arg_type == 'password': from yunohost.utils.password import assert_password_is_strong_enough assert_password_is_strong_enough('user', arg_value) @@ -2267,7 +2267,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): app_label=app_label, )) - raise MoulinetteError('app_location_unavailable', apps="\n".join(apps)) + raise YunohostError('app_location_unavailable', apps="\n".join(apps)) # (We save this normalized path so that the install script have a # standard path format to deal with no matter what the user inputted) @@ -2417,7 +2417,7 @@ def _read_appslist_list(): try: appslists = json.loads(appslists_json) except ValueError: - raise MoulinetteError('appslist_corrupted_json', filename=APPSLISTS_JSON) + raise YunohostError('appslist_corrupted_json', filename=APPSLISTS_JSON) return appslists @@ -2432,7 +2432,7 @@ def _write_appslist_list(appslist_lists): with open(APPSLISTS_JSON, "w") as f: json.dump(appslist_lists, f) except Exception as e: - raise MoulinetteError("Error while writing list of appslist %s: %s" % + raise YunohostError("Error while writing list of appslist %s: %s" % (APPSLISTS_JSON, str(e))) @@ -2446,13 +2446,13 @@ def _register_new_appslist(url, name): # Check if name conflicts with an existing list if name in appslist_list: - raise MoulinetteError('appslist_name_already_tracked', name=name) + raise YunohostError('appslist_name_already_tracked', name=name) # Check if url conflicts with an existing list known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()] if url in known_appslist_urls: - raise MoulinetteError('appslist_url_already_tracked', url=url) + raise YunohostError('appslist_url_already_tracked', url=url) logger.debug("Registering new appslist %s at %s" % (name, url)) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 08544a1a6..24313d416 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -38,7 +38,7 @@ from glob import glob from collections import OrderedDict from moulinette import msignals, m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils import filesystem from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file @@ -308,14 +308,14 @@ class BackupManager(): Ensure the working directory exists and is empty exception: - backup_output_directory_not_empty -- (MoulinetteError) Raised if the + backup_output_directory_not_empty -- (YunohostError) Raised if the directory was given by the user and isn't empty - (TODO) backup_cant_clean_tmp_working_directory -- (MoulinetteError) + (TODO) backup_cant_clean_tmp_working_directory -- (YunohostError) Raised if the working directory isn't empty, is temporary and can't be automaticcaly cleaned - (TODO) backup_cant_create_working_directory -- (MoulinetteError) Raised + (TODO) backup_cant_create_working_directory -- (YunohostError) Raised if iyunohost can't create the working directory """ @@ -327,7 +327,7 @@ class BackupManager(): logger.debug("temporary directory for backup '%s' already exists", self.work_dir) # FIXME May be we should clean the workdir here - raise MoulinetteError('backup_output_directory_not_empty') + raise YunohostError('backup_output_directory_not_empty') ########################################################################### # Backup target management # @@ -493,7 +493,7 @@ class BackupManager(): copied here Exceptions: - "backup_nothings_done" -- (MoulinetteError) This exception is raised if + "backup_nothings_done" -- (YunohostError) This exception is raised if nothing has been listed. """ @@ -506,7 +506,7 @@ class BackupManager(): if not successfull_apps and not successfull_system: filesystem.rm(self.work_dir, True, True) - raise MoulinetteError('backup_nothings_done') + raise YunohostError('backup_nothings_done') # Add unlisted files from backup tmp dir self._add_to_list_to_backup('backup.csv') @@ -856,7 +856,7 @@ class RestoreManager(): self.info["system"] = self.info["hooks"] except IOError: logger.debug("unable to load '%s'", info_file, exc_info=1) - raise MoulinetteError('backup_invalid_archive') + raise YunohostError('backup_invalid_archive') else: logger.debug("restoring from backup '%s' created on %s", self.name, datetime.utcfromtimestamp(self.info['created_at'])) @@ -879,7 +879,7 @@ class RestoreManager(): logger.debug("unable to retrieve current_host from the backup", exc_info=1) # FIXME include the current_host by default ? - raise MoulinetteError('backup_invalid_archive') + raise YunohostError('backup_invalid_archive') logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) @@ -1008,7 +1008,7 @@ class RestoreManager(): subprocess.call(['rmdir', self.work_dir]) logger.debug("Unmount dir: {}".format(self.work_dir)) else: - raise MoulinetteError('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) @@ -1016,7 +1016,7 @@ class RestoreManager(): if ret == 0: logger.debug("Delete dir: {}".format(self.work_dir)) else: - raise MoulinetteError('restore_removing_tmp_dir_failed') + raise YunohostError('restore_removing_tmp_dir_failed') filesystem.mkdir(self.work_dir, parents=True) @@ -1083,9 +1083,9 @@ class RestoreManager(): return True elif free_space > needed_space: # TODO Add --force options to avoid the error raising - raise MoulinetteError('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 MoulinetteError('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) # @@ -1135,7 +1135,7 @@ class RestoreManager(): newlines.append(row) except (IOError, OSError, csv.Error) as e: - raise MoulinetteError('error_reading_file', file=backup_csv, error=str(e)) + raise YunohostError('error_reading_file', file=backup_csv, error=str(e)) if not contains_php5: return @@ -1428,7 +1428,7 @@ class BackupMethod(object): @property def method_name(self): """Return the string name of a BackupMethod (eg "tar" or "copy")""" - raise MoulinetteError('backup_abstract_method') + raise YunohostError('backup_abstract_method') @property def name(self): @@ -1510,7 +1510,7 @@ class BackupMethod(object): """ if self.need_mount(): if self._recursive_umount(self.work_dir) > 0: - raise MoulinetteError('backup_cleaning_failed') + raise YunohostError('backup_cleaning_failed') if self.manager.is_tmp_work_dir: filesystem.rm(self.work_dir, True, True) @@ -1552,7 +1552,7 @@ class BackupMethod(object): if free_space < backup_size: logger.debug('Not enough space at %s (free: %s / needed: %d)', self.repo, free_space, backup_size) - raise MoulinetteError('not_enough_disk_space', path=self.repo) + raise YunohostError('not_enough_disk_space', path=self.repo) def _organize_files(self): """ @@ -1640,10 +1640,10 @@ class BackupMethod(object): i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', answers='y/N', size=str(size))) except NotImplemented: - raise MoulinetteError('backup_unable_to_organize_files') + raise YunohostError('backup_unable_to_organize_files') else: if i != 'y' and i != 'Y': - raise MoulinetteError('backup_unable_to_organize_files') + raise YunohostError('backup_unable_to_organize_files') # Copy unbinded path logger.debug(m18n.n('backup_copying_to_organize_the_archive', @@ -1733,7 +1733,7 @@ class CopyBackupMethod(BackupMethod): super(CopyBackupMethod, self).mount() if not os.path.isdir(self.repo): - raise MoulinetteError('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, @@ -1744,7 +1744,7 @@ class CopyBackupMethod(BackupMethod): logger.warning(m18n.n("bind_mouting_disable")) subprocess.call(["mountpoint", "-q", dest, "&&", "umount", "-R", dest]) - raise MoulinetteError('backup_cant_mount_uncompress_archive') + raise YunohostError('backup_cant_mount_uncompress_archive') class TarBackupMethod(BackupMethod): @@ -1789,7 +1789,7 @@ class TarBackupMethod(BackupMethod): except: logger.debug("unable to open '%s' for writing", self._archive_file, exc_info=1) - raise MoulinetteError('backup_archive_open_failed') + raise YunohostError('backup_archive_open_failed') # Add files to the archive try: @@ -1800,7 +1800,7 @@ class TarBackupMethod(BackupMethod): tar.close() except IOError: logger.error(m18n.n('backup_archive_writing_error'), exc_info=1) - raise MoulinetteError('backup_creation_failed') + raise YunohostError('backup_creation_failed') # Move info file shutil.copy(os.path.join(self.work_dir, 'info.json'), @@ -1828,7 +1828,7 @@ class TarBackupMethod(BackupMethod): except: logger.debug("cannot open backup archive '%s'", self._archive_file, exc_info=1) - raise MoulinetteError('backup_archive_open_failed') + raise YunohostError('backup_archive_open_failed') tar.close() # Mount the tarball @@ -1889,10 +1889,10 @@ class BorgBackupMethod(BackupMethod): super(CopyBackupMethod, self).backup() # TODO run borg create command - raise MoulinetteError('backup_borg_not_implemented') + raise YunohostError('backup_borg_not_implemented') def mount(self, mnt_path): - raise MoulinetteError('backup_borg_not_implemented') + raise YunohostError('backup_borg_not_implemented') class CustomBackupMethod(BackupMethod): @@ -1938,7 +1938,7 @@ class CustomBackupMethod(BackupMethod): ret = hook_callback('backup_method', [self.method], args=self._get_args('backup')) if ret['failed']: - raise MoulinetteError('backup_custom_backup_error') + raise YunohostError('backup_custom_backup_error') def mount(self, restore_manager): """ @@ -1951,7 +1951,7 @@ class CustomBackupMethod(BackupMethod): ret = hook_callback('backup_method', [self.method], args=self._get_args('mount')) if ret['failed']: - raise MoulinetteError('backup_custom_mount_error') + raise YunohostError('backup_custom_mount_error') def _get_args(self, action): """Return the arguments to give to the custom script""" @@ -1987,7 +1987,7 @@ 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 MoulinetteError('backup_archive_name_exists') + raise YunohostError('backup_archive_name_exists') # Validate output_directory option if output_directory: @@ -1997,14 +1997,14 @@ def backup_create(name=None, description=None, methods=[], if output_directory.startswith(ARCHIVES_PATH) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): - raise MoulinetteError('backup_output_directory_forbidden') + raise YunohostError('backup_output_directory_forbidden') # Check that output directory is empty if os.path.isdir(output_directory) and no_compress and \ os.listdir(output_directory): - raise MoulinetteError('backup_output_directory_not_empty') + raise YunohostError('backup_output_directory_not_empty') elif no_compress: - raise MoulinetteError('backup_output_directory_required') + raise YunohostError('backup_output_directory_required') # Define methods (retro-compat) if not methods: @@ -2101,7 +2101,7 @@ def backup_restore(auth, name, system=[], apps=[], force=False): if i == 'y' or i == 'Y': force = True if not force: - raise MoulinetteError('restore_failed') + raise YunohostError('restore_failed') # TODO Partial app restore could not work if ldap is not restored before # TODO repair mysql if broken and it's a complete restore @@ -2128,7 +2128,7 @@ def backup_restore(auth, name, system=[], apps=[], force=False): if restore_manager.success: logger.success(m18n.n('restore_complete')) else: - raise MoulinetteError('restore_nothings_done') + raise YunohostError('restore_nothings_done') return restore_manager.targets.results @@ -2164,7 +2164,7 @@ def backup_list(with_info=False, human_readable=False): for a in result: try: d[a] = backup_info(a, human_readable=human_readable) - except MoulinetteError, e: + except YunohostError, e: logger.warning('%s: %s' % (a, e.strerror)) result = d @@ -2186,7 +2186,7 @@ def backup_info(name, with_details=False, human_readable=False): # Check file exist (even if it's a broken symlink) if not os.path.lexists(archive_file): - raise MoulinetteError('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): @@ -2194,7 +2194,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 MoulinetteError('backup_archive_broken_link', + raise YunohostError('backup_archive_broken_link', path=archive_file) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) @@ -2207,7 +2207,7 @@ def backup_info(name, with_details=False, human_readable=False): except KeyError: logger.debug("unable to retrieve '%s' inside the archive", info_file, exc_info=1) - raise MoulinetteError('backup_invalid_archive') + raise YunohostError('backup_invalid_archive') else: shutil.move(os.path.join(info_dir, 'info.json'), info_file) finally: @@ -2220,7 +2220,7 @@ 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 MoulinetteError('backup_invalid_archive') + raise YunohostError('backup_invalid_archive') # Retrieve backup size size = info.get('size', 0) @@ -2259,7 +2259,7 @@ def backup_delete(name): """ if name not in backup_list()["archives"]: - raise MoulinetteError('backup_archive_name_unknown', + raise YunohostError('backup_archive_name_unknown', name=name) hook_callback('pre_backup_delete', args=[name]) @@ -2287,7 +2287,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 MoulinetteError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH) + raise YunohostError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH) os.mkdir(ARCHIVES_PATH, 0750) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e75aebcc7..ca27d59df 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -37,7 +37,7 @@ from datetime import datetime from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.utils.network import get_public_ip @@ -106,7 +106,7 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: # Is it in Yunohost domain list? if domain not in yunohost_domains_list: - raise MoulinetteError('certmanager_domain_unknown', domain=domain) + raise YunohostError('certmanager_domain_unknown', domain=domain) certificates = {} @@ -171,7 +171,7 @@ def _certificate_install_selfsigned(domain_list, force=False): status = _get_status(domain) if status["summary"]["code"] in ('good', 'great'): - raise MoulinetteError('certmanager_attempt_to_replace_valid_cert', domain=domain) + raise YunohostError('certmanager_attempt_to_replace_valid_cert', domain=domain) operation_logger.start() @@ -201,7 +201,7 @@ def _certificate_install_selfsigned(domain_list, force=False): if p.returncode != 0: logger.warning(out) - raise MoulinetteError('domain_cert_gen_failed') + raise YunohostError('domain_cert_gen_failed') else: logger.debug(out) @@ -259,12 +259,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F for domain in domain_list: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] if domain not in yunohost_domains_list: - raise MoulinetteError('certmanager_domain_unknown', domain=domain) + raise YunohostError('certmanager_domain_unknown', domain=domain) # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise MoulinetteError('certmanager_domain_cert_not_selfsigned', domain=domain) + raise YunohostError('certmanager_domain_cert_not_selfsigned', domain=domain) if staging: logger.warning( @@ -344,21 +344,21 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Is it in Yunohost dmomain list? if domain not in yunohost.domain.domain_list(auth)['domains']: - raise MoulinetteError('certmanager_domain_unknown', domain=domain) + raise YunohostError('certmanager_domain_unknown', domain=domain) status = _get_status(domain) # Does it expire soon? if status["validity"] > VALIDITY_LIMIT and not force: - raise MoulinetteError('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 MoulinetteError('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 MoulinetteError('certmanager_acme_not_configured_for_domain', domain=domain) + raise YunohostError('certmanager_acme_not_configured_for_domain', domain=domain) if staging: logger.warning( @@ -475,7 +475,7 @@ location ^~ '/.well-known/acme-challenge/' contents = f.read() if '/.well-known/acme-challenge' in contents: - raise MoulinetteError('certmanager_conflicting_nginx_file', filepath=path) + raise YunohostError('certmanager_conflicting_nginx_file', filepath=path) # Write the conf if os.path.exists(nginx_conf_file): @@ -553,22 +553,22 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): CA=certification_authority) except ValueError as e: if "urn:acme:error:rateLimited" in str(e): - raise MoulinetteError('certmanager_hit_rate_limit', domain=domain) + raise YunohostError('certmanager_hit_rate_limit', domain=domain) else: logger.error(str(e)) _display_debug_information(domain) - raise MoulinetteError('certmanager_cert_signing_failed') + raise YunohostError('certmanager_cert_signing_failed') except Exception as e: logger.error(str(e)) - raise MoulinetteError('certmanager_cert_signing_failed') + raise YunohostError('certmanager_cert_signing_failed') import requests # lazy loading this module for performance reasons try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text except requests.exceptions.Timeout as e: - raise MoulinetteError('certmanager_couldnt_fetch_intermediate_cert') + raise YunohostError('certmanager_couldnt_fetch_intermediate_cert') # Now save the key and signed certificate logger.debug("Saving the key and signed certificate...") @@ -611,7 +611,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): status_summary = _get_status(domain)["summary"] if status_summary["code"] != "great": - raise MoulinetteError('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): @@ -644,7 +644,7 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - raise MoulinetteError('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: @@ -653,7 +653,7 @@ def _get_status(domain): except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) - raise MoulinetteError('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 @@ -814,11 +814,11 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain) + raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain) # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError('certmanager_domain_http_not_working', domain=domain) + raise YunohostError('certmanager_domain_http_not_working', domain=domain) def _get_dns_ip(domain): @@ -827,7 +827,7 @@ def _get_dns_ip(domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError('certmanager_error_no_A_record', domain=domain) + raise YunohostError('certmanager_error_no_A_record', domain=domain) return str(answers[0]) diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 188c1dffc..e81b20e5f 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -7,7 +7,7 @@ import json import errno from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration @@ -29,7 +29,7 @@ class MyMigration(Migration): try: (domain, private_key_path) = _guess_current_dyndns_domain(dyn_host) assert "+157" in private_key_path - except (MoulinetteError, AssertionError): + except (YunohostError, AssertionError): logger.info(m18n.n("migrate_tsig_not_needed")) return @@ -52,7 +52,7 @@ class MyMigration(Migration): 'public_key_sha512': base64.b64encode(public_key_sha512), }, timeout=30) except requests.ConnectionError: - raise MoulinetteError('no_internet_connection') + raise YunohostError('no_internet_connection') if r.status_code != 201: try: @@ -70,7 +70,7 @@ class MyMigration(Migration): # Migration didn't succeed, so we rollback and raise an exception os.system("mv /etc/yunohost/dyndns/*+165* /tmp") - raise MoulinetteError('migrate_tsig_failed', domain=domain, + raise YunohostError('migrate_tsig_failed', domain=domain, error_code=str(r.status_code), error=error) # remove old certificates diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 474a26db5..80d182fbe 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -3,7 +3,7 @@ import os from shutil import copy2 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.process import check_output, call_async_output from moulinette.utils.filesystem import read_file @@ -30,7 +30,7 @@ class MyMigration(Migration): def backward(self): - raise MoulinetteError("migration_0003_backward_impossible") + raise YunohostError("migration_0003_backward_impossible") def migrate(self): @@ -57,7 +57,7 @@ class MyMigration(Migration): self.apt_dist_upgrade(conf_flags=["old", "miss", "def"]) _run_service_command("start", "mysql") if self.debian_major_version() == 8: - raise MoulinetteError("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile) + raise YunohostError("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile) # Specific upgrade for fail2ban... logger.info(m18n.n("migration_0003_fail2ban_upgrade")) @@ -107,11 +107,11 @@ class MyMigration(Migration): # would still be in 2.x... if not self.debian_major_version() == 8 \ and not self.yunohost_major_version() == 2: - raise MoulinetteError("migration_0003_not_jessie") + raise YunohostError("migration_0003_not_jessie") # Have > 1 Go free space on /var/ ? if free_space_in_directory("/var/") / (1024**3) < 1.0: - raise MoulinetteError("migration_0003_not_enough_free_space") + raise YunohostError("migration_0003_not_enough_free_space") # Check system is up to date # (but we don't if 'stretch' is already in the sources.list ... @@ -120,7 +120,7 @@ class MyMigration(Migration): self.apt_update() apt_list_upgradable = check_output("apt list --upgradable -a") if "upgradable" in apt_list_upgradable: - raise MoulinetteError("migration_0003_system_not_fully_up_to_date") + raise YunohostError("migration_0003_system_not_fully_up_to_date") @property def disclaimer(self): diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index f03a93ef9..0208b717e 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -1,7 +1,7 @@ import subprocess from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration @@ -20,10 +20,10 @@ class MyMigration(Migration): return if not self.package_is_installed("postgresql-9.6"): - raise MoulinetteError("migration_0005_postgresql_96_not_installed") + raise YunohostError("migration_0005_postgresql_96_not_installed") if not space_used_by_directory("/var/lib/postgresql/9.4") > free_space_in_directory("/var/lib/postgresql"): - raise MoulinetteError("migration_0005_not_enough_space", path="/var/lib/postgresql/") + raise YunohostError("migration_0005_not_enough_space", path="/var/lib/postgresql/") subprocess.check_call("service postgresql stop", shell=True) subprocess.check_call("pg_dropcluster --stop 9.6 main", shell=True) diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index ee3aeefcb..1149f28d5 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -5,7 +5,7 @@ import string import subprocess from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from moulinette.utils.process import run_commands, check_output from moulinette.utils.filesystem import append_to_file diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 7be214914..e28a24afe 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -30,7 +30,7 @@ import yaml import errno from moulinette import m18n, msettings -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger import yunohost.certificate @@ -77,8 +77,8 @@ def domain_add(operation_logger, auth, domain, dyndns=False): try: auth.validate_uniqueness({'virtualdomain': domain}) - except MoulinetteError: - raise MoulinetteError('domain_exists') + except YunohostError: + raise YunohostError('domain_exists') operation_logger.start() @@ -87,14 +87,14 @@ def domain_add(operation_logger, auth, domain, dyndns=False): # Do not allow to subscribe to multiple dyndns domains... if os.path.exists('/etc/cron.d/yunohost-dyndns'): - raise MoulinetteError('domain_dyndns_already_subscribed') + 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 MoulinetteError('domain_dyndns_root_unknown') + raise YunohostError('domain_dyndns_root_unknown') # Actually subscribe dyndns_subscribe(domain=domain) @@ -108,7 +108,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False): } if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict): - raise MoulinetteError('domain_creation_failed') + raise YunohostError('domain_creation_failed') # Don't regen these conf if we're still in postinstall if os.path.exists('/etc/yunohost/installed'): @@ -145,11 +145,11 @@ def domain_remove(operation_logger, auth, domain, force=False): from yunohost.app import app_ssowatconf if not force and domain not in domain_list(auth)['domains']: - raise MoulinetteError('domain_unknown') + raise YunohostError('domain_unknown') # Check domain is not the main domain if domain == _get_maindomain(): - raise MoulinetteError('domain_cannot_remove_main') + raise YunohostError('domain_cannot_remove_main') # Check if apps are installed on the domain for app in os.listdir('/etc/yunohost/apps/'): @@ -160,13 +160,13 @@ def domain_remove(operation_logger, auth, domain, force=False): continue else: if app_domain == domain: - raise MoulinetteError('domain_uninstall_app_first') + raise YunohostError('domain_uninstall_app_first') operation_logger.start() if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: os.system('rm -rf /etc/yunohost/certs/%s' % domain) else: - raise MoulinetteError('domain_deletion_failed') + raise YunohostError('domain_deletion_failed') service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) @@ -243,7 +243,7 @@ def _get_conflicting_apps(auth, domain, path): # Abort if domain is unknown if domain not in domain_list(auth)['domains']: - raise MoulinetteError('domain_unknown') + raise YunohostError('domain_unknown') # This import cannot be put on top of file because it would create a # recursive import... diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 93f3a7a38..13dd4b43d 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -33,7 +33,7 @@ import errno import subprocess from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, rm from moulinette.utils.network import download_json @@ -75,9 +75,9 @@ def _dyndns_provides(provider, domain): # Dyndomains will be a list of domains supported by the provider # e.g. [ "nohost.me", "noho.st" ] dyndomains = download_json('https://%s/domains' % provider, timeout=30) - except MoulinetteError as e: + except YunohostError as e: logger.error(str(e)) - raise MoulinetteError('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:]) @@ -102,9 +102,9 @@ def _dyndns_available(provider, domain): try: r = download_json('https://%s/test/%s' % (provider, domain), expected_status_code=None) - except MoulinetteError as e: + except YunohostError as e: logger.error(str(e)) - raise MoulinetteError('dyndns_could_not_check_available', + raise YunohostError('dyndns_could_not_check_available', domain=domain, provider=provider) return r == u"Domain %s is available" % domain @@ -127,11 +127,11 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom # Verify if domain is provided by subscribe_host if not _dyndns_provides(subscribe_host, domain): - raise MoulinetteError('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 MoulinetteError('dyndns_unavailable', domain=domain) + raise YunohostError('dyndns_unavailable', domain=domain) operation_logger.start() @@ -155,13 +155,13 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom try: r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) except requests.ConnectionError: - raise MoulinetteError('no_internet_connection') + raise YunohostError('no_internet_connection') if r.status_code != 201: try: error = json.loads(r.text)['error'] except: error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text) - raise MoulinetteError('dyndns_registration_failed', error=error) + raise YunohostError('dyndns_registration_failed', error=error) logger.success(m18n.n('dyndns_registered')) @@ -195,7 +195,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) if not keys: - raise MoulinetteError('dyndns_key_not_found') + raise YunohostError('dyndns_key_not_found') key = keys[0] @@ -295,7 +295,7 @@ 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 MoulinetteError('dyndns_ip_update_failed') + raise YunohostError('dyndns_ip_update_failed') logger.success(m18n.n('dyndns_ip_updated')) @@ -321,7 +321,7 @@ def dyndns_removecron(): try: os.remove("/etc/cron.d/yunohost-dyndns") except: - raise MoulinetteError('dyndns_cron_remove_failed') + raise YunohostError('dyndns_cron_remove_failed') logger.success(m18n.n('dyndns_cron_removed')) @@ -351,4 +351,4 @@ def _guess_current_dyndns_domain(dyn_host): else: return (_domain, path) - raise MoulinetteError('dyndns_no_domain_registered') + raise YunohostError('dyndns_no_domain_registered') diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index 878083bc7..fb1804659 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -34,7 +34,7 @@ except ImportError: sys.exit(1) from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils import process from moulinette.utils.log import getActionLogger from moulinette.utils.text import prependlines @@ -268,7 +268,7 @@ def firewall_reload(skip_upnp=False): reloaded = True if not reloaded: - raise MoulinetteError('firewall_reload_failed') + raise YunohostError('firewall_reload_failed') hook_callback('post_iptable_rules', args=[upnp, os.path.exists("/proc/net/if_inet6")]) @@ -338,7 +338,7 @@ def firewall_upnp(action='status', no_refresh=False): if action == 'status': no_refresh = True else: - raise MoulinetteError('action_invalid', action=action) + raise YunohostError('action_invalid', action=action) # Refresh port mapping using UPnP if not no_refresh: @@ -407,7 +407,7 @@ def firewall_upnp(action='status', no_refresh=False): firewall_reload(skip_upnp=True) if action == 'enable' and not enabled: - raise MoulinetteError('upnp_port_open_failed') + raise YunohostError('upnp_port_open_failed') return {'enabled': enabled} @@ -419,7 +419,7 @@ def firewall_stop(): """ if os.system("iptables -w -P INPUT ACCEPT") != 0: - raise MoulinetteError('iptables_unavailable') + raise YunohostError('iptables_unavailable') os.system("iptables -w -F") os.system("iptables -w -X") diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 665d2fe40..901321926 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -30,7 +30,7 @@ import tempfile from glob import iglob from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils import log HOOK_FOLDER = '/usr/share/yunohost/hooks/' @@ -112,7 +112,7 @@ def hook_info(action, name): }) if not hooks: - raise MoulinetteError('hook_name_unknown', name=name) + raise YunohostError('hook_name_unknown', name=name) return { 'action': action, 'name': name, @@ -174,7 +174,7 @@ def hook_list(action, list_by='name', show_info=False): # Add only the name d.add(name) else: - raise MoulinetteError('hook_list_by_invalid') + raise YunohostError('hook_list_by_invalid') def _append_folder(d, folder): # Iterate over and add hook from a folder @@ -255,7 +255,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, try: hl = hooks_names[n] except KeyError: - raise MoulinetteError('hook_name_unknown', n) + raise YunohostError('hook_name_unknown', n) # Iterate over hooks with this name for h in hl: # Update hooks dict @@ -281,7 +281,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None, path=path, args=args) hook_exec(path, args=hook_args, chdir=chdir, env=env, no_trace=no_trace, raise_on_error=True) - except MoulinetteError as e: + except YunohostError as e: state = 'failed' logger.error(e.strerror, exc_info=1) post_callback(name=name, priority=priority, path=path, @@ -318,7 +318,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, if path[0] != '/': path = os.path.realpath(path) if not os.path.isfile(path): - raise MoulinetteError('file_not_exist', path=path) + raise YunohostError('file_not_exist', path=path) # Construct command variables cmd_args = '' @@ -383,12 +383,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, # Check and return process' return code if returncode is None: if raise_on_error: - raise MoulinetteError('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)) return 1 elif raise_on_error and returncode != 0: - raise MoulinetteError('hook_exec_failed', path=path) + raise YunohostError('hook_exec_failed', path=path) return returncode diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 3df630b14..3c7ff02f3 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -34,7 +34,7 @@ from logging import FileHandler, getLogger, Formatter from sys import exc_info 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 read_file @@ -148,7 +148,7 @@ def log_display(path, number=50, share=False): log_path = base_path + LOG_FILE_EXT if not os.path.exists(md_path) and not os.path.exists(log_path): - raise MoulinetteError('log_does_exists', log=path) + raise YunohostError('log_does_exists', log=path) infos = {} @@ -188,7 +188,7 @@ def log_display(path, number=50, share=False): if os.path.exists(log_path): logger.warning(error) else: - raise MoulinetteError(error) + raise YunohostError(error) # Display logs if exist if os.path.exists(log_path): diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 0839c6ae7..4fef2ca61 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -38,7 +38,7 @@ import cPickle as pickle from datetime import datetime from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.utils.network import get_public_ip @@ -83,7 +83,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): result_dname = dn if len(devices) == 0: if mountpoint is not None: - raise MoulinetteError('mountpoint_unknown') + raise YunohostError('mountpoint_unknown') return result # Retrieve monitoring for unit(s) @@ -141,7 +141,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False): for dname in devices_names: _set(dname, 'not-available') else: - raise MoulinetteError('unit_unknown', unit=u) + raise YunohostError('unit_unknown', unit=u) if result_dname is not None: return result[result_dname] @@ -237,7 +237,7 @@ def monitor_network(units=None, human_readable=False): 'gateway': gateway, } else: - raise MoulinetteError('unit_unknown', unit=u) + raise YunohostError('unit_unknown', unit=u) if len(units) == 1: return result[units[0]] @@ -287,7 +287,7 @@ def monitor_system(units=None, human_readable=False): elif u == 'infos': result[u] = json.loads(glances.getSystem()) else: - raise MoulinetteError('unit_unknown', unit=u) + raise YunohostError('unit_unknown', unit=u) if len(units) == 1 and type(result[units[0]]) is not str: return result[units[0]] @@ -303,7 +303,7 @@ def monitor_update_stats(period): """ if period not in ['day', 'week', 'month']: - raise MoulinetteError('monitor_period_invalid') + raise YunohostError('monitor_period_invalid') stats = _retrieve_stats(period) if not stats: @@ -321,7 +321,7 @@ def monitor_update_stats(period): else: monitor = _monitor_all(p, 0) if not monitor: - raise MoulinetteError('monitor_stats_no_update') + raise YunohostError('monitor_stats_no_update') stats['timestamp'].append(time.time()) @@ -386,13 +386,13 @@ def monitor_show_stats(period, date=None): """ if period not in ['day', 'week', 'month']: - raise MoulinetteError('monitor_period_invalid') + raise YunohostError('monitor_period_invalid') result = _retrieve_stats(period, date) if result is False: - raise MoulinetteError('monitor_stats_file_not_found') + raise YunohostError('monitor_stats_file_not_found') elif result is None: - raise MoulinetteError('monitor_stats_period_unavailable') + raise YunohostError('monitor_stats_period_unavailable') return result @@ -440,7 +440,7 @@ def monitor_disable(): if glances['loaded'] != 'disabled': try: service_disable('glances') - except MoulinetteError as e: + except YunohostError as e: logger.warning(e.strerror) # Remove crontab @@ -468,8 +468,8 @@ def _get_glances_api(): from yunohost.service import service_status if service_status('glances')['status'] != 'running': - raise MoulinetteError('monitor_not_enabled') - raise MoulinetteError('monitor_glances_con_failed') + raise YunohostError('monitor_not_enabled') + raise YunohostError('monitor_glances_con_failed') def _extract_inet(string, skip_netmask=False, skip_loopback=True): diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 140c107be..082b32eab 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -36,7 +36,7 @@ from difflib import unified_diff from datetime import datetime from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils import log, filesystem from yunohost.log import is_unit_operation @@ -85,7 +85,7 @@ def service_add(name, status=None, log=None, runlevel=None, need_lock=False, des _save_services(services) except: # we'll get a logger.warning with more details in _save_services - raise MoulinetteError('service_add_failed', service=name) + raise YunohostError('service_add_failed', service=name) logger.success(m18n.n('service_added', service=name)) @@ -103,13 +103,13 @@ def service_remove(name): try: del services[name] except KeyError: - raise MoulinetteError('service_unknown', service=name) + raise YunohostError('service_unknown', service=name) try: _save_services(services) except: # we'll get a logger.warning with more details in _save_services - raise MoulinetteError('service_remove_failed', service=name) + raise YunohostError('service_remove_failed', service=name) logger.success(m18n.n('service_removed', service=name)) @@ -130,7 +130,7 @@ def service_start(names): logger.success(m18n.n('service_started', service=name)) else: if service_status(name)['status'] != 'running': - raise MoulinetteError('service_start_failed', service=name, logs=_get_journalctl_logs(name)) + raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name)) logger.debug(m18n.n('service_already_started', service=name)) @@ -149,7 +149,7 @@ def service_stop(names): logger.success(m18n.n('service_stopped', service=name)) else: if service_status(name)['status'] != 'inactive': - raise MoulinetteError('service_stop_failed', service=name, logs=_get_journalctl_logs(name)) + raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name)) logger.debug(m18n.n('service_already_stopped', service=name)) @is_unit_operation() @@ -168,7 +168,7 @@ def service_enable(operation_logger, names): if _run_service_command('enable', name): logger.success(m18n.n('service_enabled', service=name)) else: - raise MoulinetteError('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): @@ -185,7 +185,7 @@ def service_disable(names): if _run_service_command('disable', name): logger.success(m18n.n('service_disabled', service=name)) else: - raise MoulinetteError('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=[]): @@ -208,7 +208,7 @@ def service_status(names=[]): for name in names: if check_names and name not in services.keys(): - raise MoulinetteError('service_unknown', service=name) + raise YunohostError('service_unknown', service=name) # this "service" isn't a service actually so we skip it # @@ -307,10 +307,10 @@ def service_log(name, number=50): services = _get_services() if name not in services.keys(): - raise MoulinetteError('service_unknown', service=name) + raise YunohostError('service_unknown', service=name) if 'log' not in services[name]: - raise MoulinetteError('service_no_log', service=name) + raise YunohostError('service_no_log', service=name) log_list = services[name]['log'] @@ -418,7 +418,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, names = pre_result['succeed'].keys() if not names: - raise MoulinetteError('service_regenconf_failed', + raise YunohostError('service_regenconf_failed', services=', '.join(pre_result['failed'])) # Set the processing method @@ -595,7 +595,7 @@ def _run_service_command(action, service): """ services = _get_services() if service not in services.keys(): - raise MoulinetteError('service_unknown', service=service) + raise YunohostError('service_unknown', service=service) possible_actions = ['start', 'stop', 'restart', 'reload', 'enable', 'disable'] if action not in possible_actions: diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 5d875afb9..8783188a1 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 moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger logger = getActionLogger('yunohost.settings') @@ -54,7 +54,7 @@ def settings_get(key, full=False): settings = _get_settings() if key not in settings: - raise MoulinetteError('global_settings_key_doesnt_exists', settings_key=key) + raise YunohostError('global_settings_key_doesnt_exists', settings_key=key) if full: return settings[key] @@ -82,13 +82,13 @@ def settings_set(key, value): settings = _get_settings() if key not in settings: - raise MoulinetteError('global_settings_key_doesnt_exists', settings_key=key) + raise YunohostError('global_settings_key_doesnt_exists', settings_key=key) key_type = settings[key]["type"] if key_type == "bool": if not isinstance(value, bool): - raise MoulinetteError('global_settings_bad_type_for_setting', setting=key, + 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): @@ -100,19 +100,19 @@ def settings_set(key, value): 'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)) else: - raise MoulinetteError('global_settings_bad_type_for_setting', setting=key, + 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): - raise MoulinetteError('global_settings_bad_type_for_setting', setting=key, + 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 MoulinetteError('global_settings_bad_choice_for_enum', setting=key, + raise YunohostError('global_settings_bad_choice_for_enum', setting=key, received_type=type(value).__name__, expected_type=", ".join(settings[key]["choices"])) else: - raise MoulinetteError('global_settings_unknown_type', setting=key, + raise YunohostError('global_settings_unknown_type', setting=key, unknown_type=key_type) settings[key]["value"] = value @@ -131,7 +131,7 @@ def settings_reset(key): settings = _get_settings() if key not in settings: - raise MoulinetteError('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) @@ -207,7 +207,7 @@ def _get_settings(): setting_key=key)) unknown_settings[key] = value except Exception as e: - raise MoulinetteError('global_settings_cant_open_settings', reason=e, exc_info=1) + raise YunohostError('global_settings_cant_open_settings', reason=e, exc_info=1) if unknown_settings: try: @@ -228,10 +228,10 @@ def _save_settings(settings, location=SETTINGS_PATH): try: result = json.dumps(settings_without_description, indent=4) except Exception as e: - raise MoulinetteError('global_settings_cant_serialize_settings', reason=e, exc_info=1) + raise YunohostError('global_settings_cant_serialize_settings', reason=e, exc_info=1) try: with open(location, "w") as settings_fd: settings_fd.write(result) except Exception as e: - raise MoulinetteError('global_settings_cant_write_settings', reason=e, exc_info=1) + raise YunohostError('global_settings_cant_write_settings', reason=e, exc_info=1) diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index 41ac64293..e284736c4 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -7,7 +7,7 @@ import pwd import subprocess from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" @@ -23,7 +23,7 @@ def user_ssh_allow(auth, username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(auth, username): - raise MoulinetteError('user_unknown', user=username) + raise YunohostError('user_unknown', user=username) auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'}) @@ -42,7 +42,7 @@ def user_ssh_disallow(auth, username): # TODO it would be good to support different kind of shells if not _get_user_for_ssh(auth, username): - raise MoulinetteError('user_unknown', user=username) + raise YunohostError('user_unknown', user=username) auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'}) diff --git a/src/yunohost/tests/test_appslist.py b/src/yunohost/tests/test_appslist.py index 6b7141f4a..7a6f27d6c 100644 --- a/src/yunohost/tests/test_appslist.py +++ b/src/yunohost/tests/test_appslist.py @@ -5,7 +5,7 @@ import requests_mock import glob import time -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from yunohost.app import app_fetchlist, app_removelist, app_listlists, _using_legacy_appslist_system, _migrate_appslist_system, _register_new_appslist @@ -79,7 +79,7 @@ def test_appslist_list_register_conflict_name(): """ _register_new_appslist("https://lol.com/appslist.json", "dummy") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): _register_new_appslist("https://lol.com/appslist2.json", "dummy") appslist_dict = app_listlists() @@ -94,7 +94,7 @@ def test_appslist_list_register_conflict_url(): """ _register_new_appslist("https://lol.com/appslist.json", "dummy") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): _register_new_appslist("https://lol.com/appslist.json", "plopette") appslist_dict = app_listlists() @@ -161,7 +161,7 @@ def test_appslist_fetch_unknownlist(): assert app_listlists() == {} - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_fetchlist(name="swag") @@ -170,7 +170,7 @@ def test_appslist_fetch_url_but_no_name(): Do a fetchlist with url given, but no name given """ - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_fetchlist(url=URL_OFFICIAL_APP_LIST) @@ -270,7 +270,7 @@ def test_appslist_remove_unknown(): Attempt to remove an unknown list """ - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_removelist("dummy") diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index dc1dbc29b..58c345dea 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -1,6 +1,6 @@ import pytest -from moulinette.core import MoulinetteError, init_authenticator +from yunohost.utils.error import YunohostError, init_authenticator from yunohost.app import app_install, app_remove from yunohost.domain import _get_maindomain, domain_url_available, _normalize_domain_path @@ -43,7 +43,7 @@ def test_urlavailable(): assert domain_url_available(auth, maindomain, "/macnuggets") # We don't know the domain yolo.swag - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): assert domain_url_available(auth, "yolo.swag", "/macnuggets") @@ -55,13 +55,13 @@ def test_registerurl(): assert not domain_url_available(auth, maindomain, "/urlregisterapp") # Try installing at same location - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_install(auth, "./tests/apps/register_url_app_ynh", args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) def test_registerurl_baddomain(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_install(auth, "./tests/apps/register_url_app_ynh", args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp")) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index c9d4c6b15..abe5bb1c8 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -12,7 +12,7 @@ from yunohost.app import app_install, app_remove, app_ssowatconf from yunohost.app import _is_installed from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete from yunohost.domain import _get_maindomain -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError # Get main domain maindomain = "" @@ -214,7 +214,7 @@ def test_backup_system_part_that_does_not_exists(mocker): mocker.spy(m18n, "n") # Create the backup - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_create(system=["yolol"], apps=None) m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") @@ -295,7 +295,7 @@ def test_backup_script_failure_handling(monkeypatch, mocker): monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app') @@ -315,7 +315,7 @@ def test_backup_not_enough_free_space(monkeypatch, mocker): mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_create(system=None, apps=["backup_recommended_app"]) m18n.n.assert_any_call('not_enough_disk_space', path=ANY) @@ -327,7 +327,7 @@ def test_backup_app_not_installed(mocker): mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_create(system=None, apps=["wordpress"]) m18n.n.assert_any_call("unbackup_app", app="wordpress") @@ -343,8 +343,9 @@ def test_backup_app_with_no_backup_script(mocker): mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_create(system=None, apps=["backup_recommended_app"]) +>>>>>>> modif YunohostError to YunohostError m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app") m18n.n.assert_any_call('backup_nothings_done') @@ -420,7 +421,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): assert not _is_installed("wordpress") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) @@ -441,7 +442,7 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): assert not _is_installed("wordpress") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) @@ -460,7 +461,7 @@ def test_restore_app_not_in_backup(mocker): mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["yoloswag"]) @@ -480,7 +481,7 @@ def test_restore_app_already_installed(mocker): assert _is_installed("wordpress") mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], apps=["wordpress"]) @@ -544,7 +545,7 @@ def test_restore_archive_with_no_json(mocker): assert "badbackup" in backup_list()["archives"] mocker.spy(m18n, "n") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): backup_restore(auth, name="badbackup", force=True) m18n.n.assert_any_call('backup_invalid_archive') diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index eb10cd604..826ec659b 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -6,7 +6,7 @@ from moulinette.core import init_authenticator from yunohost.app import app_install, app_change_url, app_remove, app_map from yunohost.domain import _get_maindomain -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError # Instantiate LDAP Authenticator AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') @@ -57,5 +57,5 @@ def test_appchangeurl_sameurl(): install_changeurl_app("/changeurl") check_changeurl_app("/changeurl") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): app_change_url(auth, "change_url_app", maindomain, "changeurl") diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py index 746f5a9d4..afb9faee9 100644 --- a/src/yunohost/tests/test_settings.py +++ b/src/yunohost/tests/test_settings.py @@ -2,7 +2,7 @@ import os import json import pytest -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from yunohost.settings import settings_get, settings_list, _get_settings, \ settings_set, settings_reset, settings_reset_all, \ @@ -46,7 +46,7 @@ def test_settings_get_full_enum(): def test_settings_get_doesnt_exists(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_get("doesnt.exists") @@ -70,39 +70,39 @@ def test_settings_set_enum(): def test_settings_set_doesexit(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("doesnt.exist", True) def test_settings_set_bad_type_bool(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.bool", 42) - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.bool", "pouet") def test_settings_set_bad_type_int(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.int", True) - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.int", "pouet") def test_settings_set_bad_type_string(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.string", True) - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.string", 42) def test_settings_set_bad_value_enum(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.enum", True) - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.enum", "e") - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.enum", 42) - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_set("example.enum", "pouet") @@ -119,7 +119,7 @@ def test_reset(): def test_settings_reset_doesexit(): - with pytest.raises(MoulinetteError): + with pytest.raises(YunohostError): settings_reset("doesnt.exist") diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 8e7da9939..51c19ff34 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -40,7 +40,7 @@ import apt import apt.progress from moulinette import msettings, msignals, m18n -from moulinette.core import MoulinetteError, init_authenticator +from yunohost.utils.error import YunohostError, init_authenticator from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json @@ -112,7 +112,7 @@ def tools_ldapinit(): pwd.getpwnam("admin") except KeyError: logger.error(m18n.n('ldap_init_failed_to_create_admin')) - raise MoulinetteError('installation_failed') + raise YunohostError('installation_failed') logger.success(m18n.n('ldap_initialized')) return auth @@ -139,7 +139,7 @@ def tools_adminpw(auth, new_password, check_strength=True): auth.update("cn=admin", { "userPassword": new_hash, }) except: logger.exception('unable to change admin password') - raise MoulinetteError('admin_password_change_failed') + raise YunohostError('admin_password_change_failed') else: # Write as root password try: @@ -175,7 +175,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None): # Check domain exists if new_domain not in domain_list(auth)['domains']: - raise MoulinetteError('domain_unknown') + raise YunohostError('domain_unknown') operation_logger.related_to.append(('domain', new_domain)) operation_logger.start() @@ -198,7 +198,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None): _set_maindomain(new_domain) except Exception as e: logger.warning("%s" % e, exc_info=1) - raise MoulinetteError('maindomain_change_failed') + raise YunohostError('maindomain_change_failed') _set_hostname(new_domain) @@ -247,7 +247,7 @@ def _set_hostname(hostname, pretty_hostname=None): if p.returncode != 0: logger.warning(command) logger.warning(out) - raise MoulinetteError('domain_hostname_failed') + raise YunohostError('domain_hostname_failed') else: logger.debug(out) @@ -288,7 +288,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, # Do some checks at first if os.path.isfile('/etc/yunohost/installed'): - raise MoulinetteError('yunohost_already_installed') + raise YunohostError('yunohost_already_installed') # Check password if not force_password: @@ -317,7 +317,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, dyndns = True # If not, abort the postinstall else: - raise MoulinetteError('dyndns_unavailable', domain=domain) + raise YunohostError('dyndns_unavailable', domain=domain) else: dyndns = False else: @@ -360,7 +360,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError('ssowat_persistent_conf_read_error', error=str(e)) + raise YunohostError('ssowat_persistent_conf_read_error', error=str(e)) except IOError: ssowat_conf = {} @@ -373,7 +373,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError('ssowat_persistent_conf_write_error', error=str(e)) + raise YunohostError('ssowat_persistent_conf_write_error', error=str(e)) os.system('chmod 644 /etc/ssowat/conf.json.persistent') @@ -400,7 +400,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, if p.returncode != 0: logger.warning(out) - raise MoulinetteError('yunohost_ca_creation_failed') + raise YunohostError('yunohost_ca_creation_failed') else: logger.debug(out) @@ -476,7 +476,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): # Update APT cache logger.debug(m18n.n('updating_apt_cache')) if not cache.update(): - raise MoulinetteError('update_cache_failed') + raise YunohostError('update_cache_failed') cache.open(None) cache.upgrade(True) @@ -495,7 +495,7 @@ def tools_update(ignore_apps=False, ignore_packages=False): if not ignore_apps: try: app_fetchlist() - except MoulinetteError: + except YunohostError: # FIXME : silent exception !? pass @@ -626,7 +626,7 @@ def tools_diagnosis(auth, private=False): diagnosis['system'] = OrderedDict() try: disks = monitor_disk(units=['filesystem'], human_readable=True) - except (MoulinetteError, Fault) as e: + except (YunohostError, Fault) as e: logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1) else: diagnosis['system']['disks'] = {} @@ -642,7 +642,7 @@ def tools_diagnosis(auth, private=False): try: system = monitor_system(units=['cpu', 'memory'], human_readable=True) - except MoulinetteError as e: + except YunohostError as e: logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1) else: diagnosis['system']['memory'] = { @@ -668,7 +668,7 @@ def tools_diagnosis(auth, private=False): # YNH Applications try: applications = app_list()['apps'] - except MoulinetteError as e: + except YunohostError as e: diagnosis['applications'] = m18n.n('diagnosis_no_apps') else: diagnosis['applications'] = {} @@ -800,7 +800,7 @@ def tools_migrations_list(pending=False, done=False): # Check for option conflict if pending and done: - raise MoulinetteError("migrations_list_conflict_pending_done") + raise YunohostError("migrations_list_conflict_pending_done") # Get all migrations migrations = _get_migrations_list() @@ -857,7 +857,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai # validate input, target must be "0" or a valid number elif target != 0 and target not in all_migration_numbers: - raise MoulinetteError('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers))) + raise YunohostError('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers))) logger.debug(m18n.n('migrations_current_target', target)) @@ -1063,7 +1063,7 @@ def _load_migration(migration_file): import traceback traceback.print_exc() - raise MoulinetteError('migrations_error_failed_to_load_migration', + raise YunohostError('migrations_error_failed_to_load_migration', number=number, name=name) def _skip_all_migrations(): diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 67cfe6cf1..313835fe0 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -34,7 +34,7 @@ import string import subprocess from moulinette import m18n -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from yunohost.service import service_status from yunohost.log import is_unit_operation @@ -71,7 +71,7 @@ def user_list(auth, fields=None): if attr in keys: attrs.append(attr) else: - raise MoulinetteError('field_invalid', attr) + raise YunohostError('field_invalid', attr) else: attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] @@ -129,7 +129,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # 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 MoulinetteError('system_username_exists') + raise YunohostError('system_username_exists') main_domain = _get_maindomain() aliases = [ @@ -140,11 +140,11 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas ] if mail in aliases: - raise MoulinetteError('mail_unavailable') + raise YunohostError('mail_unavailable') # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: - raise MoulinetteError(m18n.n('mail_domain_unknown', + raise YunohostError(m18n.n('mail_domain_unknown', domain=mail.split("@")[1])) operation_logger.start() @@ -186,7 +186,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas with open('/etc/ssowat/conf.json.persistent') as json_conf: ssowat_conf = json.loads(str(json_conf.read())) except ValueError as e: - raise MoulinetteError('ssowat_persistent_conf_read_error', error=e.strerror) + raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror) except IOError: ssowat_conf = {} @@ -196,7 +196,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas with open('/etc/ssowat/conf.json.persistent', 'w+') as f: json.dump(ssowat_conf, f, sort_keys=True, indent=4) except IOError as e: - raise MoulinetteError('ssowat_persistent_conf_write_error', error=e.strerror) + raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror) if auth.add('uid=%s,ou=users' % username, attr_dict): # Invalidate passwd to take user creation into account @@ -222,7 +222,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas return {'fullname': fullname, 'username': username, 'mail': mail} - raise MoulinetteError('user_creation_failed') + raise YunohostError('user_creation_failed') @is_unit_operation([('username', 'user')]) @@ -253,7 +253,7 @@ def user_delete(operation_logger, auth, username, purge=False): if purge: subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) else: - raise MoulinetteError('user_deletion_failed') + raise YunohostError('user_deletion_failed') app_ssowatconf(auth) @@ -292,7 +292,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, # Populate user informations result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) if not result: - raise MoulinetteError('user_unknown', user=username) + raise YunohostError('user_unknown', user=username) user = result[0] # Get modifications from arguments @@ -323,10 +323,10 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, ] auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise MoulinetteError(m18n.n('mail_domain_unknown', + raise YunohostError(m18n.n('mail_domain_unknown', domain=mail[mail.find('@') + 1:])) if mail in aliases: - raise MoulinetteError('mail_unavailable') + raise YunohostError('mail_unavailable') del user['mail'][0] new_attr_dict['mail'] = [mail] + user['mail'] @@ -337,7 +337,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, for mail in add_mailalias: auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise MoulinetteError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) + raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) user['mail'].append(mail) new_attr_dict['mail'] = user['mail'] @@ -348,7 +348,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if len(user['mail']) > 1 and mail in user['mail'][1:]: user['mail'].remove(mail) else: - raise MoulinetteError('mail_alias_remove_failed', mail=mail) + raise YunohostError('mail_alias_remove_failed', mail=mail) new_attr_dict['mail'] = user['mail'] if add_mailforward: @@ -367,7 +367,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]: user['maildrop'].remove(mail) else: - raise MoulinetteError('mail_forward_remove_failed', mail=mail) + raise YunohostError('mail_forward_remove_failed', mail=mail) new_attr_dict['maildrop'] = user['maildrop'] if mailbox_quota is not None: @@ -380,7 +380,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, app_ssowatconf(auth) return user_info(auth, username) else: - raise MoulinetteError('user_update_failed') + raise YunohostError('user_update_failed') def user_info(auth, username): @@ -405,7 +405,7 @@ def user_info(auth, username): if result: user = result[0] else: - raise MoulinetteError('user_unknown', user=username) + raise YunohostError('user_unknown', user=username) result_dict = { 'username': user['uid'][0], @@ -461,7 +461,7 @@ def user_info(auth, username): if result: return result_dict else: - raise MoulinetteError('user_info_failed') + raise YunohostError('user_info_failed') # # SSH subcategory diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index a2b6f5055..479176ee3 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -20,6 +20,7 @@ """ from moulinette.core import MoulinetteError +from moulinette.__init__ import m18n class YunohostError(MoulinetteError): """Yunohost base exception""" diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 6e8f5ba0a..b86fa23b1 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -84,14 +84,14 @@ class PasswordValidator(object): import errno import logging from moulinette import m18n - from moulinette.core import MoulinetteError + from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger logger = logging.getLogger('yunohost.utils.password') status, msg = self.validation_summary(password) if status == "error": - raise MoulinetteError(1, m18n.n(msg)) + raise YunohostError(1, m18n.n(msg)) def validation_summary(self, password): """ diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 475f29f79..7a3847873 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -4,7 +4,7 @@ import requests import json import errno -from moulinette.core import MoulinetteError +from yunohost.utils.error import YunohostError def yunopaste(data): @@ -13,14 +13,14 @@ def yunopaste(data): try: r = requests.post("%s/documents" % paste_server, data=data, timeout=30) except Exception as e: - raise MoulinetteError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) + raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e)) if r.status_code != 200: - raise MoulinetteError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) + raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text)) try: url = json.loads(r.text)["key"] except: - raise MoulinetteError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) + raise YunohostError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text) return "%s/raw/%s" % (paste_server, url) From 24cf090221e0a50c72acb856869de51cba4d2f56 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 01:53:53 +0000 Subject: [PATCH 232/250] Fix YunohostError definition --- src/yunohost/utils/error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index 479176ee3..8a73e52b7 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -20,11 +20,11 @@ """ from moulinette.core import MoulinetteError -from moulinette.__init__ import m18n +from moulinette import m18n class YunohostError(MoulinetteError): """Yunohost base exception""" def __init__(self, key, *args, **kwargs): msg = m18n.n(key, *args, **kwargs) - super(MoulinetteError, self).__init__(msg) + super(YunohostError, self).__init__(msg) From 07ffe6f592ed8c7454a941fd4dc82314f58bbc09 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 02:00:58 +0000 Subject: [PATCH 233/250] Fix misc typo and forgotten m18n.n --- src/yunohost/backup.py | 2 +- src/yunohost/user.py | 6 ++---- src/yunohost/utils/password.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 24313d416..a44819a33 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -2164,7 +2164,7 @@ def backup_list(with_info=False, human_readable=False): for a in result: try: d[a] = backup_info(a, human_readable=human_readable) - except YunohostError, e: + except YunohostError as e: logger.warning('%s: %s' % (a, e.strerror)) result = d diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 313835fe0..1cec8922d 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -144,8 +144,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas # Check that the mail domain exists if mail.split("@")[1] not in domain_list(auth)['domains']: - raise YunohostError(m18n.n('mail_domain_unknown', - domain=mail.split("@")[1])) + raise YunohostError('mail_domain_unknown', domain=mail.split("@")[1]) operation_logger.start() @@ -323,8 +322,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, ] auth.validate_uniqueness({'mail': mail}) if mail[mail.find('@') + 1:] not in domains: - raise YunohostError(m18n.n('mail_domain_unknown', - domain=mail[mail.find('@') + 1:])) + raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:]) if mail in aliases: raise YunohostError('mail_unavailable') diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index b86fa23b1..ddc59cbe9 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -91,7 +91,7 @@ class PasswordValidator(object): status, msg = self.validation_summary(password) if status == "error": - raise YunohostError(1, m18n.n(msg)) + raise YunohostError(msg) def validation_summary(self, password): """ From 2dd1d8ccfe59bbbf32813172ead3d04b283c490c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 02:49:36 +0000 Subject: [PATCH 234/250] Unused errno + remaining unecessary m18n.n --- src/yunohost/app.py | 8 +++----- src/yunohost/backup.py | 1 - src/yunohost/certificate.py | 1 - .../data_migrations/0002_migrate_to_tsig_sha256.py | 1 - src/yunohost/domain.py | 1 - src/yunohost/dyndns.py | 1 - src/yunohost/firewall.py | 1 - src/yunohost/hook.py | 1 - src/yunohost/log.py | 1 - src/yunohost/monitor.py | 1 - src/yunohost/service.py | 1 - src/yunohost/settings.py | 1 - src/yunohost/ssh.py | 1 - src/yunohost/tools.py | 1 - src/yunohost/user.py | 1 - src/yunohost/utils/password.py | 2 -- src/yunohost/utils/yunopaste.py | 1 - 17 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index f4e48de45..510b60e95 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -30,7 +30,6 @@ import yaml import time import re import urlparse -import errno import subprocess import glob import pwd @@ -2076,10 +2075,9 @@ def _check_manifest_requirements(manifest, app_instance_name): for pkgname, spec in requirements.items(): version = versions[pkgname] if version not in packages.SpecifierSet(spec): - raise YunohostError( - errno.EINVAL, m18n.n('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={}, auth=None): diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index a44819a33..6ed73d343 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -26,7 +26,6 @@ import os import re import json -import errno import time import tarfile import shutil diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ca27d59df..cdcf4bbe9 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -24,7 +24,6 @@ import os import sys -import errno import shutil import pwd import grp diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index e81b20e5f..39e4d2afe 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -4,7 +4,6 @@ import requests import base64 import time import json -import errno from moulinette import m18n from yunohost.utils.error import YunohostError diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index e28a24afe..83da7e18c 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -27,7 +27,6 @@ import os import re import json import yaml -import errno from moulinette import m18n, msettings from yunohost.utils.error import YunohostError diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 13dd4b43d..e83280b19 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -29,7 +29,6 @@ import json import glob import time import base64 -import errno import subprocess from moulinette import m18n diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index fb1804659..f7c6c0875 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -26,7 +26,6 @@ import os import sys import yaml -import errno try: import miniupnpc except ImportError: diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 901321926..5a6c4c154 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -25,7 +25,6 @@ """ import os import re -import errno import tempfile from glob import iglob diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 3c7ff02f3..7b32f18dd 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -26,7 +26,6 @@ import os import yaml -import errno import collections from datetime import datetime diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 4fef2ca61..469ac41f1 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -31,7 +31,6 @@ import calendar import subprocess import xmlrpclib import os.path -import errno import os import dns.resolver import cPickle as pickle diff --git a/src/yunohost/service.py b/src/yunohost/service.py index 082b32eab..b2c18c74e 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -28,7 +28,6 @@ import time import yaml import json import subprocess -import errno import shutil import hashlib diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 8783188a1..12b046146 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -1,6 +1,5 @@ import os import json -import errno from datetime import datetime from collections import OrderedDict diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index e284736c4..a9e5babf7 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -2,7 +2,6 @@ import re import os -import errno import pwd import subprocess diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 51c19ff34..6102b9f7a 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -27,7 +27,6 @@ import re import os import yaml import json -import errno import logging import subprocess import pwd diff --git a/src/yunohost/user.py b/src/yunohost/user.py index 1cec8922d..ea8dc30a9 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -27,7 +27,6 @@ import os import re import pwd import json -import errno import crypt import random import string diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index ddc59cbe9..9cee56d53 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -81,9 +81,7 @@ 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. - import errno import logging - from moulinette import m18n from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 7a3847873..e349ebc32 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -2,7 +2,6 @@ import requests import json -import errno from yunohost.utils.error import YunohostError From e97b07403f72b772ba557e9ed4b41e7e15df2e71 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 13:23:53 +0000 Subject: [PATCH 235/250] Fixes following rebase --- .../0007_ssh_conf_managed_by_yunohost_step1.py | 6 +++--- .../0008_ssh_conf_managed_by_yunohost_step2.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py index 73cb162b6..306e47672 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py @@ -4,7 +4,6 @@ import re from shutil import copyfile from moulinette import m18n -from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, rm @@ -12,6 +11,7 @@ from yunohost.tools import Migration from yunohost.service import service_regen_conf, _get_conf_hashes, \ _calculate_hash, _run_service_command from yunohost.settings import settings_set +from yunohost.utils.error import YunohostError logger = getActionLogger('yunohost.migration') @@ -67,11 +67,11 @@ class MyMigration(Migration): # Restart ssh and backward if it fail if not _run_service_command('restart', 'ssh'): self.backward() - raise MoulinetteError(m18n.n("migration_0007_cancel")) + raise YunohostError("migration_0007_cancel") def backward(self): # We don't backward completely but it should be enough copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF) if not _run_service_command('restart', 'ssh'): - raise MoulinetteError(m18n.n("migration_0007_cannot_restart")) + raise YunohostError("migration_0007_cannot_restart") diff --git a/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py index c53154192..5d8fee89c 100644 --- a/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py +++ b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py @@ -1,13 +1,12 @@ import re -from moulinette import m18n -from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from yunohost.tools import Migration from yunohost.service import service_regen_conf, _get_conf_hashes, \ _calculate_hash from yunohost.settings import settings_set, settings_get +from yunohost.utils.error import YunohostError logger = getActionLogger('yunohost.migration') @@ -33,7 +32,7 @@ class MyMigration(Migration): def backward(self): - raise MoulinetteError(m18n.n("migration_0008_backward_impossible")) + raise YunohostError("migration_0008_backward_impossible") @property def mode(self): From caa2d0d91b325ffcf1e867052e4989c0e052f825 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 14:23:11 +0000 Subject: [PATCH 236/250] MoulinetteError / OSError never accepted keyword arguments such as exc_info :/ ... --- src/yunohost/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index 12b046146..fbf998d3a 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -206,7 +206,7 @@ def _get_settings(): setting_key=key)) unknown_settings[key] = value except Exception as e: - raise YunohostError('global_settings_cant_open_settings', reason=e, exc_info=1) + raise YunohostError('global_settings_cant_open_settings', reason=e) if unknown_settings: try: @@ -227,10 +227,10 @@ 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, exc_info=1) + 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, exc_info=1) + raise YunohostError('global_settings_cant_write_settings', reason=e) From 18c33db8b60779ac4c66b7a6efde5973cbe7136f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 19:07:25 +0000 Subject: [PATCH 237/250] MoulinetteError -> YunohostError --- src/yunohost/settings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index fbf998d3a..c48f567ab 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -95,9 +95,10 @@ def settings_set(key, value): try: value=int(value) except: - raise MoulinetteError(errno.EINVAL, m18n.n( - '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) From 712f742ef6cc790b87358a2901fa3ee83c5ade2e Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 19:10:17 +0000 Subject: [PATCH 238/250] Woopsies typo --- src/yunohost/tests/test_backuprestore.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index abe5bb1c8..a2356318d 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -345,7 +345,6 @@ def test_backup_app_with_no_backup_script(mocker): with pytest.raises(YunohostError): backup_create(system=None, apps=["backup_recommended_app"]) ->>>>>>> modif YunohostError to YunohostError m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app") m18n.n.assert_any_call('backup_nothings_done') From b93e96d33bb999fd0e96ef6e6c1293d0ddc85d49 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 12 Dec 2018 19:38:05 +0000 Subject: [PATCH 239/250] Fix tests + allow to bypass m18n in YunohostError if a raw message is provided --- src/yunohost/app.py | 4 ++-- src/yunohost/tests/test_appurl.py | 4 ++-- src/yunohost/tools.py | 3 ++- src/yunohost/utils/error.py | 9 ++++++--- src/yunohost/utils/yunopaste.py | 6 +++--- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 510b60e95..3177407c1 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -840,9 +840,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg - raise YunohostError(msg) + raise YunohostError(msg, __raw_msg__=True) msg = error_msg - raise YunohostError(msg) + raise YunohostError(msg, __raw_msg__=True) # Clean hooks and add new ones hook_remove(app_instance_name) diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 58c345dea..85f290b5d 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -1,7 +1,7 @@ import pytest -from yunohost.utils.error import YunohostError, init_authenticator - +from moulinette.core import init_authenticator +from yunohost.utils.error import YunohostError from yunohost.app import app_install, app_remove from yunohost.domain import _get_maindomain, domain_url_available, _normalize_domain_path diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 6102b9f7a..58bdd23d3 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -39,7 +39,8 @@ import apt import apt.progress from moulinette import msettings, msignals, m18n -from yunohost.utils.error import YunohostError, init_authenticator +from moulinette.core import init_authenticator +from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger from moulinette.utils.process import check_output from moulinette.utils.filesystem import read_json, write_to_json diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index 8a73e52b7..7c00ee5a4 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -24,7 +24,10 @@ from moulinette import m18n class YunohostError(MoulinetteError): """Yunohost base exception""" - def __init__(self, key, *args, **kwargs): - msg = m18n.n(key, *args, **kwargs) - super(YunohostError, self).__init__(msg) + def __init__(self, key, __raw_msg__=False, *args, **kwargs): + if __raw_msg__: + msg = key + else: + msg = m18n.n(key, *args, **kwargs) + super(YunohostError, self).__init__(msg, __raw_msg__=True) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index e349ebc32..436f94911 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -12,14 +12,14 @@ def yunopaste(data): 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)) + 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)) + 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) + 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) From 2ece557e0c12481dca02a02870d644bbb3e13c91 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 13 Dec 2018 15:16:07 +0000 Subject: [PATCH 240/250] Raw messages --- src/yunohost/app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 3177407c1..fca2d376b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -183,7 +183,7 @@ def app_fetchlist(url=None, name=None): with open(list_file, "w") as f: f.write(appslist) except Exception as e: - raise YunohostError("Error while writing appslist %s: %s" % (name, str(e))) + raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), __raw_msg__=True) now = int(time.time()) appslists[name]["lastUpdate"] = now @@ -1472,7 +1472,7 @@ def app_action_run(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()))) + raise YunohostError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys())), __raw_msg__=True) action_declaration = actions[action] @@ -2431,7 +2431,7 @@ def _write_appslist_list(appslist_lists): json.dump(appslist_lists, f) except Exception as e: raise YunohostError("Error while writing list of appslist %s: %s" % - (APPSLISTS_JSON, str(e))) + (APPSLISTS_JSON, str(e)), __raw_msg__=True) def _register_new_appslist(url, name): From f6b244f294df316028e2a0cb7f508b4cbfb33ab5 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 13 Dec 2018 15:39:24 +0000 Subject: [PATCH 241/250] Raw message --- 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 fca2d376b..36a0414ec 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1510,7 +1510,7 @@ def app_action_run(app, action, args=None): ) if retcode not in action_declaration.get("accepted_return_codes", [0]): - raise YunohostError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode)) + raise YunohostError("Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode), __raw_msg__=True) os.remove(path) From fb010765bd5cdabc38186c5ab5db9fdad3776d1a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 13 Dec 2018 15:40:00 +0000 Subject: [PATCH 242/250] Those are genuine MoulinetteError because base on moulinette helpers --- src/yunohost/domain.py | 3 ++- src/yunohost/dyndns.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 83da7e18c..515a452a8 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -29,6 +29,7 @@ import json import yaml from moulinette import m18n, msettings +from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError from moulinette.utils.log import getActionLogger @@ -76,7 +77,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False): try: auth.validate_uniqueness({'virtualdomain': domain}) - except YunohostError: + except MoulinetteError: raise YunohostError('domain_exists') operation_logger.start() diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index e83280b19..0428eb93c 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -32,13 +32,13 @@ import base64 import subprocess from moulinette import m18n -from yunohost.utils.error import YunohostError +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import read_file, write_to_file, rm from moulinette.utils.network import download_json from 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.log import is_unit_operation @@ -74,7 +74,7 @@ def _dyndns_provides(provider, domain): # Dyndomains will be a list of domains supported by the provider # e.g. [ "nohost.me", "noho.st" ] dyndomains = download_json('https://%s/domains' % provider, timeout=30) - except YunohostError as e: + except MoulinetteError as e: logger.error(str(e)) raise YunohostError('dyndns_could_not_check_provide', domain=domain, provider=provider) @@ -101,7 +101,7 @@ def _dyndns_available(provider, domain): try: r = download_json('https://%s/test/%s' % (provider, domain), expected_status_code=None) - except YunohostError as e: + except MoulinetteError as e: logger.error(str(e)) raise YunohostError('dyndns_could_not_check_available', domain=domain, provider=provider) From 115b557b67893111d5b7708f6b3b3226615f3092 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 13 Dec 2018 18:49:20 +0000 Subject: [PATCH 243/250] autopep8 + a few manual tweaks --- src/yunohost/app.py | 51 +++--- src/yunohost/backup.py | 165 +++++++++--------- src/yunohost/certificate.py | 64 +++---- .../0001_change_cert_group_to_sslcert.py | 2 + .../0002_migrate_to_tsig_sha256.py | 4 +- .../0003_migrate_to_stretch.py | 13 +- .../0004_php5_to_php7_pools.py | 3 +- .../0005_postgresql_9p4_to_9p6.py | 1 + .../0006_sync_admin_and_root_passwords.py | 2 + ...0007_ssh_conf_managed_by_yunohost_step1.py | 9 +- ...0008_ssh_conf_managed_by_yunohost_step2.py | 5 +- src/yunohost/domain.py | 4 +- src/yunohost/dyndns.py | 11 +- src/yunohost/firewall.py | 4 +- src/yunohost/hook.py | 6 +- src/yunohost/log.py | 3 +- src/yunohost/monitor.py | 8 +- src/yunohost/service.py | 27 +-- src/yunohost/settings.py | 14 +- src/yunohost/ssh.py | 2 +- src/yunohost/tests/conftest.py | 15 +- src/yunohost/tests/test_appslist.py | 26 +-- src/yunohost/tests/test_appurl.py | 7 +- src/yunohost/tests/test_backuprestore.py | 95 +++++----- src/yunohost/tests/test_changeurl.py | 1 + src/yunohost/tests/test_settings.py | 7 +- src/yunohost/tools.py | 41 +++-- src/yunohost/user.py | 16 +- src/yunohost/utils/error.py | 4 +- src/yunohost/utils/filesystem.py | 2 + src/yunohost/utils/network.py | 2 +- src/yunohost/utils/packages.py | 6 + src/yunohost/utils/password.py | 6 +- src/yunohost/utils/yunopaste.py | 1 + 34 files changed, 341 insertions(+), 286 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 36a0414ec..8eaef03c5 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -137,7 +137,7 @@ def app_fetchlist(url=None, name=None): else: appslists_to_be_fetched = appslists.keys() - import requests # lazy loading this module for performance reasons + import requests # lazy loading this module for performance reasons # Fetch all appslists to be fetched for name in appslists_to_be_fetched: @@ -172,7 +172,7 @@ def app_fetchlist(url=None, name=None): appslist = appslist_request.text try: json.loads(appslist) - except ValueError, e: + except ValueError as e: logger.error(m18n.n('appslist_retrieve_bad_format', appslist=name)) continue @@ -542,7 +542,7 @@ def app_change_url(operation_logger, auth, app, domain, path): 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)) + app=app, domain=domain, path=path)) hook_callback('post_app_change_url', args=args_list, env=env_dict) @@ -701,7 +701,6 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.log import OperationLogger - # Fetch or extract sources try: os.listdir(INSTALL_TMP) @@ -758,7 +757,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) # Start register change on system - operation_logger.extra.update({'env':env_dict}) + operation_logger.extra.update({'env': env_dict}) operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"] operation_logger.related_to.append(("app", app_id)) operation_logger.start() @@ -816,8 +815,8 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on # Execute remove script operation_logger_remove = OperationLogger('remove_on_failed_install', - [('app', app_instance_name)], - env=env_dict_remove) + [('app', app_instance_name)], + env=env_dict_remove) operation_logger_remove.start() remove_retcode = hook_exec( @@ -944,7 +943,6 @@ def app_addaccess(auth, apps, users=[]): for app in apps: - app_settings = _get_app_settings(app) if not app_settings: continue @@ -957,7 +955,7 @@ def app_addaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - operation_logger= OperationLogger('app_addaccess', related_to) + operation_logger = OperationLogger('app_addaccess', related_to) operation_logger.start() allowed_users = set() @@ -1020,7 +1018,7 @@ def app_removeaccess(auth, apps, users=[]): # Start register change on system related_to = [('app', app)] - operation_logger= OperationLogger('app_removeaccess', related_to) + operation_logger = OperationLogger('app_removeaccess', related_to) operation_logger.start() if remove_all: @@ -1034,7 +1032,7 @@ def app_removeaccess(auth, apps, users=[]): if allowed_user not in users: allowed_users.add(allowed_user) - operation_logger.related_to += [ ('user', x) for x in allowed_users ] + operation_logger.related_to += [('user', x) for x in allowed_users] operation_logger.flush() new_users = ','.join(allowed_users) app_setting(app, 'allowed_users', new_users) @@ -1069,7 +1067,7 @@ def app_clearaccess(auth, apps): # Start register change on system related_to = [('app', app)] - operation_logger= OperationLogger('app_clearaccess', related_to) + operation_logger = OperationLogger('app_clearaccess', related_to) operation_logger.start() if 'mode' in app_settings: @@ -1126,7 +1124,7 @@ def app_makedefault(operation_logger, auth, app, domain=None): if domain is None: domain = app_domain - operation_logger.related_to.append(('domain',domain)) + operation_logger.related_to.append(('domain', domain)) elif domain not in domain_list(auth)['domains']: raise YunohostError('domain_unknown') @@ -1218,7 +1216,7 @@ def app_register_url(auth, app, domain, path): # This line can't be moved on top of file, otherwise it creates an infinite # loop of import with tools.py... - from domain import _get_conflicting_apps, _normalize_domain_path + from .domain import _get_conflicting_apps, _normalize_domain_path domain, path = _normalize_domain_path(domain, path) @@ -1569,10 +1567,10 @@ def app_config_show_panel(app): parsed_values[key] = value return_code = hook_exec(config_script, - args=["show"], - env=env, - stdout_callback=parse_stdout, - ) + args=["show"], + env=env, + stdout_callback=parse_stdout, + ) if return_code != 0: raise Exception("script/config show return value code: %s (considered as an error)", return_code) @@ -1656,9 +1654,9 @@ def app_config_apply(app, args): 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, - ) + args=["apply"], + env=env, + ) if return_code != 0: raise Exception("'script/config apply' return value code: %s (considered as an error)", return_code) @@ -2185,7 +2183,6 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None): elif arg_type == 'password': msignals.display(m18n.n('good_practices_about_user_password')) - try: input_string = msignals.prompt(ask_string, is_password) except NotImplementedError: @@ -2385,7 +2382,7 @@ def _install_appslist_fetch_cron(): with open(cron_job_file, "w") as f: f.write('\n'.join(cron_job)) - _set_permissions(cron_job_file, "root", "root", 0755) + _set_permissions(cron_job_file, "root", "root", 0o755) # FIXME - Duplicate from certificate.py, should be moved into a common helper @@ -2431,7 +2428,7 @@ def _write_appslist_list(appslist_lists): json.dump(appslist_lists, f) except Exception as e: raise YunohostError("Error while writing list of appslist %s: %s" % - (APPSLISTS_JSON, str(e)), __raw_msg__=True) + (APPSLISTS_JSON, str(e)), __raw_msg__=True) def _register_new_appslist(url, name): @@ -2541,7 +2538,7 @@ def _patch_php5(app_folder): continue c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' " \ - "-e 's@/var/run/php5-fpm@/var/run/php/php7.0-fpm@g' " \ - "-e 's@php5@php7.0@g' " \ - "%s" % filename + "-e 's@/var/run/php5-fpm@/var/run/php/php7.0-fpm@g' " \ + "-e 's@php5@php7.0@g' " \ + "%s" % filename os.system(c) diff --git a/src/yunohost/backup.py b/src/yunohost/backup.py index 6ed73d343..745291fb1 100644 --- a/src/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -52,6 +52,7 @@ from yunohost.monitor import binary_to_human from yunohost.tools import tools_postinstall from yunohost.service import service_regen_conf from yunohost.log import OperationLogger +from functools import reduce BACKUP_PATH = '/home/yunohost.backup' ARCHIVES_PATH = '%s/archives' % BACKUP_PATH @@ -63,6 +64,7 @@ logger = getActionLogger('yunohost.backup') class BackupRestoreTargetsManager(object): + """ BackupRestoreTargetsManager manage the targets in BackupManager and RestoreManager @@ -176,6 +178,7 @@ class BackupRestoreTargetsManager(object): class BackupManager(): + """ This class collect files to backup in a list and apply one or several backup method on it. @@ -267,9 +270,9 @@ class BackupManager(): self.work_dir = os.path.join(BACKUP_PATH, 'tmp', name) self._init_work_dir() - ########################################################################### - # Misc helpers # - ########################################################################### + # + # Misc helpers # + # @property def info(self): @@ -321,16 +324,16 @@ 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, 0750, 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", self.work_dir) # FIXME May be we should clean the workdir here raise YunohostError('backup_output_directory_not_empty') - ########################################################################### - # Backup target management # - ########################################################################### + # + # Backup target management # + # def set_system_targets(self, system_parts=[]): """ @@ -380,9 +383,9 @@ class BackupManager(): logger.warning(m18n.n('backup_with_no_restore_script_for_app', app=app)) self.targets.set_result("apps", app, "Warning") - ########################################################################### - # Management of files to backup / "The CSV" # - ########################################################################### + # + # Management of files to backup / "The CSV" # + # def _import_to_list_to_backup(self, tmp_csv): """ @@ -465,9 +468,9 @@ class BackupManager(): logger.error(m18n.n('backup_csv_addition_failed')) self.csv_file.close() - ########################################################################### - # File collection from system parts and apps # - ########################################################################### + # + # File collection from system parts and apps # + # def collect_files(self): """ @@ -602,7 +605,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=0750, + filesystem.mkdir(restore_hooks_dir, mode=0o750, parents=True, uid='admin') restore_hooks = hook_list("restore")["hooks"] @@ -668,7 +671,7 @@ class BackupManager(): logger.debug(m18n.n('backup_running_app_script', app=app)) try: # Prepare backup directory for the app - filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') + 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) @@ -702,9 +705,9 @@ class BackupManager(): filesystem.rm(tmp_script, force=True) filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True) - ########################################################################### - # Actual backup archive creation / method management # - ########################################################################### + # + # Actual backup archive creation / method management # + # def add(self, method): """ @@ -776,6 +779,7 @@ class BackupManager(): class RestoreManager(): + """ RestoreManager allow to restore a past backup archive @@ -824,9 +828,9 @@ class RestoreManager(): self.method = BackupMethod.create(method) self.targets = BackupRestoreTargetsManager() - ########################################################################### - # Misc helpers # - ########################################################################### + # + # Misc helpers # + # @property def success(self): @@ -876,7 +880,7 @@ class RestoreManager(): domain = f.readline().rstrip() except IOError: logger.debug("unable to retrieve current_host from the backup", - exc_info=1) + exc_info=1) # FIXME include the current_host by default ? raise YunohostError('backup_invalid_archive') @@ -902,9 +906,9 @@ class RestoreManager(): logger.warning(m18n.n('restore_cleaning_failed')) filesystem.rm(self.work_dir, True, True) - ########################################################################### - # Restore target manangement # - ########################################################################### + # + # Restore target manangement # + # def set_system_targets(self, system_parts=[]): """ @@ -980,9 +984,9 @@ class RestoreManager(): self.info['apps'].keys(), unknown_error) - ########################################################################### - # Archive mounting # - ########################################################################### + # + # Archive mounting # + # def mount(self): """ @@ -1023,9 +1027,9 @@ class RestoreManager(): self._read_info_files() - ########################################################################### - # Space computation / checks # - ########################################################################### + # + # Space computation / checks # + # def _compute_needed_space(self): """ @@ -1082,13 +1086,13 @@ 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) - ########################################################################### - # "Actual restore" (reverse step of the backup collect part) # - ########################################################################### + # + # "Actual restore" (reverse step of the backup collect part) # + # def restore(self): """ @@ -1104,7 +1108,6 @@ class RestoreManager(): # Apply dirty patch to redirect php5 file on php7 self._patch_backup_csv_file() - self._restore_system() self._restore_apps() finally: @@ -1130,7 +1133,7 @@ class RestoreManager(): contains_php5 = True row['source'] = row['source'].replace('/etc/php5', '/etc/php/7.0') \ .replace('/var/run/php5-fpm', '/var/run/php/php7.0-fpm') \ - .replace('php5','php7') + .replace('php5', 'php7') newlines.append(row) except (IOError, OSError, csv.Error) as e: @@ -1260,7 +1263,7 @@ class RestoreManager(): # Check if the app has a restore script app_restore_script_in_archive = os.path.join(app_scripts_in_archive, - 'restore') + 'restore') if not os.path.isfile(app_restore_script_in_archive): logger.warning(m18n.n('unrestore_app', app=app_instance_name)) self.targets.set_result("apps", app_instance_name, "Warning") @@ -1273,7 +1276,7 @@ class RestoreManager(): app_instance_name) app_scripts_new_path = os.path.join(app_settings_new_path, 'scripts') shutil.copytree(app_settings_in_archive, app_settings_new_path) - filesystem.chmod(app_settings_new_path, 0400, 0400, True) + filesystem.chmod(app_settings_new_path, 0o400, 0o400, True) filesystem.chown(app_scripts_new_path, 'admin', None, True) # Copy the app scripts to a writable temporary folder @@ -1281,7 +1284,7 @@ class RestoreManager(): # in the backup method ? tmp_folder_for_app_restore = tempfile.mkdtemp(prefix='restore') copytree(app_scripts_in_archive, tmp_folder_for_app_restore) - filesystem.chmod(tmp_folder_for_app_restore, 0550, 0550, True) + filesystem.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') @@ -1298,7 +1301,7 @@ class RestoreManager(): raise_on_error=True, env=env_dict) except: - msg = m18n.n('restore_app_failed',app=app_instance_name) + msg = m18n.n('restore_app_failed', app=app_instance_name) logger.exception(msg) operation_logger.error(msg) @@ -1314,8 +1317,8 @@ class RestoreManager(): env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) operation_logger = OperationLogger('remove_on_failed_restore', - [('app', app_instance_name)], - env=env_dict_remove) + [('app', app_instance_name)], + env=env_dict_remove) operation_logger.start() # Execute remove script @@ -1359,12 +1362,13 @@ class RestoreManager(): return env_var -############################################################################### -# Backup methods # -############################################################################### +# +# Backup methods # +# class BackupMethod(object): + """ BackupMethod is an abstract class that represents a way to backup and restore a list of files. @@ -1637,7 +1641,7 @@ class BackupMethod(object): if size > MB_ALLOWED_TO_ORGANIZE: try: i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed', - answers='y/N', size=str(size))) + answers='y/N', size=str(size))) except NotImplemented: raise YunohostError('backup_unable_to_organize_files') else: @@ -1646,7 +1650,7 @@ class BackupMethod(object): # Copy unbinded path logger.debug(m18n.n('backup_copying_to_organize_the_archive', - size=str(size))) + 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']): @@ -1686,6 +1690,7 @@ class BackupMethod(object): class CopyBackupMethod(BackupMethod): + """ This class just do an uncompress copy of each file in a location, and could be the inverse for restoring @@ -1712,7 +1717,7 @@ class CopyBackupMethod(BackupMethod): dest_parent = os.path.dirname(dest) if not os.path.exists(dest_parent): - filesystem.mkdir(dest_parent, 0750, True, uid='admin') + filesystem.mkdir(dest_parent, 0o750, True, uid='admin') if os.path.isdir(source): shutil.copytree(source, dest) @@ -1747,6 +1752,7 @@ class CopyBackupMethod(BackupMethod): class TarBackupMethod(BackupMethod): + """ This class compress all files to backup in archive. """ @@ -1777,7 +1783,7 @@ class TarBackupMethod(BackupMethod): """ if not os.path.exists(self.repo): - filesystem.mkdir(self.repo, 0750, parents=True, uid='admin') + filesystem.mkdir(self.repo, 0o750, parents=True, uid='admin') # Check free space in output self._check_is_enough_free_space() @@ -1895,6 +1901,7 @@ class BorgBackupMethod(BackupMethod): class CustomBackupMethod(BackupMethod): + """ This class use a bash script/hook "backup_method" to do the backup/restore operations. A user can add his own hook inside @@ -1958,9 +1965,9 @@ class CustomBackupMethod(BackupMethod): self.manager.description] -############################################################################### -# "Front-end" # -############################################################################### +# +# "Front-end" # +# def backup_create(name=None, description=None, methods=[], output_directory=None, no_compress=False, @@ -1980,9 +1987,9 @@ def backup_create(name=None, description=None, methods=[], # TODO: Add a 'clean' argument to clean output directory - ########################################################################### - # Validate / parse arguments # - ########################################################################### + # + # Validate / parse arguments # + # # Validate there is no archive with the same name if name and name in backup_list()['archives']: @@ -1995,7 +2002,7 @@ def backup_create(name=None, description=None, methods=[], # Check for forbidden folders if output_directory.startswith(ARCHIVES_PATH) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', - output_directory): + output_directory): raise YunohostError('backup_output_directory_forbidden') # Check that output directory is empty @@ -2017,9 +2024,9 @@ def backup_create(name=None, description=None, methods=[], system = [] apps = [] - ########################################################################### - # Intialize # - ########################################################################### + # + # Intialize # + # # Create yunohost archives directory if it does not exists _create_archive_dir() @@ -2044,9 +2051,9 @@ def backup_create(name=None, description=None, methods=[], backup_manager.set_system_targets(system) backup_manager.set_apps_targets(apps) - ########################################################################### - # Collect files and put them in the archive # - ########################################################################### + # + # Collect files and put them in the archive # + # # Collect files to be backup (by calling app backup script / system hooks) backup_manager.collect_files() @@ -2074,9 +2081,9 @@ def backup_restore(auth, name, system=[], apps=[], force=False): apps -- List of application names to restore """ - ########################################################################### - # Validate / parse arguments # - ########################################################################### + # + # Validate / parse arguments # + # # If no --system or --apps given, restore everything if system is None and apps is None: @@ -2105,9 +2112,9 @@ def backup_restore(auth, name, system=[], apps=[], force=False): # TODO Partial app restore could not work if ldap is not restored before # TODO repair mysql if broken and it's a complete restore - ########################################################################### - # Initialize # - ########################################################################### + # + # Initialize # + # restore_manager = RestoreManager(name) @@ -2116,9 +2123,9 @@ def backup_restore(auth, name, system=[], apps=[], force=False): restore_manager.assert_enough_free_space() - ########################################################################### - # Mount the archive then call the restore for each system part / app # - ########################################################################### + # + # Mount the archive then call the restore for each system part / app # + # restore_manager.mount() restore_manager.restore() @@ -2156,7 +2163,7 @@ def backup_list(with_info=False, human_readable=False): except ValueError: continue result.append(name) - result.sort(key=lambda x: os.path.getctime(os.path.join(ARCHIVES_PATH, x+".tar.gz"))) + result.sort(key=lambda x: os.path.getctime(os.path.join(ARCHIVES_PATH, x + ".tar.gz"))) if result and with_info: d = OrderedDict() @@ -2194,7 +2201,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) + path=archive_file) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) @@ -2259,7 +2266,7 @@ def backup_delete(name): """ if name not in backup_list()["archives"]: raise YunohostError('backup_archive_name_unknown', - name=name) + name=name) hook_callback('pre_backup_delete', args=[name]) @@ -2277,9 +2284,9 @@ def backup_delete(name): logger.success(m18n.n('backup_deleted')) -############################################################################### -# Misc helpers # -############################################################################### +# +# Misc helpers # +# def _create_archive_dir(): @@ -2288,7 +2295,7 @@ def _create_archive_dir(): if os.path.lexists(ARCHIVES_PATH): raise YunohostError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH) - os.mkdir(ARCHIVES_PATH, 0750) + os.mkdir(ARCHIVES_PATH, 0o750) def _call_for_each_path(self, callback, csv_path=None): diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index cdcf4bbe9..aea0c60b1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -80,9 +80,9 @@ DNS_RESOLVERS = [ "80.67.188.188" # LDN ] -############################################################################### -# Front-end stuff # -############################################################################### +# +# Front-end stuff # +# def certificate_status(auth, domain_list, full=False): @@ -149,7 +149,7 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)], - args={'force': force}) + args={'force': force}) # Paths of files and folder we'll need date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S") @@ -215,10 +215,10 @@ def _certificate_install_selfsigned(domain_list, force=False): crt_pem.write(ca_pem.read()) # Set appropriate permissions - _set_permissions(new_cert_folder, "root", "root", 0755) - _set_permissions(key_file, "root", "ssl-cert", 0640) - _set_permissions(crt_file, "root", "ssl-cert", 0640) - _set_permissions(conf_file, "root", "root", 0600) + _set_permissions(new_cert_folder, "root", "root", 0o755) + _set_permissions(key_file, "root", "ssl-cert", 0o640) + _set_permissions(crt_file, "root", "ssl-cert", 0o640) + _set_permissions(conf_file, "root", "root", 0o600) # Actually enable the certificate we created _enable_certificate(domain, new_cert_folder) @@ -273,8 +273,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F for domain in domain_list: operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)], - args={'force': force, 'no_checks': no_checks, - 'staging': staging}) + args={'force': force, 'no_checks': no_checks, + 'staging': staging}) logger.info( "Now attempting install of certificate for domain %s!", domain) @@ -298,6 +298,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.error(msg) operation_logger.error(msg) + def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -367,8 +368,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal for domain in domain_list: operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)], - args={'force': force, 'no_checks': no_checks, - 'staging': staging, 'email': email}) + args={'force': force, 'no_checks': no_checks, + 'staging': staging, 'email': email}) logger.info( "Now attempting renewing of certificate for domain %s !", domain) @@ -401,9 +402,10 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e, stack.getvalue()) -############################################################################### -# Back-end stuff # -############################################################################### +# +# Back-end stuff # +# + def _install_cron(): cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" @@ -412,7 +414,7 @@ def _install_cron(): f.write("#!/bin/bash\n") f.write("yunohost domain cert-renew --email\n") - _set_permissions(cron_job_file, "root", "root", 0755) + _set_permissions(cron_job_file, "root", "root", 0o755) def _email_renewing_failed(domain, exception_message, stack): @@ -517,8 +519,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): if not os.path.exists(TMP_FOLDER): os.makedirs(TMP_FOLDER) - _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) - _set_permissions(TMP_FOLDER, "root", "root", 0640) + _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0o650) + _set_permissions(TMP_FOLDER, "root", "root", 0o640) # Regen conf for dnsmasq if needed _regen_dnsmasq_if_needed() @@ -529,7 +531,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) - _set_permissions(domain_key_file, "root", "ssl-cert", 0640) + _set_permissions(domain_key_file, "root", "ssl-cert", 0o640) _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) @@ -563,7 +565,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): raise YunohostError('certmanager_cert_signing_failed') - import requests # lazy loading this module for performance reasons + import requests # lazy loading this module for performance reasons try: intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text except requests.exceptions.Timeout as e: @@ -585,12 +587,12 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): os.makedirs(new_cert_folder) - _set_permissions(new_cert_folder, "root", "root", 0655) + _set_permissions(new_cert_folder, "root", "root", 0o655) # Move the private key domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") shutil.move(domain_key_file, domain_key_file_finaldest) - _set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0640) + _set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0o640) # Write the cert domain_cert_file = os.path.join(new_cert_folder, "crt.pem") @@ -599,7 +601,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): f.write(signed_certificate) f.write(intermediate_certificate) - _set_permissions(domain_cert_file, "root", "ssl-cert", 0640) + _set_permissions(domain_cert_file, "root", "ssl-cert", 0o640) if staging: return @@ -614,7 +616,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False): def _prepare_certificate_signing_request(domain, key_file, output_folder): - from OpenSSL import crypto # lazy loading this module for performance reasons + from OpenSSL import crypto # lazy loading this module for performance reasons # Init a request csr = crypto.X509Req() @@ -645,7 +647,7 @@ def _get_status(domain): if not os.path.isfile(cert_file): raise YunohostError('certmanager_no_cert_file', domain=domain, file=cert_file) - from OpenSSL import crypto # lazy loading this module for performance reasons + from OpenSSL import crypto # lazy loading this module for performance reasons try: cert = crypto.load_certificate( crypto.FILETYPE_PEM, open(cert_file).read()) @@ -735,19 +737,19 @@ def _get_status(domain): "ACME_eligible": ACME_eligible } -############################################################################### -# Misc small stuff ... # -############################################################################### +# +# Misc small stuff ... # +# def _generate_account_key(): logger.debug("Generating account key ...") _generate_key(ACCOUNT_KEY_FILE) - _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400) + _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0o400) def _generate_key(destination_path): - from OpenSSL import crypto # lazy loading this module for performance reasons + from OpenSSL import crypto # lazy loading this module for performance reasons k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, KEY_SIZE) @@ -836,7 +838,7 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): - import requests # lazy loading this module for performance reasons + import requests # lazy loading this module for performance reasons try: requests.head("http://" + ip, headers={"Host": domain}, timeout=10) except requests.exceptions.Timeout as e: diff --git a/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py b/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py index cd39df9fa..6485861b7 100644 --- a/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py +++ b/src/yunohost/data_migrations/0001_change_cert_group_to_sslcert.py @@ -3,7 +3,9 @@ import glob from yunohost.tools import Migration from moulinette.utils.filesystem import chown + class MyMigration(Migration): + "Change certificates group permissions from 'metronome' to 'ssl-cert'" all_certificate_files = glob.glob("/etc/yunohost/certs/*/*.pem") diff --git a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py index 39e4d2afe..824245c82 100644 --- a/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py +++ b/src/yunohost/data_migrations/0002_migrate_to_tsig_sha256.py @@ -16,6 +16,7 @@ logger = getActionLogger('yunohost.migration') class MyMigration(Migration): + "Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG" def backward(self): @@ -70,7 +71,7 @@ class MyMigration(Migration): os.system("mv /etc/yunohost/dyndns/*+165* /tmp") raise YunohostError('migrate_tsig_failed', domain=domain, - error_code=str(r.status_code), error=error) + error_code=str(r.status_code), error=error) # remove old certificates os.system("mv /etc/yunohost/dyndns/*+157* /tmp") @@ -87,4 +88,3 @@ class MyMigration(Migration): logger.info(m18n.n('migrate_tsig_end')) return - diff --git a/src/yunohost/data_migrations/0003_migrate_to_stretch.py b/src/yunohost/data_migrations/0003_migrate_to_stretch.py index 80d182fbe..ee8c09849 100644 --- a/src/yunohost/data_migrations/0003_migrate_to_stretch.py +++ b/src/yunohost/data_migrations/0003_migrate_to_stretch.py @@ -24,6 +24,7 @@ YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"] class MyMigration(Migration): + "Upgrade the system to Debian Stretch and Yunohost 3.0" mode = "manual" @@ -168,11 +169,11 @@ class MyMigration(Migration): # - switch yunohost's repo to forge for f in sources_list: command = "sed -i -e 's@ jessie @ stretch @g' " \ - "-e '/backports/ s@^#*@#@' " \ - "-e 's@ jessie/updates @ stretch/updates @g' " \ - "-e 's@ jessie-updates @ stretch-updates @g' " \ - "-e 's@repo.yunohost@forge.yunohost@g' " \ - "{}".format(f) + "-e '/backports/ s@^#*@#@' " \ + "-e 's@ jessie/updates @ stretch/updates @g' " \ + "-e 's@ jessie-updates @ stretch-updates @g' " \ + "-e 's@repo.yunohost@forge.yunohost@g' " \ + "{}".format(f) os.system(command) def get_apps_equivs_packages(self): @@ -286,7 +287,7 @@ class MyMigration(Migration): # Create tmp directory if it does not exists tmp_dir = os.path.join("/tmp/", self.name) if not os.path.exists(tmp_dir): - os.mkdir(tmp_dir, 0700) + os.mkdir(tmp_dir, 0o700) for f in self.files_to_keep: dest_file = f.strip('/').replace("/", "_") diff --git a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py index 0237ddb38..46a5eb91d 100644 --- a/src/yunohost/data_migrations/0004_php5_to_php7_pools.py +++ b/src/yunohost/data_migrations/0004_php5_to_php7_pools.py @@ -19,6 +19,7 @@ MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {} class MyMigration(Migration): + "Migrate php5-fpm 'pool' conf files to php7 stuff" def migrate(self): @@ -58,7 +59,7 @@ class MyMigration(Migration): _run_service_command("enable", "php7.0-fpm") os.system("systemctl stop php5-fpm") os.system("systemctl disable php5-fpm") - os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy + os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy # Get list of nginx conf file nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") diff --git a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py index 0208b717e..c4a6e7f34 100644 --- a/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py +++ b/src/yunohost/data_migrations/0005_postgresql_9p4_to_9p6.py @@ -11,6 +11,7 @@ logger = getActionLogger('yunohost.migration') class MyMigration(Migration): + "Migrate DBs from Postgresql 9.4 to 9.6 after migrating to Stretch" def migrate(self): diff --git a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py index 1149f28d5..cd13d680d 100644 --- a/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py +++ b/src/yunohost/data_migrations/0006_sync_admin_and_root_passwords.py @@ -15,7 +15,9 @@ from yunohost.tools import Migration logger = getActionLogger('yunohost.migration') SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] + class MyMigration(Migration): + "Synchronize admin and root passwords" def migrate(self): diff --git a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py index 306e47672..080cc0163 100644 --- a/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py +++ b/src/yunohost/data_migrations/0007_ssh_conf_managed_by_yunohost_step1.py @@ -8,8 +8,10 @@ from moulinette.utils.log import getActionLogger from moulinette.utils.filesystem import mkdir, rm from yunohost.tools import Migration -from yunohost.service import service_regen_conf, _get_conf_hashes, \ - _calculate_hash, _run_service_command +from yunohost.service import service_regen_conf, \ + _get_conf_hashes, \ + _calculate_hash, \ + _run_service_command from yunohost.settings import settings_set from yunohost.utils.error import YunohostError @@ -19,6 +21,7 @@ SSHD_CONF = '/etc/ssh/sshd_config' class MyMigration(Migration): + """ This is the first step of a couple of migrations that ensure SSH conf is managed by YunoHost (even if the "from_script" flag is present, which was @@ -48,7 +51,7 @@ class MyMigration(Migration): # Create sshd_config.d dir if not os.path.exists(SSHD_CONF + '.d'): - mkdir(SSHD_CONF + '.d', 0755, uid='root', gid='root') + mkdir(SSHD_CONF + '.d', 0o755, uid='root', gid='root') # Here, we make it so that /etc/ssh/sshd_config is managed # by the regen conf (in particular in the case where the diff --git a/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py index 5d8fee89c..6adfa769f 100644 --- a/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py +++ b/src/yunohost/data_migrations/0008_ssh_conf_managed_by_yunohost_step2.py @@ -3,7 +3,8 @@ import re from moulinette.utils.log import getActionLogger from yunohost.tools import Migration -from yunohost.service import service_regen_conf, _get_conf_hashes, \ +from yunohost.service import service_regen_conf, \ + _get_conf_hashes, \ _calculate_hash from yunohost.settings import settings_set, settings_get from yunohost.utils.error import YunohostError @@ -12,7 +13,9 @@ logger = getActionLogger('yunohost.migration') SSHD_CONF = '/etc/ssh/sshd_config' + class MyMigration(Migration): + """ In this second step, the admin is asked if it's okay to use the recommended SSH configuration - which also implies diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 515a452a8..29c7ab554 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -115,8 +115,8 @@ def domain_add(operation_logger, auth, domain, dyndns=False): service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) app_ssowatconf(auth) - except Exception, e: - from sys import exc_info; + except Exception as e: + from sys import exc_info t, v, tb = exc_info() # Force domain removal silently diff --git a/src/yunohost/dyndns.py b/src/yunohost/dyndns.py index 0428eb93c..59a26e74b 100644 --- a/src/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -104,7 +104,7 @@ def _dyndns_available(provider, domain): except MoulinetteError as e: logger.error(str(e)) raise YunohostError('dyndns_could_not_check_available', - domain=domain, provider=provider) + domain=domain, provider=provider) return r == u"Domain %s is available" % domain @@ -149,7 +149,7 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom with open(key_file) as f: key = f.readline().strip().split(' ', 6)[-1] - import requests # lazy loading this module for performance reasons + 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) @@ -211,7 +211,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, exception=e, number=migration.number, name=migration.name), - exc_info=1) + exc_info=1) return # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' @@ -225,7 +225,6 @@ 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)).strip() or None old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() or None @@ -252,7 +251,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, logger.info("Updated needed, going on...") dns_conf = _build_dns_conf(domain) - del dns_conf["extra"] # Ignore records from the 'extra' category + del dns_conf["extra"] # Ignore records from the 'extra' category # Delete the old records for all domain/subdomains @@ -273,7 +272,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None, # should be muc.the.domain.tld. or the.domain.tld if record["value"] == "@": record["value"] = domain - record["value"] = record["value"].replace(";","\;") + record["value"] = record["value"].replace(";", "\;") action = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record) action = action.replace(" @.", " ") diff --git a/src/yunohost/firewall.py b/src/yunohost/firewall.py index f7c6c0875..39102bdc2 100644 --- a/src/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -374,10 +374,10 @@ def firewall_upnp(action='status', no_refresh=False): try: # Add new port mapping upnpc.addportmapping(port, protocol, upnpc.lanaddr, - port, 'yunohost firewall: port %d' % port, '') + port, 'yunohost firewall: port %d' % port, '') except: logger.debug('unable to add port %d using UPnP', - port, exc_info=1) + port, exc_info=1) enabled = False if enabled != firewall['uPnP']['enabled']: diff --git a/src/yunohost/hook.py b/src/yunohost/hook.py index 5a6c4c154..79e9289ef 100644 --- a/src/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -354,7 +354,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, # prepend environment variables cmd = '{0} {1}'.format( ' '.join(['{0}={1}'.format(k, shell_quote(v)) - for k, v in env.items()]), cmd) + for k, v in env.items()]), cmd) command.append(cmd.format(script=cmd_script, args=cmd_args)) if logger.isEnabledFor(log.DEBUG): @@ -369,8 +369,8 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False, ) if stdinfo: - callbacks = ( callbacks[0], callbacks[1], - lambda l: logger.info(l.rstrip())) + callbacks = (callbacks[0], callbacks[1], + lambda l: logger.info(l.rstrip())) logger.debug("About to run the command '%s'" % command) diff --git a/src/yunohost/log.py b/src/yunohost/log.py index 7b32f18dd..d1e0c0e05 100644 --- a/src/yunohost/log.py +++ b/src/yunohost/log.py @@ -283,6 +283,7 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'], class OperationLogger(object): + """ Instances of this class represents unit operation done on the ynh instance. @@ -423,7 +424,7 @@ class OperationLogger(object): else: if is_api: msg = "" + m18n.n('log_link_to_failed_log', - name=self.name, desc=desc) + "" + name=self.name, desc=desc) + "" else: msg = m18n.n('log_help_to_get_failed_log', name=self.name, desc=desc) diff --git a/src/yunohost/monitor.py b/src/yunohost/monitor.py index 469ac41f1..7af55f287 100644 --- a/src/yunohost/monitor.py +++ b/src/yunohost/monitor.py @@ -288,7 +288,7 @@ def monitor_system(units=None, human_readable=False): else: raise YunohostError('unit_unknown', unit=u) - if len(units) == 1 and type(result[units[0]]) is not str: + if len(units) == 1 and not isinstance(result[units[0]], str): return result[units[0]] return result @@ -404,7 +404,7 @@ def monitor_enable(with_stats=False): """ from yunohost.service import (service_status, service_enable, - service_start) + service_start) glances = service_status('glances') if glances['status'] != 'running': @@ -414,7 +414,7 @@ def monitor_enable(with_stats=False): # Install crontab if with_stats: - # day: every 5 min # week: every 1 h # month: every 4 h # + # day: every 5 min # week: every 1 h # month: every 4 h # rules = ('*/5 * * * * root {cmd} day >> /dev/null\n' '3 * * * * root {cmd} week >> /dev/null\n' '6 */4 * * * root {cmd} month >> /dev/null').format( @@ -431,7 +431,7 @@ def monitor_disable(): """ from yunohost.service import (service_status, service_disable, - service_stop) + service_stop) glances = service_status('glances') if glances['status'] != 'inactive': diff --git a/src/yunohost/service.py b/src/yunohost/service.py index b2c18c74e..a8ef0e796 100644 --- a/src/yunohost/service.py +++ b/src/yunohost/service.py @@ -151,6 +151,7 @@ def service_stop(names): raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name)) logger.debug(m18n.n('service_already_stopped', service=name)) + @is_unit_operation() def service_enable(operation_logger, names): """ @@ -377,7 +378,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, if not names: operation_logger.name_parameter_override = 'all' elif len(names) != 1: - operation_logger.name_parameter_override = str(len(operation_logger.related_to))+'_services' + operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_services' operation_logger.start() # Clean pending conf directory @@ -389,7 +390,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, shutil.rmtree(os.path.join(PENDING_CONF_DIR, name), ignore_errors=True) else: - filesystem.mkdir(PENDING_CONF_DIR, 0755, True) + filesystem.mkdir(PENDING_CONF_DIR, 0o755, True) # Format common hooks arguments common_args = [1 if force else 0, 1 if dry_run else 0] @@ -400,7 +401,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, def _pre_call(name, priority, path, args): # create the pending conf directory for the service service_pending_path = os.path.join(PENDING_CONF_DIR, name) - filesystem.mkdir(service_pending_path, 0755, True, uid='root') + filesystem.mkdir(service_pending_path, 0o755, True, uid='root') # return the arguments to pass to the script return pre_args + [service_pending_path, ] @@ -418,7 +419,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False, if not names: raise YunohostError('service_regenconf_failed', - services=', '.join(pre_result['failed'])) + services=', '.join(pre_result['failed'])) # Set the processing method _regen = _process_regen_conf if not dry_run else lambda *a, **k: True @@ -603,7 +604,7 @@ def _run_service_command(action, service): cmd = 'systemctl %s %s' % (action, service) need_lock = services[service].get('need_lock', False) \ - and action in ['start', 'stop', 'restart', 'reload'] + and action in ['start', 'stop', 'restart', 'reload'] try: # Launch the command @@ -637,10 +638,10 @@ def _give_lock(action, service, p): else: systemctl_PID_name = "ControlPID" - cmd_get_son_PID ="systemctl show %s -p %s" % (service, systemctl_PID_name) + cmd_get_son_PID = "systemctl show %s -p %s" % (service, systemctl_PID_name) son_PID = 0 # As long as we did not found the PID and that the command is still running - while son_PID == 0 and p.poll() == None: + 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()) \ @@ -657,11 +658,12 @@ def _give_lock(action, service, p): return son_PID + def _remove_lock(PID_to_remove): # FIXME ironically not concurrency safe because it's not atomic... PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n") - PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ] + PIDs_to_keep = [PID for PID in PIDs if int(PID) != PID_to_remove] filesystem.write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep)) @@ -775,6 +777,7 @@ def _find_previous_log_file(file): return None + def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True): """Compare two files and return the differences @@ -919,22 +922,22 @@ def _process_regen_conf(system_conf, new_conf=None, save=True): backup_dir = os.path.dirname(backup_path) if not os.path.isdir(backup_dir): - filesystem.mkdir(backup_dir, 0755, True) + filesystem.mkdir(backup_dir, 0o755, True) shutil.copy2(system_conf, backup_path) logger.debug(m18n.n('service_conf_file_backed_up', - conf=system_conf, backup=backup_path)) + conf=system_conf, backup=backup_path)) try: if not new_conf: os.remove(system_conf) logger.debug(m18n.n('service_conf_file_removed', - conf=system_conf)) + conf=system_conf)) else: system_dir = os.path.dirname(system_conf) if not os.path.isdir(system_dir): - filesystem.mkdir(system_dir, 0755, True) + filesystem.mkdir(system_dir, 0o755, True) shutil.copyfile(new_conf, system_conf) logger.debug(m18n.n('service_conf_file_updated', diff --git a/src/yunohost/settings.py b/src/yunohost/settings.py index c48f567ab..bbfb3ca56 100644 --- a/src/yunohost/settings.py +++ b/src/yunohost/settings.py @@ -88,12 +88,12 @@ def settings_set(key, value): if key_type == "bool": if not isinstance(value, bool): raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + 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) + value = int(value) except: raise YunohostError('global_settings_bad_type_for_setting', setting=key, @@ -101,19 +101,19 @@ def settings_set(key, value): expected_type=key_type) else: raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + received_type=type(value).__name__, expected_type=key_type) elif key_type == "string": if not isinstance(value, basestring): raise YunohostError('global_settings_bad_type_for_setting', setting=key, - received_type=type(value).__name__, expected_type=key_type) + 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, - received_type=type(value).__name__, - expected_type=", ".join(settings[key]["choices"])) + received_type=type(value).__name__, + expected_type=", ".join(settings[key]["choices"])) else: raise YunohostError('global_settings_unknown_type', setting=key, - unknown_type=key_type) + unknown_type=key_type) settings[key]["value"] = value diff --git a/src/yunohost/ssh.py b/src/yunohost/ssh.py index a9e5babf7..7c8c85b35 100644 --- a/src/yunohost/ssh.py +++ b/src/yunohost/ssh.py @@ -98,7 +98,7 @@ def user_ssh_add_key(auth, username, key, comment): # create empty file to set good permissions write_to_file(authorized_keys_file, "") chown(authorized_keys_file, uid=user["uid"][0]) - chmod(authorized_keys_file, 0600) + chmod(authorized_keys_file, 0o600) authorized_keys_content = read_file(authorized_keys_file) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index 6958ae679..a2dc585bd 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -7,12 +7,14 @@ sys.path.append("..") def pytest_addoption(parser): parser.addoption("--yunodebug", action="store_true", default=False) -############################################################################### -# Tweak translator to raise exceptions if string keys are not defined # -############################################################################### +# +# Tweak translator to raise exceptions if string keys are not defined # +# old_translate = moulinette.core.Translator.translate + + def new_translate(self, key, *args, **kwargs): if key not in self._translations[self.default_locale].keys(): @@ -21,14 +23,15 @@ def new_translate(self, key, *args, **kwargs): return old_translate(self, key, *args, **kwargs) moulinette.core.Translator.translate = new_translate + def new_m18nn(self, key, *args, **kwargs): return self._namespaces[self._current_namespace].translate(key, *args, **kwargs) moulinette.core.Moulinette18n.n = new_m18nn -############################################################################### -# Init the moulinette to have the cli loggers stuff # -############################################################################### +# +# Init the moulinette to have the cli loggers stuff # +# def pytest_cmdline_main(config): diff --git a/src/yunohost/tests/test_appslist.py b/src/yunohost/tests/test_appslist.py index 7a6f27d6c..817807ed9 100644 --- a/src/yunohost/tests/test_appslist.py +++ b/src/yunohost/tests/test_appslist.py @@ -17,7 +17,7 @@ APPSLISTS_JSON = '/etc/yunohost/appslists.json' def setup_function(function): # Clear all appslist - files = glob.glob(REPO_PATH+"/*") + files = glob.glob(REPO_PATH + "/*") for f in files: os.remove(f) @@ -42,9 +42,9 @@ def cron_job_is_there(): return r == 0 -############################################################################### -# Test listing of appslists and registering of appslists # -############################################################################### +# +# Test listing of appslists and registering of appslists # +# def test_appslist_list_empty(): @@ -103,9 +103,9 @@ def test_appslist_list_register_conflict_url(): assert "plopette" not in appslist_dict.keys() -############################################################################### -# Test fetching of appslists # -############################################################################### +# +# Test fetching of appslists # +# def test_appslist_fetch(): @@ -244,9 +244,9 @@ def test_appslist_fetch_timeout(): app_fetchlist() -############################################################################### -# Test remove of appslist # -############################################################################### +# +# Test remove of appslist # +# def test_appslist_remove(): @@ -274,9 +274,9 @@ def test_appslist_remove_unknown(): app_removelist("dummy") -############################################################################### -# Test migration from legacy appslist system # -############################################################################### +# +# Test migration from legacy appslist system # +# def add_legacy_cron(name, url): diff --git a/src/yunohost/tests/test_appurl.py b/src/yunohost/tests/test_appurl.py index 85f290b5d..d9d5fa7ab 100644 --- a/src/yunohost/tests/test_appurl.py +++ b/src/yunohost/tests/test_appurl.py @@ -22,6 +22,7 @@ def setup_function(function): except: pass + def teardown_function(function): try: @@ -50,18 +51,18 @@ def test_urlavailable(): def test_registerurl(): app_install(auth, "./tests/apps/register_url_app_ynh", - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) assert not domain_url_available(auth, maindomain, "/urlregisterapp") # Try installing at same location with pytest.raises(YunohostError): app_install(auth, "./tests/apps/register_url_app_ynh", - args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) + args="domain=%s&path=%s" % (maindomain, "/urlregisterapp")) def test_registerurl_baddomain(): with pytest.raises(YunohostError): app_install(auth, "./tests/apps/register_url_app_ynh", - args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp")) + args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp")) diff --git a/src/yunohost/tests/test_backuprestore.py b/src/yunohost/tests/test_backuprestore.py index a2356318d..af8538dae 100644 --- a/src/yunohost/tests/test_backuprestore.py +++ b/src/yunohost/tests/test_backuprestore.py @@ -22,6 +22,7 @@ AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous') AUTH_PARAMETERS = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'} auth = None + def setup_function(function): global maindomain @@ -87,9 +88,9 @@ def teardown_function(function): shutil.rmtree("/opt/test_backup_output_directory") -############################################################################### -# Helpers # -############################################################################### +# +# Helpers # +# def app_is_installed(app): @@ -111,6 +112,7 @@ def backup_test_dependencies_are_met(): return True + def tmp_backup_directory_is_empty(): if not os.path.exists("/home/yunohost.backup/tmp/"): @@ -118,6 +120,7 @@ def tmp_backup_directory_is_empty(): else: return len(os.listdir('/home/yunohost.backup/tmp/')) == 0 + def clean_tmp_backup_directory(): if tmp_backup_directory_is_empty(): @@ -125,10 +128,10 @@ def clean_tmp_backup_directory(): mount_lines = subprocess.check_output("mount").split("\n") - points_to_umount = [ line.split(" ")[2] - for line in mount_lines - if len(line) >= 3 - and line.split(" ")[2].startswith("/home/yunohost.backup/tmp") ] + 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) @@ -138,6 +141,7 @@ def clean_tmp_backup_directory(): shutil.rmtree("/home/yunohost.backup/tmp/") + def reset_ssowat_conf(): # Make sure we have a ssowat @@ -191,9 +195,10 @@ def add_archive_system_from_2p4(): os.system("cp ./tests/apps/backup_system_from_2p4/backup.tar.gz \ /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz") -############################################################################### -# System backup # -############################################################################### +# +# System backup # +# + def test_backup_only_ldap(): @@ -220,9 +225,10 @@ def test_backup_system_part_that_does_not_exists(mocker): m18n.n.assert_any_call('backup_hook_unknown', hook="yolol") m18n.n.assert_any_call('backup_nothings_done') -############################################################################### -# System backup and restore # -############################################################################### +# +# System backup and restore # +# + def test_backup_and_restore_all_sys(): @@ -250,9 +256,9 @@ def test_backup_and_restore_all_sys(): assert os.path.exists("/etc/ssowat/conf.json") -############################################################################### -# System restore from 2.4 # -############################################################################### +# +# System restore from 2.4 # +# @pytest.mark.with_system_archive_from_2p4 def test_restore_system_from_Ynh2p4(monkeypatch, mocker): @@ -265,19 +271,20 @@ def test_restore_system_from_Ynh2p4(monkeypatch, mocker): # Restore system archive from 2.4 try: backup_restore(auth, name=backup_list()["archives"][1], - system=[], - apps=None, - force=True) + system=[], + apps=None, + force=True) finally: # Restore system as it was backup_restore(auth, name=backup_list()["archives"][0], - system=[], - apps=None, - force=True) + system=[], + apps=None, + force=True) + +# +# App backup # +# -############################################################################### -# App backup # -############################################################################### @pytest.mark.with_backup_recommended_app_installed def test_backup_script_failure_handling(monkeypatch, mocker): @@ -300,6 +307,7 @@ def test_backup_script_failure_handling(monkeypatch, mocker): m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app') + @pytest.mark.with_backup_recommended_app_installed def test_backup_not_enough_free_space(monkeypatch, mocker): @@ -385,6 +393,7 @@ def test_backup_with_different_output_directory(): assert len(archives_info["system"].keys()) == 1 assert "conf_ssh" in archives_info["system"].keys() + @pytest.mark.clean_opt_dir def test_backup_with_no_compress(): # Create the backup @@ -396,15 +405,15 @@ def test_backup_with_no_compress(): assert os.path.exists("/opt/test_backup_output_directory/info.json") -############################################################################### -# App restore # -############################################################################### +# +# App restore # +# @pytest.mark.with_wordpress_archive_from_2p4 def test_restore_app_wordpress_from_Ynh2p4(): backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + apps=["wordpress"]) @pytest.mark.with_wordpress_archive_from_2p4 @@ -422,7 +431,7 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker): with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + apps=["wordpress"]) m18n.n.assert_any_call('restore_app_failed', app='wordpress') m18n.n.assert_any_call('restore_nothings_done') @@ -443,12 +452,12 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker): with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + apps=["wordpress"]) m18n.n.assert_any_call('restore_not_enough_disk_space', - free_space=0, - margin=ANY, - needed_space=ANY) + free_space=0, + margin=ANY, + needed_space=ANY) assert not _is_installed("wordpress") @@ -462,7 +471,7 @@ def test_restore_app_not_in_backup(mocker): with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["yoloswag"]) + apps=["yoloswag"]) m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag") assert not _is_installed("wordpress") @@ -475,14 +484,14 @@ def test_restore_app_already_installed(mocker): assert not _is_installed("wordpress") backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + apps=["wordpress"]) assert _is_installed("wordpress") mocker.spy(m18n, "n") with pytest.raises(YunohostError): backup_restore(auth, system=None, name=backup_list()["archives"][0], - apps=["wordpress"]) + apps=["wordpress"]) m18n.n.assert_any_call('restore_already_installed_app', app="wordpress") m18n.n.assert_any_call('restore_nothings_done') @@ -531,9 +540,10 @@ def _test_backup_and_restore_app(app): assert app_is_installed(app) -############################################################################### -# Some edge cases # -############################################################################### +# +# Some edge cases # +# + def test_restore_archive_with_no_json(mocker): @@ -555,10 +565,9 @@ def test_backup_binds_are_readonly(monkeypatch): self.manager = backup_manager self._organize_files() - confssh = os.path.join(self.work_dir, "conf/ssh") output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh, - shell=True, env={'LANG' : 'en_US.UTF-8'}) + shell=True, env={'LANG': 'en_US.UTF-8'}) assert "Read-only file system" in output @@ -568,7 +577,7 @@ def test_backup_binds_are_readonly(monkeypatch): self.clean() monkeypatch.setattr("yunohost.backup.BackupMethod.mount_and_backup", - custom_mount_and_backup) + custom_mount_and_backup) # Create the backup backup_create(system=[]) diff --git a/src/yunohost/tests/test_changeurl.py b/src/yunohost/tests/test_changeurl.py index 826ec659b..4856e18c1 100644 --- a/src/yunohost/tests/test_changeurl.py +++ b/src/yunohost/tests/test_changeurl.py @@ -53,6 +53,7 @@ def test_appchangeurl(): check_changeurl_app("/newchangeurl") + def test_appchangeurl_sameurl(): install_changeurl_app("/changeurl") check_changeurl_app("/changeurl") diff --git a/src/yunohost/tests/test_settings.py b/src/yunohost/tests/test_settings.py index afb9faee9..0da12597f 100644 --- a/src/yunohost/tests/test_settings.py +++ b/src/yunohost/tests/test_settings.py @@ -18,7 +18,8 @@ def teardown_function(function): def test_settings_get_bool(): - assert settings_get("example.bool") == True + assert settings_get("example.bool") + def test_settings_get_full_bool(): assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Example boolean option"} @@ -27,6 +28,7 @@ def test_settings_get_full_bool(): def test_settings_get_int(): assert settings_get("example.int") == 42 + def test_settings_get_full_int(): assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Example int option"} @@ -34,6 +36,7 @@ def test_settings_get_full_int(): def test_settings_get_string(): assert settings_get("example.string") == "yolo swag" + def test_settings_get_full_string(): assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Example string option"} @@ -41,6 +44,7 @@ def test_settings_get_full_string(): def test_settings_get_enum(): assert settings_get("example.enum") == "a" + def test_settings_get_full_enum(): assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Example enum option", "choices": ["a", "b", "c"]} @@ -152,7 +156,6 @@ def test_reset_all_backup(): assert settings_after_modification == json.load(open(old_settings_backup_path, "r")) - def test_unknown_keys(): unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown" unknown_setting = { diff --git a/src/yunohost/tools.py b/src/yunohost/tools.py index 58bdd23d3..43190e5b8 100644 --- a/src/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -136,7 +136,7 @@ def tools_adminpw(auth, new_password, check_strength=True): new_hash = _hash_user_password(new_password) try: - auth.update("cn=admin", { "userPassword": new_hash, }) + auth.update("cn=admin", {"userPassword": new_hash, }) except: logger.exception('unable to change admin password') raise YunohostError('admin_password_change_failed') @@ -265,7 +265,7 @@ def _is_inside_container(): stderr=subprocess.STDOUT) out, _ = p.communicate() - container = ['lxc','lxd','docker'] + container = ['lxc', 'lxd', 'docker'] return out.split()[0] in container @@ -323,7 +323,6 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False, else: dyndns = False - operation_logger.start() logger.info(m18n.n('yunohost_installing')) @@ -539,7 +538,7 @@ def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=Fal # If API call if is_api: critical_packages = ("moulinette", "yunohost", - "yunohost-admin", "ssowat", "python") + "yunohost-admin", "ssowat", "python") critical_upgrades = set() for pkg in cache.get_changes(): @@ -575,7 +574,6 @@ def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=Fal else: logger.info(m18n.n('packages_no_upgrade')) - if not ignore_apps: try: app_upgrade(auth) @@ -719,7 +717,7 @@ def _check_if_vulnerable_to_meltdown(): try: call = subprocess.Popen("bash %s --batch json --variant 3" % SCRIPT_PATH, shell=True, - stdout=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) output, _ = call.communicate() @@ -815,12 +813,12 @@ def tools_migrations_list(pending=False, done=False): migrations = [m for m in migrations if m.number > last_migration] # Reduce to dictionnaries - migrations = [{ "id": migration.id, - "number": migration.number, - "name": migration.name, - "mode": migration.mode, - "description": migration.description, - "disclaimer": migration.disclaimer } for migration in migrations ] + migrations = [{"id": migration.id, + "number": migration.number, + "name": migration.name, + "mode": migration.mode, + "description": migration.description, + "disclaimer": migration.disclaimer} for migration in migrations] return {"migrations": migrations} @@ -914,7 +912,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai accept_disclaimer = False # Start register change on system - operation_logger= OperationLogger('tools_migrations_migrate_' + mode) + operation_logger = OperationLogger('tools_migrations_migrate_' + mode) operation_logger.start() if not skip: @@ -934,9 +932,9 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai # 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, - number=migration.number, - name=migration.name) + exception=e, + number=migration.number, + name=migration.name) logger.error(msg, exc_info=1) operation_logger.error(msg) break @@ -967,6 +965,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai write_to_json(MIGRATIONS_STATE_PATH, state) + def tools_migrations_state(): """ Show current migration state @@ -1009,7 +1008,7 @@ def _get_migrations_list(): migrations = [] try: - import data_migrations + from . import data_migrations except ImportError: # not data migrations present, return empty list return migrations @@ -1032,7 +1031,7 @@ def _get_migration_by_name(migration_name): """ try: - import data_migrations + from . import data_migrations except ImportError: raise AssertionError("Unable to find migration with name %s" % migration_name) @@ -1051,7 +1050,7 @@ def _load_migration(migration_file): number, name = migration_id.split("_", 1) logger.debug(m18n.n('migrations_loading_migration', - number=number, name=name)) + number=number, name=name)) try: # this is python builtin method to import a module using a name, we @@ -1064,7 +1063,8 @@ def _load_migration(migration_file): traceback.print_exc() raise YunohostError('migrations_error_failed_to_load_migration', - number=number, name=name) + number=number, name=name) + def _skip_all_migrations(): """ @@ -1115,4 +1115,3 @@ class Migration(object): @property def description(self): return m18n.n("migration_description_%s" % self.id) - diff --git a/src/yunohost/user.py b/src/yunohost/user.py index ea8dc30a9..d0c432c56 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -40,6 +40,7 @@ from yunohost.log import is_unit_operation logger = getActionLogger('yunohost.user') + def user_list(auth, fields=None): """ List users @@ -98,7 +99,7 @@ def user_list(auth, fields=None): @is_unit_operation([('username', 'user')]) def user_create(operation_logger, auth, username, firstname, lastname, mail, password, - mailbox_quota="0"): + mailbox_quota="0"): """ Create user @@ -262,8 +263,8 @@ def user_delete(operation_logger, auth, username, purge=False): @is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) def user_update(operation_logger, auth, username, firstname=None, lastname=None, mail=None, - change_password=None, add_mailforward=None, remove_mailforward=None, - add_mailalias=None, remove_mailalias=None, mailbox_quota=None): + change_password=None, add_mailforward=None, remove_mailforward=None, + add_mailalias=None, remove_mailalias=None, mailbox_quota=None): """ Update user informations @@ -466,18 +467,23 @@ def user_info(auth, username): # import yunohost.ssh + def user_ssh_allow(auth, username): return yunohost.ssh.user_ssh_allow(auth, username) + def user_ssh_disallow(auth, username): return yunohost.ssh.user_ssh_disallow(auth, username) + def user_ssh_list_keys(auth, username): return yunohost.ssh.user_ssh_list_keys(auth, username) + def user_ssh_add_key(auth, username, key, comment): return yunohost.ssh.user_ssh_add_key(auth, username, key, comment) + def user_ssh_remove_key(auth, username, key): return yunohost.ssh.user_ssh_remove_key(auth, username, key) @@ -485,6 +491,7 @@ def user_ssh_remove_key(auth, username, key): # End SSH subcategory # + def _convertSize(num, suffix=''): for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(num) < 1024.0: @@ -520,6 +527,3 @@ def _hash_user_password(password): 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 7c00ee5a4..1d5b05a6f 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -22,12 +22,14 @@ from moulinette.core import MoulinetteError from moulinette import m18n + class YunohostError(MoulinetteError): + """Yunohost base exception""" + def __init__(self, key, __raw_msg__=False, *args, **kwargs): if __raw_msg__: msg = key else: msg = m18n.n(key, *args, **kwargs) super(YunohostError, self).__init__(msg, __raw_msg__=True) - diff --git a/src/yunohost/utils/filesystem.py b/src/yunohost/utils/filesystem.py index 3f026f980..04d7d3906 100644 --- a/src/yunohost/utils/filesystem.py +++ b/src/yunohost/utils/filesystem.py @@ -20,10 +20,12 @@ """ import os + def free_space_in_directory(dirpath): stat = os.statvfs(dirpath) return stat.f_frsize * stat.f_bavail + def space_used_by_directory(dirpath): stat = os.statvfs(dirpath) return stat.f_frsize * stat.f_blocks diff --git a/src/yunohost/utils/network.py b/src/yunohost/utils/network.py index a9602ff56..1f82a87b0 100644 --- a/src/yunohost/utils/network.py +++ b/src/yunohost/utils/network.py @@ -71,7 +71,7 @@ def get_gateway(): return addr.popitem()[1] if len(addr) == 1 else None -############################################################################### +# def _extract_inet(string, skip_netmask=False, skip_loopback=True): diff --git a/src/yunohost/utils/packages.py b/src/yunohost/utils/packages.py index 3917ef563..5ef97618b 100644 --- a/src/yunohost/utils/packages.py +++ b/src/yunohost/utils/packages.py @@ -33,6 +33,7 @@ logger = logging.getLogger('yunohost.utils.packages') # Exceptions ----------------------------------------------------------------- class PackageException(Exception): + """Base exception related to a package Represent an exception related to the package named `pkgname`. If no @@ -50,16 +51,19 @@ class PackageException(Exception): class UnknownPackage(PackageException): + """The package is not found in the cache.""" message_key = 'package_unknown' class UninstalledPackage(PackageException): + """The package is not installed.""" message_key = 'package_not_installed' class InvalidSpecifier(ValueError): + """An invalid specifier was found.""" @@ -68,6 +72,7 @@ class InvalidSpecifier(ValueError): # See: https://github.com/pypa/packaging class Specifier(object): + """Unique package version specifier Restrict a package version according to the `spec`. It must be a string @@ -257,6 +262,7 @@ class Specifier(object): class SpecifierSet(object): + """A set of package version specifiers Combine several Specifier separated by a comma. It allows to restrict diff --git a/src/yunohost/utils/password.py b/src/yunohost/utils/password.py index 9cee56d53..b4f7025f7 100644 --- a/src/yunohost/utils/password.py +++ b/src/yunohost/utils/password.py @@ -38,9 +38,11 @@ STRENGTH_LEVELS = [ (12, 1, 1, 1, 1), ] + def assert_password_is_strong_enough(profile, password): PasswordValidator(profile).validate(password) + class PasswordValidator(object): def __init__(self, profile): @@ -157,7 +159,7 @@ class PasswordValidator(object): # and the strength of the password (e.g. [11, 2, 7, 2, 0]) # and compare the values 1-by-1. # If one False is found, the password does not satisfy the level - if False in [s>=c for s, c in zip(strength, level_criterias)]: + if False in [s >= c for s, c in zip(strength, level_criterias)]: break # Otherwise, the strength of the password is at least of the current level. strength_level = level + 1 @@ -186,7 +188,7 @@ if __name__ == '__main__': if len(sys.argv) < 2: import getpass pwd = getpass.getpass("") - #print("usage: password.py PASSWORD") + # print("usage: password.py PASSWORD") else: pwd = sys.argv[1] status, msg = PasswordValidator('user').validation_summary(pwd) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 436f94911..3d80ddd4e 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -5,6 +5,7 @@ import json from yunohost.utils.error import YunohostError + def yunopaste(data): paste_server = "https://paste.yunohost.org" From 46972788b2f4c3b3ac79e2d2fb9b8dd6a3834148 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Dec 2018 15:05:56 +0100 Subject: [PATCH 244/250] Add comment about the motivation behind YunohostError --- src/yunohost/utils/error.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index 7c00ee5a4..a3842547e 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -23,7 +23,12 @@ from moulinette.core import MoulinetteError from moulinette import m18n class YunohostError(MoulinetteError): - """Yunohost base exception""" + """ + Yunohost base exception + + The (only?) main difference with MoulinetteError being that keys + are translated via m18n.n (namespace) instead of m18n.g (global?) + """ def __init__(self, key, __raw_msg__=False, *args, **kwargs): if __raw_msg__: msg = key From 7e3a90232f79b576c654bf26ee001631c5ef0dee Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 14 Dec 2018 14:17:20 +0000 Subject: [PATCH 245/250] __raw_msg__ -> raw_msg --- src/yunohost/app.py | 12 ++++++------ src/yunohost/utils/error.py | 6 +++--- src/yunohost/utils/yunopaste.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 36a0414ec..935b1ea6b 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -183,7 +183,7 @@ def app_fetchlist(url=None, name=None): with open(list_file, "w") as f: f.write(appslist) except Exception as e: - raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), __raw_msg__=True) + raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), raw_msg=True) now = int(time.time()) appslists[name]["lastUpdate"] = now @@ -840,9 +840,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on if install_retcode == -1: msg = m18n.n('operation_interrupted') + " " + error_msg - raise YunohostError(msg, __raw_msg__=True) + raise YunohostError(msg, raw_msg=True) msg = error_msg - raise YunohostError(msg, __raw_msg__=True) + raise YunohostError(msg, raw_msg=True) # Clean hooks and add new ones hook_remove(app_instance_name) @@ -1472,7 +1472,7 @@ def app_action_run(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) action_declaration = actions[action] @@ -1510,7 +1510,7 @@ def app_action_run(app, action, args=None): ) if retcode not in action_declaration.get("accepted_return_codes", [0]): - raise YunohostError("Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode), __raw_msg__=True) + raise YunohostError("Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode), raw_msg=True) os.remove(path) @@ -2431,7 +2431,7 @@ def _write_appslist_list(appslist_lists): json.dump(appslist_lists, f) except Exception as e: raise YunohostError("Error while writing list of appslist %s: %s" % - (APPSLISTS_JSON, str(e)), __raw_msg__=True) + (APPSLISTS_JSON, str(e)), raw_msg=True) def _register_new_appslist(url, name): diff --git a/src/yunohost/utils/error.py b/src/yunohost/utils/error.py index a3842547e..74a63db7c 100644 --- a/src/yunohost/utils/error.py +++ b/src/yunohost/utils/error.py @@ -29,10 +29,10 @@ class YunohostError(MoulinetteError): The (only?) main difference with MoulinetteError being that keys are translated via m18n.n (namespace) instead of m18n.g (global?) """ - def __init__(self, key, __raw_msg__=False, *args, **kwargs): - if __raw_msg__: + def __init__(self, key, raw_msg=False, *args, **kwargs): + if raw_msg: msg = key else: msg = m18n.n(key, *args, **kwargs) - super(YunohostError, self).__init__(msg, __raw_msg__=True) + super(YunohostError, self).__init__(msg, raw_msg=True) diff --git a/src/yunohost/utils/yunopaste.py b/src/yunohost/utils/yunopaste.py index 436f94911..9226a7b12 100644 --- a/src/yunohost/utils/yunopaste.py +++ b/src/yunohost/utils/yunopaste.py @@ -12,14 +12,14 @@ def yunopaste(data): 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) From 44f65651299ca3664c7eeb1bc939e4e5818e07eb Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 14 Dec 2018 21:00:45 +0100 Subject: [PATCH 246/250] Update locales/en.json Co-Authored-By: alexAubin --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 8e4e18497..01b74a1dc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -134,7 +134,7 @@ "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "confirm_app_install_warning": "Warning : this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway ? [{answers:s}] ", - "confirm_app_install_danger": "WARNING ! This application is still experimental (if not explicitly not working) and it is likely to break your system ! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", + "confirm_app_install_danger": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ", "confirm_app_install_thirdparty": "WARNING ! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", From ce1773d2ff6615b478f39b23e9a62722a6c8d4dc Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 14 Dec 2018 21:01:18 +0100 Subject: [PATCH 247/250] Update locales/en.json Co-Authored-By: alexAubin --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 01b74a1dc..09101b93c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -133,7 +133,7 @@ "certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})", "certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})", "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", - "confirm_app_install_warning": "Warning : this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway ? [{answers:s}] ", + "confirm_app_install_warning": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ", "confirm_app_install_danger": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ", "confirm_app_install_thirdparty": "WARNING ! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", From 65970e30fbd41b665c9d522c5f03d2b841885585 Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 14 Dec 2018 21:01:30 +0100 Subject: [PATCH 248/250] Update locales/en.json Co-Authored-By: alexAubin --- locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en.json b/locales/en.json index 09101b93c..319feb064 100644 --- a/locales/en.json +++ b/locales/en.json @@ -135,7 +135,7 @@ "certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})", "confirm_app_install_warning": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ", "confirm_app_install_danger": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ", - "confirm_app_install_thirdparty": "WARNING ! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk ? [{answers:s}] ", + "confirm_app_install_thirdparty": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ", "custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}", "custom_appslist_name_required": "You must provide a name for your custom app list", "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}", From 2c41a4539e4fd68a7464ce273280dd65acbb780a Mon Sep 17 00:00:00 2001 From: Bram Date: Fri, 14 Dec 2018 21:01:46 +0100 Subject: [PATCH 249/250] Update src/yunohost/app.py Co-Authored-By: alexAubin --- 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 e9a4b67f1..cf2e24a4f 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -725,7 +725,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on if confirm is None or force or msettings.get('interface') == 'api': return - answer = msignals.prompt(m18n.n('confirm_app_install_'+confirm, + answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm, answers='Y/N')) if answer.upper() != "Y": raise MoulinetteError(errno.EINVAL, m18n.n("aborting")) From a6f4f13031c6b1b9c68c7b57b08f6866f554aa36 Mon Sep 17 00:00:00 2001 From: Tristan Hill Date: Sat, 15 Dec 2018 07:52:07 +0000 Subject: [PATCH 250/250] Missing import --- 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 bb39dd58e..d77ee4b1e 100644 --- a/src/yunohost/user.py +++ b/src/yunohost/user.py @@ -285,7 +285,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None, remove_mailalias -- Mail aliases to remove """ - from yunohost.domain import domain_list + from yunohost.domain import domain_list, _get_maindomain from yunohost.app import app_ssowatconf from yunohost.utils.password import assert_password_is_strong_enough