Merge branch 'dev' into permission_protection

This commit is contained in:
Alexandre Aubin 2020-09-08 17:06:49 +02:00 committed by GitHub
commit 83585b2375
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1424 additions and 1205 deletions

View file

@ -38,7 +38,8 @@ build-ssowat:
variables: variables:
PACKAGE: "ssowat" PACKAGE: "ssowat"
script: script:
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "ssowat \([>,=,<]+ .*\)" | grep -Po "[0-9]+([.][0-9]+)?" | head -n 1)
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE
- *build_script - *build_script
@ -47,6 +48,7 @@ build-moulinette:
variables: variables:
PACKAGE: "moulinette" PACKAGE: "moulinette"
script: script:
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1 - DEBIAN_DEPENDS=$(cat debian/control | tr "," "\n" | grep -Po "moulinette \([>,=,<]+ .*\)" | grep -Po "[0-9]+([.][0-9]+)?" | head -n 1)
- git clone $YNH_SOURCE/$PACKAGE -b $CI_COMMIT_REF_NAME $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE -b $DEBIAN_DEPENDS $YNH_BUILD_DIR/$PACKAGE --depth 1 || git clone $YNH_SOURCE/$PACKAGE $YNH_BUILD_DIR/$PACKAGE --depth 1
- DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE - DEBIAN_FRONTEND=noninteractive apt --assume-yes -o Dpkg::Options::="--force-confold" build-dep $(pwd)/$YNH_BUILD_DIR/$PACKAGE
- *build_script - *build_script

View file

@ -5,70 +5,28 @@ import os
import sys import sys
import argparse import argparse
# Either we are in a development environment or not sys.path.insert(0, "/usr/lib/moulinette/")
IN_DEVEL = False import yunohost
# Level for which loggers will log
LOGGERS_LEVEL = 'DEBUG'
TTY_LOG_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - tty: log to current tty
LOGGERS_HANDLERS = ['file', 'tty']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-cli.log'
# Check and load - as needed - development environment
if not __file__.startswith('/usr/'):
IN_DEVEL = True
if IN_DEVEL:
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir(os.path.join(basedir, 'moulinette')):
sys.path.insert(0, basedir)
LOG_DIR = os.path.join(basedir, 'log')
import moulinette
from moulinette.actionsmap import ActionsMap
from moulinette.interfaces.cli import colorize, get_locale
# Initialization & helpers functions -----------------------------------
def _die(message, title='Error:'):
"""Print error message and exit"""
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _parse_cli_args(): def _parse_cli_args():
"""Parse additional arguments for the cli""" """Parse additional arguments for the cli"""
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--no-cache',
action='store_false', default=True, dest='use_cache',
help="Don't use actions map cache",
)
parser.add_argument('--output-as', parser.add_argument('--output-as',
choices=['json', 'plain', 'none'], default=None, choices=['json', 'plain', 'none'], default=None,
help="Output result in another format", help="Output result in another format"
) )
parser.add_argument('--debug', parser.add_argument('--debug',
action='store_true', default=False, action='store_true', default=False,
help="Log and print debug messages", help="Log and print debug messages"
) )
parser.add_argument('--quiet', parser.add_argument('--quiet',
action='store_true', default=False, action='store_true', default=False,
help="Don't produce any output", help="Don't produce any output"
) )
parser.add_argument('--timeout', parser.add_argument('--timeout',
type=int, default=None, type=int, default=None,
help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock", help="Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock"
)
parser.add_argument('--admin-password',
default=None, dest='password', metavar='PASSWORD',
help="The admin password to use to authenticate",
) )
# deprecated arguments # deprecated arguments
parser.add_argument('--plain', parser.add_argument('--plain',
@ -88,96 +46,6 @@ def _parse_cli_args():
return (parser, opts, args) return (parser, opts, args)
def _init_moulinette(debug=False, quiet=False):
"""Configure logging and initialize the moulinette"""
# Define loggers handlers
handlers = set(LOGGERS_HANDLERS)
if quiet and 'tty' in handlers:
handlers.remove('tty')
elif 'tty' not in handlers:
handlers.append('tty')
root_handlers = set(handlers)
if not debug and 'tty' in root_handlers:
root_handlers.remove('tty')
# Define loggers level
level = LOGGERS_LEVEL
tty_level = TTY_LOG_LEVEL
if debug:
tty_level = 'DEBUG'
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'tty': {
'level': tty_level,
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': 'tty-debug' if debug else '',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': level,
'handlers': handlers,
'propagate': False,
},
'moulinette': {
'level': level,
'handlers': [],
'propagate': True,
},
'moulinette.interface': {
'level': level,
'handlers': handlers,
'propagate': False,
},
},
'root': {
'level': level,
'handlers': root_handlers,
},
}
# Create log directory
if not os.path.isdir(LOG_DIR):
try:
os.makedirs(LOG_DIR, 0750)
except os.error as e:
_die(str(e))
# Initialize moulinette
moulinette.init(logging_config=logging, _from_source=IN_DEVEL)
def _retrieve_namespaces():
"""Return the list of namespaces to load"""
ret = ['yunohost']
for n in ActionsMap.get_namespaces():
# Append YunoHost modules
if n.startswith('ynh_'):
ret.append(n)
return ret
# Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ... # Stupid PATH management because sometimes (e.g. some cron job) PATH is only /usr/bin:/bin ...
default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" default_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
@ -188,33 +56,18 @@ if os.environ["PATH"] != default_path:
if __name__ == '__main__': if __name__ == '__main__':
if os.geteuid() != 0: if os.geteuid() != 0:
# since moulinette isn't initialized, we can't use m18n here sys.stderr.write("\033[1;31mError:\033[0m yunohost command must be "
sys.stderr.write("\033[1;31mError:\033[0m yunohost command must be " \
"run as root or with sudo.\n") "run as root or with sudo.\n")
sys.exit(1) sys.exit(1)
parser, opts, args = _parse_cli_args() parser, opts, args = _parse_cli_args()
_init_moulinette(opts.debug, opts.quiet)
# Check that YunoHost is installed
if not os.path.isfile('/etc/yunohost/installed') and \
(len(args) < 2 or (args[0] +' '+ args[1] != 'tools postinstall' and \
args[0] +' '+ args[1] != 'backup restore' and \
args[0] +' '+ args[1] != 'log display')):
from moulinette import m18n
# Init i18n
m18n.load_namespace('yunohost')
m18n.set_locale(get_locale())
# Print error and exit
_die(m18n.n('yunohost_not_installed'), m18n.g('error'))
# Execute the action # Execute the action
ret = moulinette.cli( yunohost.cli(
_retrieve_namespaces(), args, debug=opts.debug,
use_cache=opts.use_cache, output_as=opts.output_as, quiet=opts.quiet,
password=opts.password, parser_kwargs={'top_parser': parser}, output_as=opts.output_as,
timeout=opts.timeout, timeout=opts.timeout,
args=args,
parser=parser
) )
sys.exit(ret)

View file

@ -1,52 +1,16 @@
#! /usr/bin/python #! /usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import sys import sys
import argparse import argparse
# Either we are in a development environment or not sys.path.insert(0, "/usr/lib/moulinette/")
IN_DEVEL = False import yunohost
# Default server configuration # Default server configuration
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 6787 DEFAULT_PORT = 6787
# Level for which loggers will log
LOGGERS_LEVEL = 'DEBUG'
API_LOGGER_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
# - api: serve logs through the api
# - console: log to stderr
LOGGERS_HANDLERS = ['file', 'api']
# Directory and file to be used by logging
LOG_DIR = '/var/log/yunohost'
LOG_FILE = 'yunohost-api.log'
# Check and load - as needed - development environment
if not __file__.startswith('/usr/'):
IN_DEVEL = True
if IN_DEVEL:
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
if os.path.isdir(os.path.join(basedir, 'moulinette')):
sys.path.insert(0, basedir)
LOG_DIR = os.path.join(basedir, 'log')
import moulinette
from moulinette.actionsmap import ActionsMap
from moulinette.interfaces.cli import colorize
# Initialization & helpers functions -----------------------------------
def _die(message, title='Error:'):
"""Print error message and exit"""
print('%s %s' % (colorize(title, 'red'), message))
sys.exit(1)
def _parse_api_args(): def _parse_api_args():
"""Parse main arguments for the api""" """Parse main arguments for the api"""
@ -62,149 +26,19 @@ def _parse_api_args():
action='store', default=DEFAULT_PORT, type=int, action='store', default=DEFAULT_PORT, type=int,
help="Port to listen on (default: %d)" % DEFAULT_PORT, help="Port to listen on (default: %d)" % DEFAULT_PORT,
) )
srv_group.add_argument('--no-websocket',
action='store_true', default=True, dest='use_websocket',
help="Serve without WebSocket support, used to handle "
"asynchronous responses such as the messages",
)
glob_group = parser.add_argument_group('global arguments') glob_group = parser.add_argument_group('global arguments')
glob_group.add_argument('--no-cache',
action='store_false', default=True, dest='use_cache',
help="Don't use actions map cache",
)
glob_group.add_argument('--debug', glob_group.add_argument('--debug',
action='store_true', default=False, action='store_true', default=False,
help="Set log level to DEBUG", help="Set log level to DEBUG",
) )
glob_group.add_argument('--verbose',
action='store_true', default=False,
help="Be verbose in the output",
)
glob_group.add_argument('--help', glob_group.add_argument('--help',
action='help', help="Show this help message and exit", action='help', help="Show this help message and exit",
) )
return parser.parse_args() return parser.parse_args()
def _init_moulinette(use_websocket=True, debug=False, verbose=False):
"""Configure logging and initialize the moulinette"""
# Define loggers handlers
handlers = set(LOGGERS_HANDLERS)
if not use_websocket and 'api' in handlers:
handlers.remove('api')
if verbose and 'console' not in handlers:
handlers.add('console')
root_handlers = handlers - set(['api'])
# Define loggers level
level = LOGGERS_LEVEL
api_level = API_LOGGER_LEVEL
if debug:
level = 'DEBUG'
api_level = 'DEBUG'
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'console': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'api': {
'level': api_level,
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {
'class': 'logging.handlers.WatchedFileHandler',
'formatter': 'precise',
'filename': '%s/%s' % (LOG_DIR, LOG_FILE),
'filters': ['action'],
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
'stream': 'ext://sys.stdout',
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': level,
'handlers': handlers,
'propagate': False,
},
'moulinette': {
'level': level,
'handlers': [],
'propagate': True,
},
'gnupg': {
'level': 'INFO',
'handlers': [],
'propagate': False,
},
},
'root': {
'level': level,
'handlers': root_handlers,
},
}
# Create log directory
if not os.path.isdir(LOG_DIR):
try:
os.makedirs(LOG_DIR, 0750)
except os.error as e:
_die(str(e))
# Initialize moulinette
moulinette.init(logging_config=logging, _from_source=IN_DEVEL)
def _retrieve_namespaces():
"""Return the list of namespaces to load"""
ret = ['yunohost']
for n in ActionsMap.get_namespaces():
# Append YunoHost modules
if n.startswith('ynh_'):
ret.append(n)
return ret
# Callbacks for additional routes --------------------------------------
def is_installed():
"""
Check whether YunoHost is installed or not
"""
installed = False
if os.path.isfile('/etc/yunohost/installed'):
installed = True
return { 'installed': installed }
# Main action ----------------------------------------------------------
if __name__ == '__main__': if __name__ == '__main__':
opts = _parse_api_args() opts = _parse_api_args()
_init_moulinette(opts.use_websocket, opts.debug, opts.verbose)
# Run the server # Run the server
ret = moulinette.api( yunohost.api(debug=opts.debug, host=opts.host, port=opts.port)
_retrieve_namespaces(),
host=opts.host, port=opts.port, routes={
('GET', '/installed'): is_installed,
}, use_cache=opts.use_cache, use_websocket=opts.use_websocket
)
sys.exit(ret)

View file

@ -99,13 +99,7 @@ user:
- "pattern_lastname" - "pattern_lastname"
-m: -m:
full: --mail full: --mail
help: Main unique email address help: (Deprecated, see --domain) Main unique email address
extra:
ask: ask_email
required: True
pattern: &pattern_email
- !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+([^\W\d_]{2,})$
- "pattern_email"
-p: -p:
full: --password full: --password
help: User password help: User password
@ -116,6 +110,13 @@ user:
- !!str ^.{3,}$ - !!str ^.{3,}$
- "pattern_password" - "pattern_password"
comment: good_practices_about_user_password comment: good_practices_about_user_password
-d:
full: --domain
help: Domain for the email address and xmpp account
extra:
pattern: &pattern_domain
- !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
- "pattern_domain"
-q: -q:
full: --mailbox-quota full: --mailbox-quota
help: Mailbox size quota help: Mailbox size quota
@ -157,7 +158,9 @@ user:
-m: -m:
full: --mail full: --mail
extra: extra:
pattern: *pattern_email pattern: &pattern_email
- !!str ^[\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$
- "pattern_email"
-p: -p:
full: --change-password full: --change-password
help: New password to set help: New password to set
@ -428,9 +431,7 @@ domain:
domain: domain:
help: Domain name to add help: Domain name to add
extra: extra:
pattern: &pattern_domain pattern: *pattern_domain
- !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+([^\W\d_]{2,})$
- "pattern_domain"
-d: -d:
full: --dyndns full: --dyndns
help: Subscribe to the DynDNS service help: Subscribe to the DynDNS service
@ -669,6 +670,10 @@ app:
-f: -f:
full: --file full: --file
help: Folder or tarball for upgrade help: Folder or tarball for upgrade
-F:
full: --force
help: Force the update, even though the app is up to date
action: store_true
### app_change_url() ### app_change_url()
change-url: change-url:
@ -856,10 +861,6 @@ backup:
-o: -o:
full: --output-directory full: --output-directory
help: Output directory for the backup help: Output directory for the backup
-r:
full: --no-compress
help: Do not create an archive file
action: store_true
--methods: --methods:
help: List of backup methods to apply (copy or tar by default) help: List of backup methods to apply (copy or tar by default)
nargs: "*" nargs: "*"
@ -1640,17 +1641,19 @@ log:
action_help: List logs action_help: List logs
api: GET /logs api: GET /logs
arguments: arguments:
category:
help: Log category to display (default operations), could be operation, history, package, system, access, service or app
nargs: "*"
-l: -l:
full: --limit full: --limit
help: Maximum number of logs help: Maximum number of operations to list (default to 50)
type: int type: int
default: 50
-d: -d:
full: --with-details full: --with-details
help: Show additional infos (e.g. operation success) but may significantly increase command time. Consider using --limit in combination with this. help: Show additional infos (e.g. operation success) but may significantly increase command time. Consider using --limit in combination with this.
action: store_true action: store_true
-s:
full: --with-suboperations
help: Include metadata about operations that are not the main operation but are sub-operations triggered by another ongoing operation... (e.g. initializing groups/permissions when installing an app)
action: store_true
### log_display() ### log_display()
display: display:
@ -1671,6 +1674,10 @@ log:
full: --filter-irrelevant full: --filter-irrelevant
help: Do not show some lines deemed not relevant (like set +x or helper argument parsing) help: Do not show some lines deemed not relevant (like set +x or helper argument parsing)
action: store_true action: store_true
-s:
full: --with-suboperations
help: Include metadata about sub-operations of this operation... (e.g. initializing groups/permissions when installing an app)
action: store_true
############################# #############################

View file

@ -15,15 +15,17 @@ THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ACTIONSMAP_FILE = THIS_SCRIPT_DIR + '/yunohost.yml' ACTIONSMAP_FILE = THIS_SCRIPT_DIR + '/yunohost.yml'
BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + '/../bash-completion.d/yunohost' BASH_COMPLETION_FILE = THIS_SCRIPT_DIR + '/../bash-completion.d/yunohost'
def get_dict_actions(OPTION_SUBTREE, category): def get_dict_actions(OPTION_SUBTREE, category):
ACTIONS = [action for action in OPTION_SUBTREE[category]["actions"].keys() ACTIONS = [action for action in OPTION_SUBTREE[category]["actions"].keys()
if not action.startswith('_')] if not action.startswith('_')]
ACTIONS_STR = '{}'.format(' '.join(ACTIONS)) ACTIONS_STR = '{}'.format(' '.join(ACTIONS))
DICT = { "actions_str": ACTIONS_STR } DICT = {"actions_str": ACTIONS_STR}
return DICT return DICT
with open(ACTIONSMAP_FILE, 'r') as stream: with open(ACTIONSMAP_FILE, 'r') as stream:
# Getting the dictionary containning what actions are possible per category # Getting the dictionary containning what actions are possible per category
@ -40,10 +42,10 @@ with open(ACTIONSMAP_FILE, 'r') as stream:
ACTIONS_DICT[category]["subcategories_str"] = "" ACTIONS_DICT[category]["subcategories_str"] = ""
if "subcategories" in OPTION_TREE[category].keys(): if "subcategories" in OPTION_TREE[category].keys():
SUBCATEGORIES = [ subcategory for subcategory in OPTION_TREE[category]["subcategories"].keys() ] SUBCATEGORIES = [subcategory for subcategory in OPTION_TREE[category]["subcategories"].keys()]
SUBCATEGORIES_STR = '{}'.format(' '.join(SUBCATEGORIES)) SUBCATEGORIES_STR = '{}'.format(' '.join(SUBCATEGORIES))
ACTIONS_DICT[category]["subcategories_str"] = SUBCATEGORIES_STR ACTIONS_DICT[category]["subcategories_str"] = SUBCATEGORIES_STR
for subcategory in SUBCATEGORIES: for subcategory in SUBCATEGORIES:

View file

@ -1,7 +1,8 @@
# -*- shell-script -*- # -*- shell-script -*-
# TODO : use --regex to validate against a namespace set +x
for helper in $(run-parts --list /usr/share/yunohost/helpers.d 2>/dev/null) ; do for helper in $(run-parts --list /usr/share/yunohost/helpers.d 2>/dev/null) ; do
[ -r $helper ] && . $helper || true [ -r $helper ] && . $helper || true
done done
set -x

View file

@ -265,12 +265,6 @@ ynh_install_app_dependencies () {
then then
# Re-add sury # Re-add sury
ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600 ynh_install_extra_repo --repo="https://packages.sury.org/php/ $(ynh_get_debian_release) main" --key="https://packages.sury.org/php/apt.gpg" --name=extra_php_version --priority=600
# Pin this sury repository to prevent sury of doing shit
for package_to_not_upgrade in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev"
do
ynh_pin_repo --package="$package_to_not_upgrade" --pin="origin \"packages.sury.org\"" --priority="-1" --name=extra_php_version --append
done
fi fi
fi fi
fi fi

View file

@ -364,12 +364,6 @@ ynh_install_php () {
# Set the default PHP version back as the default version for php-cli. # Set the default PHP version back as the default version for php-cli.
update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION
# Pin this extra repository after packages are installed to prevent sury of doing shit
for package_to_not_upgrade in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev"
do
ynh_pin_repo --package="$package_to_not_upgrade" --pin="origin \"packages.sury.org\"" --priority="-1" --name=extra_php_version --append
done
# Advertise service in admin panel # Advertise service in admin panel
yunohost service add php${phpversion}-fpm --log "/var/log/php${phpversion}-fpm.log" yunohost service add php${phpversion}-fpm --log "/var/log/php${phpversion}-fpm.log"
} }

View file

@ -436,9 +436,8 @@ ynh_app_upstream_version () {
local manifest local manifest
# Manage arguments with getopts # Manage arguments with getopts
ynh_handle_getopts_args "$@" ynh_handle_getopts_args "$@"
manifest="${manifest:-../manifest.json}"
version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") version_key=$YNH_APP_MANIFEST_VERSION
echo "${version_key/~ynh*/}" echo "${version_key/~ynh*/}"
} }
@ -461,9 +460,8 @@ ynh_app_package_version () {
local manifest local manifest
# Manage arguments with getopts # Manage arguments with getopts
ynh_handle_getopts_args "$@" ynh_handle_getopts_args "$@"
manifest="${manifest:-../manifest.json}"
version_key=$(ynh_read_manifest --manifest="$manifest" --manifest_key="version") version_key=$YNH_APP_MANIFEST_VERSION
echo "${version_key/*~ynh/}" echo "${version_key/*~ynh/}"
} }
@ -516,3 +514,49 @@ ynh_check_app_version_changed () {
fi fi
echo $return_value echo $return_value
} }
# Compare the current package version against another version given as an argument.
# This is really useful when we need to do some actions only for some old package versions.
#
# example: ynh_compare_current_package_version --comparison lt --version 2.3.2~ynh1
# This example will check if the installed version is lower than (lt) the version 2.3.2~ynh1
#
# Generally you might probably use it as follow in the upgrade script
#
# if ynh_compare_current_package_version --comparaison lt --version 2.3.2~ynh1
# then
# # Do something that is needed for the package version older than 2.3.2~ynh1
# fi
#
# usage: ynh_compare_current_package_version --comparison lt|le|eq|ne|ge|gt
# | arg: --comparison - Comparison type. Could be : lt (lower than), le (lower or equal),
# | eq (equal), ne (not equal), ge (greater or equal), gt (greater than)
# | arg: --version - The version to compare. Need to be a version in the yunohost package version type (like 2.3.1~ynh4)
#
# Return 0 if the evaluation is true. 1 if false.
#
# Requires YunoHost version 3.8.0 or higher.
ynh_compare_current_package_version() {
local legacy_args=cv
declare -Ar args_array=( [c]=comparison= [v]=version= )
local version
local comparison
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local current_version=$YNH_APP_CURRENT_VERSION
# Check the syntax of the versions
if [[ ! $version =~ '~ynh' ]] || [[ ! $current_version =~ '~ynh' ]]
then
ynh_die "Invalid argument for version."
fi
# Check validity of the comparator
if [[ ! $comparison =~ (lt|le|eq|ne|ge|gt) ]]; then
ynh_die "Invialid comparator must be : lt, le, eq, ne, ge, gt"
fi
# Return the return value of dpkg --compare-versions
dpkg --compare-versions $current_version $comparison $version
}

39
data/hooks/conf_regen/10-apt Executable file
View file

@ -0,0 +1,39 @@
#!/bin/bash
set -e
do_pre_regen() {
pending_dir=$1
mkdir --parents "${pending_dir}/etc/apt/preferences.d"
for package in "php" "php-fpm" "php-mysql" "php-xml" "php-zip" "php-mbstring" "php-ldap" "php-gd" "php-curl" "php-bz2" "php-json" "php-sqlite3" "php-intl" "openssl" "libssl1.1" "libssl-dev"
do
echo "
Package: $package
Pin: origin \"packages.sury.org\"
Pin-Priority: -1" >> "/etc/apt/preferences.d/extra_php_version"
done
}
do_post_regen() {
regen_conf_files=$1
}
FORCE=${2:-0}
DRY_RUN=${3:-0}
case "$1" in
pre)
do_pre_regen $4
;;
post)
do_post_regen $4
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

View file

@ -27,14 +27,13 @@ do_pre_regen() {
ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true) ipv6=$(curl -s -6 https://ip6.yunohost.org 2>/dev/null || true)
ynh_validate_ip6 "$ipv6" || ipv6='' ynh_validate_ip6 "$ipv6" || ipv6=''
export ipv4
export ipv6
# add domain conf files # add domain conf files
for domain in $YNH_DOMAINS; do for domain in $YNH_DOMAINS; do
cat domain.tpl \ export domain
| sed "s/{{ domain }}/${domain}/g" \ ynh_render_template "domain.tpl" "${dnsmasq_dir}/${domain}"
| sed "s/{{ ip }}/${ipv4}/g" \
> "${dnsmasq_dir}/${domain}"
[[ -n $ipv6 ]] \
&& echo "address=/${domain}/${ipv6}" >> "${dnsmasq_dir}/${domain}"
done done
# remove old domain conf files # remove old domain conf files

View file

@ -63,10 +63,10 @@ class BaseSystemDiagnoser(Diagnoser):
ynh_core_version = ynh_packages["yunohost"]["version"] ynh_core_version = ynh_packages["yunohost"]["version"]
consistent_versions = all(infos["version"][:3] == ynh_core_version[:3] for infos in ynh_packages.values()) consistent_versions = all(infos["version"][:3] == ynh_core_version[:3] for infos in ynh_packages.values())
ynh_version_details = [("diagnosis_basesystem_ynh_single_version", ynh_version_details = [("diagnosis_basesystem_ynh_single_version",
{"package":package, {"package": package,
"version": infos["version"], "version": infos["version"],
"repo": infos["repo"]} "repo": infos["repo"]}
) )
for package, infos in ynh_packages.items()] for package, infos in ynh_packages.items()]
yield dict(meta={"test": "ynh_versions"}, yield dict(meta={"test": "ynh_versions"},
@ -75,7 +75,6 @@ class BaseSystemDiagnoser(Diagnoser):
summary="diagnosis_basesystem_ynh_main_version" if consistent_versions else "diagnosis_basesystem_ynh_inconsistent_versions", summary="diagnosis_basesystem_ynh_main_version" if consistent_versions else "diagnosis_basesystem_ynh_inconsistent_versions",
details=ynh_version_details) details=ynh_version_details)
if self.is_vulnerable_to_meltdown(): if self.is_vulnerable_to_meltdown():
yield dict(meta={"test": "meltdown"}, yield dict(meta={"test": "meltdown"},
status="ERROR", status="ERROR",

View file

@ -3,9 +3,9 @@
import os import os
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.utils.error import YunohostError
from yunohost.service import _get_services from yunohost.service import _get_services
class PortsDiagnoser(Diagnoser): class PortsDiagnoser(Diagnoser):
id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]

View file

@ -8,7 +8,6 @@ from moulinette.utils.filesystem import read_file
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.domain import domain_list from yunohost.domain import domain_list
from yunohost.utils.error import YunohostError
DIAGNOSIS_SERVER = "diagnosis.yunohost.org" DIAGNOSIS_SERVER = "diagnosis.yunohost.org"

View file

@ -47,7 +47,6 @@ class MailDiagnoser(Diagnoser):
status="SUCCESS", status="SUCCESS",
summary="diagnosis_mail_" + name + "_ok") summary="diagnosis_mail_" + name + "_ok")
def check_outgoing_port_25(self): def check_outgoing_port_25(self):
""" """
Check outgoing port 25 is open and not blocked by router Check outgoing port 25 is open and not blocked by router
@ -64,7 +63,6 @@ class MailDiagnoser(Diagnoser):
details=["diagnosis_mail_outgoing_port_25_blocked_details", details=["diagnosis_mail_outgoing_port_25_blocked_details",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn"]) "diagnosis_mail_outgoing_port_25_blocked_relay_vpn"])
def check_ehlo(self): def check_ehlo(self):
""" """
Check the server is reachable from outside and it's the good one Check the server is reachable from outside and it's the good one
@ -99,7 +97,6 @@ class MailDiagnoser(Diagnoser):
summary="diagnosis_mail_ehlo_wrong", summary="diagnosis_mail_ehlo_wrong",
details=["diagnosis_mail_ehlo_wrong_details"]) details=["diagnosis_mail_ehlo_wrong_details"])
def check_fcrdns(self): def check_fcrdns(self):
""" """
Check the reverse DNS is well defined by doing a Forward-confirmed Check the reverse DNS is well defined by doing a Forward-confirmed
@ -148,7 +145,6 @@ class MailDiagnoser(Diagnoser):
summary="diagnosis_mail_fcrdns_different_from_ehlo_domain", summary="diagnosis_mail_fcrdns_different_from_ehlo_domain",
details=details) details=details)
def check_blacklist(self): def check_blacklist(self):
""" """
Check with dig onto blacklist DNS server Check with dig onto blacklist DNS server
@ -225,7 +221,6 @@ class MailDiagnoser(Diagnoser):
status="SUCCESS", status="SUCCESS",
summary="diagnosis_mail_queue_ok") summary="diagnosis_mail_queue_ok")
def get_ips_checked(self): def get_ips_checked(self):
outgoing_ipversions = [] outgoing_ipversions = []
outgoing_ips = [] outgoing_ips = []
@ -245,5 +240,6 @@ class MailDiagnoser(Diagnoser):
outgoing_ips.append(global_ipv6) outgoing_ips.append(global_ipv6)
return (outgoing_ipversions, outgoing_ips) return (outgoing_ipversions, outgoing_ips)
def main(args, env, loggers): def main(args, env, loggers):
return MailDiagnoser(args, env, loggers).diagnose() return MailDiagnoser(args, env, loggers).diagnose()

View file

@ -5,6 +5,7 @@ import os
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.service import service_status from yunohost.service import service_status
class ServicesDiagnoser(Diagnoser): class ServicesDiagnoser(Diagnoser):
id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
@ -36,5 +37,6 @@ class ServicesDiagnoser(Diagnoser):
yield item yield item
def main(args, env, loggers): def main(args, env, loggers):
return ServicesDiagnoser(args, env, loggers).diagnose() return ServicesDiagnoser(args, env, loggers).diagnose()

View file

@ -1,9 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
import os import os
import psutil import psutil
import subprocess
import datetime
import re
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
class SystemResourcesDiagnoser(Diagnoser): class SystemResourcesDiagnoser(Diagnoser):
id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1] id_ = os.path.splitext(os.path.basename(__file__))[0].split("-")[1]
@ -13,7 +17,7 @@ class SystemResourcesDiagnoser(Diagnoser):
def run(self): def run(self):
MB = 1024**2 MB = 1024**2
GB = MB*1024 GB = MB * 1024
# #
# RAM # RAM
@ -76,7 +80,7 @@ class SystemResourcesDiagnoser(Diagnoser):
# N.B.: we do not use usage.total because we want # N.B.: we do not use usage.total because we want
# to take into account the 5% security margin # to take into account the 5% security margin
# correctly (c.f. the doc of psutil ...) # correctly (c.f. the doc of psutil ...)
"total": human_size(usage.used+usage.free), "total": human_size(usage.used + usage.free),
"free": human_size(usage.free), "free": human_size(usage.free),
"free_percent": free_percent}) "free_percent": free_percent})
@ -93,13 +97,54 @@ class SystemResourcesDiagnoser(Diagnoser):
item["status"] = "SUCCESS" item["status"] = "SUCCESS"
item["summary"] = "diagnosis_diskusage_ok" item["summary"] = "diagnosis_diskusage_ok"
yield item yield item
#
# Recent kills by oom_reaper
#
kills_count = self.recent_kills_by_oom_reaper()
if kills_count:
kills_summary = "\n".join(["%s (x%s)" % (proc, count) for proc, count in kills_count])
yield dict(meta={"test": "oom_reaper"},
status="WARNING",
summary="diagnosis_processes_killed_by_oom_reaper",
data={"kills_summary": kills_summary})
def recent_kills_by_oom_reaper(self):
if not os.path.exists("/var/log/kern.log"):
return []
def analyzed_kern_log():
cmd = 'tail -n 10000 /var/log/kern.log | grep "oom_reaper: reaped process" || true'
out = subprocess.check_output(cmd, shell=True)
lines = out.strip().split("\n")
now = datetime.datetime.now()
for line in reversed(lines):
# Lines look like :
# Aug 25 18:48:21 yolo kernel: [ 9623.613667] oom_reaper: reaped process 11509 (uwsgi), now anon-rss:0kB, file-rss:0kB, shmem-rss:328kB
date_str = str(now.year) + " " + " ".join(line.split()[:3])
date = datetime.datetime.strptime(date_str, '%Y %b %d %H:%M:%S')
diff = now - date
if diff.days >= 1:
break
process_killed = re.search(r"\(.*\)", line).group().strip("()")
yield process_killed
processes = list(analyzed_kern_log())
kills_count = [(p, len([p_ for p_ in processes if p_ == p])) for p in set(processes)]
kills_count = sorted(kills_count, key=lambda p: p[1], reverse=True)
return kills_count
def human_size(bytes_): def human_size(bytes_):
# Adapted from https://stackoverflow.com/a/1094933 # Adapted from https://stackoverflow.com/a/1094933
for unit in ['','ki','Mi','Gi','Ti','Pi','Ei','Zi']: for unit in ['', 'ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
if abs(bytes_) < 1024.0: if abs(bytes_) < 1024.0:
return "%s %sB" % (round_(bytes_), unit) return "%s %sB" % (round_(bytes_), unit)
bytes_ /= 1024.0 bytes_ /= 1024.0
@ -114,5 +159,6 @@ def round_(n):
n = int(round(n)) n = int(round(n))
return n return n
def main(args, env, loggers): def main(args, env, loggers):
return SystemResourcesDiagnoser(args, env, loggers).diagnose() return SystemResourcesDiagnoser(args, env, loggers).diagnose()

View file

@ -2,7 +2,6 @@
import os import os
import subprocess
from yunohost.diagnosis import Diagnoser from yunohost.diagnosis import Diagnoser
from yunohost.regenconf import _get_regenconf_infos, _calculate_hash from yunohost.regenconf import _get_regenconf_infos, _calculate_hash

View file

@ -1,4 +1,9 @@
address=/{{ domain }}/{{ ip }} address=/{{ domain }}/{{ ipv4 }}
address=/xmpp-upload.{{ domain }}/{{ ipv4 }}
{% if ipv6 %}
address=/{{ domain }}/{{ ipv6 }}
address=/xmpp-upload.{{ domain }}/{{ ipv6 }}
{% endif %}
txt-record={{ domain }},"v=spf1 mx a -all" txt-record={{ domain }},"v=spf1 mx a -all"
mx-host={{ domain }},{{ domain }},5 mx-host={{ domain }},{{ domain }},5
srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5 srv-host=_xmpp-client._tcp.{{ domain }},{{ domain }},5222,0,5

View file

@ -23,7 +23,7 @@ ssl_cert = </etc/yunohost/certs/{{ main_domain }}/crt.pem
ssl_key = </etc/yunohost/certs/{{ main_domain }}/key.pem ssl_key = </etc/yunohost/certs/{{ main_domain }}/key.pem
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dh = /usr/share/yunohost/other/ffdhe2048.pem; ssl_dh = </usr/share/yunohost/other/ffdhe2048.pem
# intermediate configuration # intermediate configuration
ssl_min_protocol = TLSv1.2 ssl_min_protocol = TLSv1.2

View file

@ -0,0 +1,7 @@
# Avoid the nginx path/alias traversal weakness ( #1037 )
rewrite ^/yunohost/sso$ /yunohost/sso/ permanent;
location /yunohost/sso/ {
# This is an empty location, only meant to avoid other locations
# from matching /yunohost/sso, such that it's correctly handled by ssowat
}

View file

@ -14,7 +14,7 @@ server {
include /etc/nginx/conf.d/{{ domain }}.d/*.conf; include /etc/nginx/conf.d/{{ domain }}.d/*.conf;
location /yunohost/admin { location /yunohost {
return 301 https://$http_host$request_uri; return 301 https://$http_host$request_uri;
} }
@ -60,6 +60,7 @@ server {
include /etc/nginx/conf.d/{{ domain }}.d/*.conf; include /etc/nginx/conf.d/{{ domain }}.d/*.conf;
include /etc/nginx/conf.d/yunohost_sso.conf.inc;
include /etc/nginx/conf.d/yunohost_admin.conf.inc; include /etc/nginx/conf.d/yunohost_admin.conf.inc;
include /etc/nginx/conf.d/yunohost_api.conf.inc; include /etc/nginx/conf.d/yunohost_api.conf.inc;

View file

@ -36,7 +36,7 @@ smtpd_tls_mandatory_ciphers = medium
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam.pem
# not actually 1024 bits, this applies to all DHE >= 1024 bits # not actually 1024 bits, this applies to all DHE >= 1024 bits
smtpd_tls_dh1024_param_file = /usr/share/yunohost/other/ffdhe2048.pem; smtpd_tls_dh1024_param_file = /usr/share/yunohost/other/ffdhe2048.pem
tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
{% else %} {% else %}
@ -45,7 +45,7 @@ tls_medium_cipherlist = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA25
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2 smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2
{% else %} {% endif %}
tls_preempt_cipherlist = no tls_preempt_cipherlist = no
############################################################################### ###############################################################################

43
debian/changelog vendored
View file

@ -1,3 +1,46 @@
yunohost (4.1.0) testing; urgency=low
- Tmp bump of the version number to fix CI (c.f. Breaks: yunohost(<<4.1) in moulinette)
yunohost (4.0.7) stable; urgency=low
- [fix] Require explicitly php7.3-foo packages because in some cases Sury's php7.4- packages are installed and php7.3-fpm doesn't get installed ... (1288159a)
- [fix] Make sure app nginx confs do not prevent the loading of /yunohost/sso (#1044)
Thanks to all contributors <3 ! (Kayou, ljf)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 04 Sep 2020 14:32:07 +0200
yunohost (4.0.6.1) stable; urgency=low
- [fix] Stupid syntax issue in dovecot conf
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 01 Sep 2020 02:00:19 +0200
yunohost (4.0.6) stable; urgency=low
- [mod] Add apt conf regen hook to manage sury pinning policy (#1041)
- [fix] Use proper templating + handle xmpp-upload.domain.tld in dnsmasq conf (bc7344b6, 503e08b5)
- [fix] Explicitly require php-fpm >= 7.3 ... (41813744)
- [i18n] Translations updated for Catalan, French, German
Thanks to all contributors <3 ! (Christian W., Titus PiJean, xaloc33)
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 31 Aug 2020 19:57:24 +0200
yunohost (4.0.5) testing; urgency=low
- [enh] Update postfix, dovecot, nginx configuration according to Mozilla guidelines (Buster + DH params) (f3a4334a, 89bcf1ba, 2d661737)
- [enh] Update acme_tiny to 4.1.0 (#1037)
- [fix] ref to variable in i18n string (c.f. issue 1647) (7b1f02e0)
- [fix] Recursively enforce ownership for rspamd (8454f2ec)
- [fix] Stupid encoding issue when fetching service description (6ec0e7b6)
- [fix] Misc fixes for CI (ca0a42f2, 485c65a9, #1038, a891d20a)
Thanks to all contributors <3 ! (Eric G., Kay0u)
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 25 Aug 2020 19:32:27 +0200
yunohost (4.0.4) stable; urgency=low yunohost (4.0.4) stable; urgency=low
- Debugging and robustness improvements for postgresql 9.6 -> 11 and xtables->nftables migrations (accc2da4, 59bd7d66, 4cb6f7fd, 4b14402c) - Debugging and robustness improvements for postgresql 9.6 -> 11 and xtables->nftables migrations (accc2da4, 59bd7d66, 4cb6f7fd, 4b14402c)

10
debian/control vendored
View file

@ -11,13 +11,13 @@ Package: yunohost
Essential: yes Essential: yes
Architecture: all Architecture: all
Depends: ${python:Depends}, ${misc:Depends} Depends: ${python:Depends}, ${misc:Depends}
, moulinette (>= 4.0.0~alpha), ssowat (>= 4.0.0~alpha) , moulinette (>= 4.1), ssowat (>= 4.0)
, python-psutil, python-requests, python-dnspython, python-openssl , python-psutil, python-requests, python-dnspython, python-openssl
, python-miniupnpc, python-dbus, python-jinja2 , python-miniupnpc, python-dbus, python-jinja2
, python-toml, python-packaging, python-publicsuffix , python-toml, python-packaging, python-publicsuffix
, apt, apt-transport-https, dirmngr , apt, apt-transport-https, dirmngr
, php-fpm, php-ldap, php-intl , php7.3-common, php7.3-fpm, php7.3-ldap, php7.3-intl
, mariadb-server, php-mysql | php-mysqlnd , mariadb-server, php7.3-mysql
, openssh-server, iptables, fail2ban, dnsutils, bind9utils , openssh-server, iptables, fail2ban, dnsutils, bind9utils
, openssl, ca-certificates, netcat-openbsd, iproute2 , openssl, ca-certificates, netcat-openbsd, iproute2
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd , slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
@ -28,11 +28,11 @@ Depends: ${python:Depends}, ${misc:Depends}
, redis-server , redis-server
, metronome (>=3.14.0) , metronome (>=3.14.0)
, git, curl, wget, cron, unzip, jq, bc , git, curl, wget, cron, unzip, jq, bc
, lsb-release, haveged, fake-hwclock, equivs, lsof, whois, python-publicsuffix , lsb-release, haveged, fake-hwclock, equivs, lsof, whois
Recommends: yunohost-admin Recommends: yunohost-admin
, ntp, inetutils-ping | iputils-ping , ntp, inetutils-ping | iputils-ping
, bash-completion, rsyslog , bash-completion, rsyslog
, php-gd, php-curl, php-gettext , php7.3-gd, php7.3-curl, php-gettext
, python-pip , python-pip
, unattended-upgrades , unattended-upgrades
, libdbd-ldap-perl, libnet-dns-perl , libdbd-ldap-perl, libnet-dns-perl

View file

@ -4,12 +4,13 @@ import os
import glob import glob
import datetime import datetime
def render(helpers): def render(helpers):
data = { "helpers": helpers, data = {"helpers": helpers,
"date": datetime.datetime.now().strftime("%m/%d/%Y"), "date": datetime.datetime.now().strftime("%m/%d/%Y"),
"version": open("../debian/changelog").readlines()[0].split()[1].strip("()") "version": open("../debian/changelog").readlines()[0].split()[1].strip("()")
} }
from jinja2 import Template from jinja2 import Template
from ansi2html import Ansi2HTMLConverter from ansi2html import Ansi2HTMLConverter
@ -22,13 +23,14 @@ def render(helpers):
return conv.convert(shell, False) return conv.convert(shell, False)
template = open("helper_doc_template.html", "r").read() template = open("helper_doc_template.html", "r").read()
t = Template(template) t = Template(template)
t.globals['now'] = datetime.datetime.utcnow t.globals['now'] = datetime.datetime.utcnow
result = t.render(data=data, convert=shell_to_html, shell_css=shell_css) result = t.render(data=data, convert=shell_to_html, shell_css=shell_css)
open("helpers.html", "w").write(result) open("helpers.html", "w").write(result)
############################################################################## ##############################################################################
class Parser(): class Parser():
def __init__(self, filename): def __init__(self, filename):
@ -42,15 +44,15 @@ class Parser():
self.blocks = [] self.blocks = []
current_reading = "void" current_reading = "void"
current_block = { "name": None, current_block = {"name": None,
"line": -1, "line": -1,
"comments": [], "comments": [],
"code": [] } "code": []}
for i, line in enumerate(self.file): for i, line in enumerate(self.file):
if line.startswith("#!/bin/bash"): if line.startswith("#!/bin/bash"):
continue continue
line = line.rstrip().replace("\t", " ") line = line.rstrip().replace("\t", " ")
@ -73,7 +75,7 @@ class Parser():
elif line.strip() == "": elif line.strip() == "":
# Well eh that was not an actual helper definition ... start over ? # Well eh that was not an actual helper definition ... start over ?
current_reading = "void" current_reading = "void"
current_block = { "name": None, current_block = {"name": None,
"line": -1, "line": -1,
"comments": [], "comments": [],
"code": [] "code": []
@ -101,10 +103,10 @@ class Parser():
# (we ignore helpers containing [internal] ...) # (we ignore helpers containing [internal] ...)
if not "[internal]" in current_block["comments"]: if not "[internal]" in current_block["comments"]:
self.blocks.append(current_block) self.blocks.append(current_block)
current_block = { "name": None, current_block = {"name": None,
"line": -1, "line": -1,
"comments": [], "comments": [],
"code": [] } "code": []}
else: else:
current_block["code"].append(line) current_block["code"].append(line)
@ -180,13 +182,14 @@ class Parser():
b["usage"] = b["usage"].strip() b["usage"] = b["usage"].strip()
def is_global_comment(line): def is_global_comment(line):
return line.startswith('#') return line.startswith('#')
def malformed_error(line_number): def malformed_error(line_number):
return "Malformed file line {} ?".format(line_number) return "Malformed file line {} ?".format(line_number)
def main(): def main():
helper_files = sorted(glob.glob("../data/helpers.d/*")) helper_files = sorted(glob.glob("../data/helpers.d/*"))
@ -204,5 +207,5 @@ def main():
render(helpers) render(helpers)
main()
main()

View file

@ -20,15 +20,15 @@
"app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}", "app_location_unavailable": "Aquesta URL no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}",
"app_manifest_invalid": "Hi ha algun error amb el manifest de l'aplicació: {error}", "app_manifest_invalid": "Hi ha algun error amb el manifest de l'aplicació: {error}",
"app_not_correctly_installed": "{app:s} sembla estar mal instal·lada", "app_not_correctly_installed": "{app:s} sembla estar mal instal·lada",
"app_not_installed": "No s'ha trobat l'aplicació «{app:s}» en la llista d'aplicacions instal·lades: {all_apps}", "app_not_installed": "No s'ha trobat {app:s} en la llista d'aplicacions instal·lades: {all_apps}",
"app_not_properly_removed": "{app:s} no s'ha pogut suprimir correctament", "app_not_properly_removed": "{app:s} no s'ha pogut suprimir correctament",
"app_removed": "{app:s} ha estat suprimida", "app_removed": "{app:s} ha estat suprimida",
"app_requirements_checking": "Verificació dels paquets requerits per {app}", "app_requirements_checking": "Verificació dels paquets requerits per {app}...",
"app_requirements_unmeet": "No es compleixen els requeriments per {app}, el paquet {pkgname} ({version}) ha de ser {spec}", "app_requirements_unmeet": "No es compleixen els requeriments per {app}, el paquet {pkgname} ({version}) ha de ser {spec}",
"app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?", "app_sources_fetch_failed": "No s'han pogut carregar els fitxers font, l'URL és correcta?",
"app_unknown": "Aplicació desconeguda", "app_unknown": "Aplicació desconeguda",
"app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat", "app_unsupported_remote_type": "El tipus remot utilitzat per l'aplicació no està suportat",
"app_upgrade_app_name": "Actualitzant {app}", "app_upgrade_app_name": "Actualitzant {app}...",
"app_upgrade_failed": "No s'ha pogut actualitzar {app:s}: {error}", "app_upgrade_failed": "No s'ha pogut actualitzar {app:s}: {error}",
"app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions", "app_upgrade_some_app_failed": "No s'han pogut actualitzar algunes aplicacions",
"app_upgraded": "S'ha actualitzat {app:s}", "app_upgraded": "S'ha actualitzat {app:s}",
@ -39,19 +39,19 @@
"ask_new_admin_password": "Nova contrasenya d'administrador", "ask_new_admin_password": "Nova contrasenya d'administrador",
"ask_password": "Contrasenya", "ask_password": "Contrasenya",
"backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat", "backup_abstract_method": "Encara està per implementar aquest mètode de còpia de seguretat",
"backup_app_failed": "No s'ha pogut fer la còpia de seguretat de l'aplicació \"{app:s}\"", "backup_app_failed": "No s'ha pogut fer la còpia de seguretat de {app:s}",
"backup_applying_method_borg": "Enviant tots els fitxers de la còpia de seguretat al repositori borg-backup", "backup_applying_method_borg": "Enviant tots els fitxers de la còpia de seguretat al repositori borg-backup...",
"backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat", "backup_applying_method_copy": "Còpia de tots els fitxers a la còpia de seguretat...",
"backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat \"{method:s}\"", "backup_applying_method_custom": "Crida del mètode de còpia de seguretat personalitzat \"{method:s}\"...",
"backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat", "backup_applying_method_tar": "Creació de l'arxiu TAR de la còpia de seguretat...",
"backup_archive_app_not_found": "No s'ha pogut trobar l'aplicació «{app:s}» dins l'arxiu de la còpia de seguretat", "backup_archive_app_not_found": "No s'ha pogut trobar {app:s} en l'arxiu de la còpia de seguretat",
"backup_archive_broken_link": "No s'ha pogut accedir a l'arxiu de la còpia de seguretat (enllaç invàlid cap a {path:s})", "backup_archive_broken_link": "No s'ha pogut accedir a l'arxiu de la còpia de seguretat (enllaç invàlid cap a {path:s})",
"backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom.", "backup_archive_name_exists": "Ja hi ha una còpia de seguretat amb aquest nom.",
"backup_archive_name_unknown": "Còpia de seguretat local \"{name:s}\" desconeguda", "backup_archive_name_unknown": "Còpia de seguretat local \"{name:s}\" desconeguda",
"backup_archive_open_failed": "No s'ha pogut obrir l'arxiu de la còpia de seguretat", "backup_archive_open_failed": "No s'ha pogut obrir l'arxiu de la còpia de seguretat",
"backup_archive_system_part_not_available": "La part «{part:s}» del sistema no està disponible en aquesta copia de seguretat", "backup_archive_system_part_not_available": "La part «{part:s}» del sistema no està disponible en aquesta copia de seguretat",
"backup_archive_writing_error": "No es poden afegir els arxius «{source:s}» (anomenats en l'arxiu «{dest:s}») a l'arxiu comprimit de la còpia de seguretat «{archive:s}»", "backup_archive_writing_error": "No es poden afegir els arxius «{source:s}» (anomenats en l'arxiu «{dest:s}») a l'arxiu comprimit de la còpia de seguretat «{archive:s}»",
"backup_ask_for_copying_if_needed": "Voleu fer la còpia de seguretat utilitzant {size:s} MB temporalment? (S'utilitza aquest mètode ja que alguns dels fitxers no s'han pogut preparar utilitzar un mètode més eficient.)", "backup_ask_for_copying_if_needed": "Voleu fer la còpia de seguretat utilitzant {size:s}MB temporalment? (S'utilitza aquest mètode ja que alguns dels fitxers no s'han pogut preparar utilitzar un mètode més eficient.)",
"backup_borg_not_implemented": "El mètode de còpia de seguretat Borg encara no està implementat", "backup_borg_not_implemented": "El mètode de còpia de seguretat Borg encara no està implementat",
"backup_cant_mount_uncompress_archive": "No es pot carregar l'arxiu descomprimit com a protegit contra escriptura", "backup_cant_mount_uncompress_archive": "No es pot carregar l'arxiu descomprimit com a protegit contra escriptura",
"backup_cleaning_failed": "No s'ha pogut netejar el directori temporal de la còpia de seguretat", "backup_cleaning_failed": "No s'ha pogut netejar el directori temporal de la còpia de seguretat",
@ -60,14 +60,14 @@
"backup_created": "S'ha creat la còpia de seguretat", "backup_created": "S'ha creat la còpia de seguretat",
"aborting": "Avortant.", "aborting": "Avortant.",
"app_not_upgraded": "L'aplicació «{failed_app}» no s'ha pogut actualitzar, i com a conseqüència s'ha cancel·lat l'actualització de les següents aplicacions: {apps}", "app_not_upgraded": "L'aplicació «{failed_app}» no s'ha pogut actualitzar, i com a conseqüència s'ha cancel·lat l'actualització de les següents aplicacions: {apps}",
"app_start_install": "instal·lant l'aplicació «{app}»…", "app_start_install": "instal·lant {app}...",
"app_start_remove": "Eliminant l'aplicació «{app}»…", "app_start_remove": "Eliminant {app}...",
"app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»", "app_start_backup": "Recuperant els fitxers pels que s'ha de fer una còpia de seguretat per «{app}»...",
"app_start_restore": "Recuperant l'aplicació «{app}»…", "app_start_restore": "Recuperant {app}...",
"app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}", "app_upgrade_several_apps": "S'actualitzaran les següents aplicacions: {apps}",
"ask_new_domain": "Nou domini", "ask_new_domain": "Nou domini",
"ask_new_path": "Nou camí", "ask_new_path": "Nou camí",
"backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats", "backup_actually_backuping": "Creant un arxiu de còpia de seguretat a partir dels fitxers recuperats...",
"backup_creation_failed": "No s'ha pogut crear l'arxiu de la còpia de seguretat", "backup_creation_failed": "No s'ha pogut crear l'arxiu de la còpia de seguretat",
"backup_csv_addition_failed": "No s'han pogut afegir fitxers per a fer-ne la còpia de seguretat al fitxer CSV", "backup_csv_addition_failed": "No s'han pogut afegir fitxers per a fer-ne la còpia de seguretat al fitxer CSV",
"backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a la restauració", "backup_csv_creation_failed": "No s'ha pogut crear el fitxer CSV necessari per a la restauració",
@ -81,7 +81,7 @@
"backup_method_copy_finished": "La còpia de la còpia de seguretat ha acabat", "backup_method_copy_finished": "La còpia de la còpia de seguretat ha acabat",
"backup_method_custom_finished": "El mètode de còpia de seguretat personalitzat \"{method:s}\" ha acabat", "backup_method_custom_finished": "El mètode de còpia de seguretat personalitzat \"{method:s}\" ha acabat",
"backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat TAR", "backup_method_tar_finished": "S'ha creat l'arxiu de còpia de seguretat TAR",
"backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració", "backup_mount_archive_for_restore": "Preparant l'arxiu per la restauració...",
"good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).", "good_practices_about_user_password": "Esteu a punt de definir una nova contrasenya d'usuari. La contrasenya ha de tenir un mínim de 8 caràcters; tot i que és de bona pràctica utilitzar una contrasenya més llarga (és a dir una frase de contrasenya) i/o utilitzar diferents tipus de caràcters (majúscules, minúscules, dígits i caràcters especials).",
"password_listed": "Aquesta contrasenya és una de les més utilitzades en el món. Si us plau utilitzeu-ne una més única.", "password_listed": "Aquesta contrasenya és una de les més utilitzades en el món. Si us plau utilitzeu-ne una més única.",
"password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters", "password_too_simple_1": "La contrasenya ha de tenir un mínim de 8 caràcters",
@ -95,11 +95,11 @@
"backup_output_directory_required": "Heu d'especificar un directori de sortida per la còpia de seguretat", "backup_output_directory_required": "Heu d'especificar un directori de sortida per la còpia de seguretat",
"backup_output_symlink_dir_broken": "El directori del arxiu «{path:s}» es un enllaç simbòlic trencat. Pot ser heu oblidat muntar, tornar a muntar o connectar el mitja d'emmagatzematge al que apunta.", "backup_output_symlink_dir_broken": "El directori del arxiu «{path:s}» es un enllaç simbòlic trencat. Pot ser heu oblidat muntar, tornar a muntar o connectar el mitja d'emmagatzematge al que apunta.",
"backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar PHP 7, pot ser que no es puguin restaurar les vostres aplicacions PHP (raó: {error:s})", "backup_php5_to_php7_migration_may_fail": "No s'ha pogut convertir l'arxiu per suportar PHP 7, pot ser que no es puguin restaurar les vostres aplicacions PHP (raó: {error:s})",
"backup_running_hooks": "Executant els scripts de la còpia de seguretat", "backup_running_hooks": "Executant els scripts de la còpia de seguretat...",
"backup_system_part_failed": "No s'ha pogut fer la còpia de seguretat de la part \"{part:s}\" del sistema", "backup_system_part_failed": "No s'ha pogut fer la còpia de seguretat de la part \"{part:s}\" del sistema",
"backup_unable_to_organize_files": "No s'ha pogut utilitzar el mètode ràpid per organitzar els fitxers dins de l'arxiu", "backup_unable_to_organize_files": "No s'ha pogut utilitzar el mètode ràpid per organitzar els fitxers dins de l'arxiu",
"backup_with_no_backup_script_for_app": "L'aplicació «{app:s}» no té un script de còpia de seguretat. Serà ignorat.", "backup_with_no_backup_script_for_app": "L'aplicació «{app:s}» no té un script de còpia de seguretat. Serà ignorat.",
"backup_with_no_restore_script_for_app": "L'aplicació «{app:s}» no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.", "backup_with_no_restore_script_for_app": "{app:s} no té un script de restauració, no podreu restaurar automàticament la còpia de seguretat d'aquesta aplicació.",
"certmanager_acme_not_configured_for_domain": "No s'ha pogut executar el ACME challenge pel domini {domain} en aquests moments ja que a la seva configuració de nginx li manca el codi corresponent… Assegureu-vos que la configuració nginx està actualitzada utilitzant «yunohost tools regen-conf nginx --dry-run --with-diff».", "certmanager_acme_not_configured_for_domain": "No s'ha pogut executar el ACME challenge pel domini {domain} en aquests moments ja que a la seva configuració de nginx li manca el codi corresponent… Assegureu-vos que la configuració nginx està actualitzada utilitzant «yunohost tools regen-conf nginx --dry-run --with-diff».",
"certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini «{domain:s}» no ha estat emès per Let's Encrypt. No es pot renovar automàticament!", "certmanager_attempt_to_renew_nonLE_cert": "El certificat pel domini «{domain:s}» no ha estat emès per Let's Encrypt. No es pot renovar automàticament!",
"certmanager_attempt_to_renew_valid_cert": "El certificat pel domini «{domain:s}» està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)", "certmanager_attempt_to_renew_valid_cert": "El certificat pel domini «{domain:s}» està a punt de caducar! (Utilitzeu --force si sabeu el que esteu fent)",
@ -109,7 +109,7 @@
"certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain:s}»", "certmanager_cert_install_success_selfsigned": "S'ha instal·lat correctament un certificat auto-signat pel domini «{domain:s}»",
"certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini «{domain:s}»", "certmanager_cert_renew_success": "S'ha renovat correctament el certificat Let's Encrypt pel domini «{domain:s}»",
"certmanager_cert_signing_failed": "No s'ha pogut firmar el nou certificat", "certmanager_cert_signing_failed": "No s'ha pogut firmar el nou certificat",
"certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain:s} ha fallat", "certmanager_certificate_fetching_or_enabling_failed": "Sembla que utilitzar el nou certificat per {domain:s} ha fallat...",
"certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració NGINX {filepath:s} entra en conflicte i s'ha d'eliminar primer", "certmanager_conflicting_nginx_file": "No s'ha pogut preparar el domini per al desafiament ACME: l'arxiu de configuració NGINX {filepath:s} entra en conflicte i s'ha d'eliminar primer",
"certmanager_couldnt_fetch_intermediate_cert": "S'ha exhaurit el temps d'esperar al intentar recollir el certificat intermedi des de Let's Encrypt. La instal·lació/renovació del certificat s'ha cancel·lat - torneu a intentar-ho més tard.", "certmanager_couldnt_fetch_intermediate_cert": "S'ha exhaurit el temps d'esperar al intentar recollir el certificat intermedi des de Let's Encrypt. La instal·lació/renovació del certificat s'ha cancel·lat - torneu a intentar-ho més tard.",
"certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu «--force» per fer-ho)", "certmanager_domain_cert_not_selfsigned": "El certificat pel domini {domain:s} no és auto-signat Esteu segur de voler canviar-lo? (Utilitzeu «--force» per fer-ho)",
@ -149,16 +149,16 @@
"dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain:s} a {provider:s}.", "dyndns_could_not_check_available": "No s'ha pogut verificar la disponibilitat de {domain:s} a {provider:s}.",
"dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS", "dyndns_ip_update_failed": "No s'ha pogut actualitzar l'adreça IP al DynDNS",
"dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS", "dyndns_ip_updated": "S'ha actualitzat l'adreça IP al DynDNS",
"dyndns_key_generating": "S'està generant la clau DNS això pot trigar una estona.", "dyndns_key_generating": "S'està generant la clau DNS... això pot trigar una estona.",
"dyndns_key_not_found": "No s'ha trobat la clau DNS pel domini", "dyndns_key_not_found": "No s'ha trobat la clau DNS pel domini",
"dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS", "dyndns_no_domain_registered": "No hi ha cap domini registrat amb DynDNS",
"dyndns_registered": "S'ha registrat el domini DynDNS", "dyndns_registered": "S'ha registrat el domini DynDNS",
"dyndns_registration_failed": "No s'ha pogut registrar el domini DynDNS: {error:s}", "dyndns_registration_failed": "No s'ha pogut registrar el domini DynDNS: {error:s}",
"dyndns_domain_not_provided": "El proveïdor de DynDNS {provider:s} no pot oferir el domini {domain:s}.", "dyndns_domain_not_provided": "El proveïdor de DynDNS {provider:s} no pot oferir el domini {domain:s}.",
"dyndns_unavailable": "El domini {domain:s} no està disponible.", "dyndns_unavailable": "El domini {domain:s} no està disponible.",
"executing_command": "Execució de l'ordre « {command:s} »", "executing_command": "Execució de l'ordre « {command:s} »...",
"executing_script": "Execució de l'script « {script:s} »", "executing_script": "Execució de l'script « {script:s} »...",
"extracting": "Extracció en curs", "extracting": "Extracció en curs...",
"dyndns_cron_installed": "S'ha creat la tasca cron pel DynDNS", "dyndns_cron_installed": "S'ha creat la tasca cron pel DynDNS",
"dyndns_cron_remove_failed": "No s'ha pogut eliminar la tasca cron per a DynDNS: {error}", "dyndns_cron_remove_failed": "No s'ha pogut eliminar la tasca cron per a DynDNS: {error}",
"dyndns_cron_removed": "S'ha eliminat la tasca cron pel DynDNS", "dyndns_cron_removed": "S'ha eliminat la tasca cron pel DynDNS",
@ -288,10 +288,10 @@
"migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració… (?) Ometent.", "migration_0009_not_needed": "Sembla que ja s'ha fet aquesta migració… (?) Ometent.",
"migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí «%s»", "migrations_cant_reach_migration_file": "No s'ha pogut accedir als fitxers de migració al camí «%s»",
"migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.", "migrations_list_conflict_pending_done": "No es pot utilitzar «--previous» i «--done» al mateix temps.",
"migrations_loading_migration": "Carregant la migració {id}", "migrations_loading_migration": "Carregant la migració {id}...",
"migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}", "migrations_migration_has_failed": "La migració {id} ha fallat, cancel·lant. Error: {exception}",
"migrations_no_migrations_to_run": "No hi ha cap migració a fer", "migrations_no_migrations_to_run": "No hi ha cap migració a fer",
"migrations_skip_migration": "Saltant migració {id}", "migrations_skip_migration": "Saltant migració {id}...",
"migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».", "migrations_to_be_ran_manually": "La migració {id} s'ha de fer manualment. Aneu a Eines → Migracions a la interfície admin, o executeu «yunohost tools migrations migrate».",
"migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».", "migrations_need_to_accept_disclaimer": "Per fer la migració {id}, heu d'acceptar aquesta clàusula de no responsabilitat:\n---\n{disclaimer}\n---\nSi accepteu fer la migració, torneu a executar l'ordre amb l'opció «--accept-disclaimer».",
"no_internet_connection": "El servidor no està connectat a Internet", "no_internet_connection": "El servidor no està connectat a Internet",
@ -325,9 +325,9 @@
"regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»", "regenconf_would_be_updated": "La configuració hagués estat actualitzada per la categoria «{category}»",
"regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…", "regenconf_dry_pending_applying": "Verificació de la configuració pendent que s'hauria d'haver aplicat per la categoria «{category}»…",
"regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}", "regenconf_failed": "No s'ha pogut regenerar la configuració per la/les categoria/es : {categories}",
"regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»", "regenconf_pending_applying": "Aplicació de la configuració pendent per la categoria «{category}»...",
"restore_already_installed_app": "Una aplicació amb la ID «{app:s}» ja està instal·lada", "restore_already_installed_app": "Una aplicació amb la ID «{app:s}» ja està instal·lada",
"restore_app_failed": "No s'ha pogut restaurar l'aplicació «{app:s}»", "restore_app_failed": "No s'ha pogut restaurar {app:s}",
"restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració", "restore_cleaning_failed": "No s'ha pogut netejar el directori temporal de restauració",
"restore_complete": "Restauració completada", "restore_complete": "Restauració completada",
"restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers:s}]", "restore_confirm_yunohost_installed": "Esteu segur de voler restaurar un sistema ja instal·lat? [{answers:s}]",
@ -390,7 +390,7 @@
"ssowat_conf_updated": "S'ha actualitzat la configuració SSOwat", "ssowat_conf_updated": "S'ha actualitzat la configuració SSOwat",
"system_upgraded": "S'ha actualitzat el sistema", "system_upgraded": "S'ha actualitzat el sistema",
"system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema", "system_username_exists": "El nom d'usuari ja existeix en la llista d'usuaris de sistema",
"this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema) Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».", "this_action_broke_dpkg": "Aquesta acció a trencat dpkg/APT (els gestors de paquets del sistema)... Podeu intentar resoldre el problema connectant-vos amb SSH i executant «sudo apt install --fix-broken» i/o «sudo dpkg --configure -a».",
"tools_upgrade_at_least_one": "Especifiqueu «--apps», o «--system»", "tools_upgrade_at_least_one": "Especifiqueu «--apps», o «--system»",
"tools_upgrade_cant_both": "No es poden actualitzar tant el sistema com les aplicacions al mateix temps", "tools_upgrade_cant_both": "No es poden actualitzar tant el sistema com les aplicacions al mateix temps",
"tools_upgrade_cant_hold_critical_packages": "No es poden mantenir els paquets crítics…", "tools_upgrade_cant_hold_critical_packages": "No es poden mantenir els paquets crítics…",
@ -400,15 +400,15 @@
"tools_upgrade_special_packages": "Actualitzant els paquets «especials» (relacionats amb YunoHost)…", "tools_upgrade_special_packages": "Actualitzant els paquets «especials» (relacionats amb YunoHost)…",
"tools_upgrade_special_packages_explanation": "Aquesta actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Després d'això, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines → Registres (a la interfície d'administració) o utilitzant «yunohost log list» (des de la línia d'ordres).", "tools_upgrade_special_packages_explanation": "Aquesta actualització especial continuarà en segon pla. No comenceu cap altra acció al servidor en els pròxims ~10 minuts (depèn de la velocitat del maquinari). Després d'això, pot ser que us hagueu de tornar a connectar a la interfície d'administració. Els registres de l'actualització estaran disponibles a Eines → Registres (a la interfície d'administració) o utilitzant «yunohost log list» (des de la línia d'ordres).",
"tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada.\nPremeu [Enter] per tornar a la línia d'ordres", "tools_upgrade_special_packages_completed": "Actualització dels paquets YunoHost acabada.\nPremeu [Enter] per tornar a la línia d'ordres",
"unbackup_app": "L'aplicació «{app:s}» no serà guardada", "unbackup_app": "{app:s} no es guardarà",
"unexpected_error": "Hi ha hagut un error inesperat: {error}", "unexpected_error": "Hi ha hagut un error inesperat: {error}",
"unlimit": "Sense quota", "unlimit": "Sense quota",
"unrestore_app": "L'aplicació «{app:s} no serà restaurada", "unrestore_app": "{app:s} no es restaurarà",
"update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list, que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", "update_apt_cache_failed": "No s'ha pogut actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list, que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}",
"update_apt_cache_warning": "Hi ha hagut errors al actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}", "update_apt_cache_warning": "Hi ha hagut errors al actualitzar la memòria cau d'APT (el gestor de paquets de Debian). Aquí teniu les línies de sources.list que poden ajudar-vos a identificar les línies problemàtiques:\n{sourceslist}",
"updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema", "updating_apt_cache": "Obtenció de les actualitzacions disponibles per als paquets del sistema...",
"upgrade_complete": "Actualització acabada", "upgrade_complete": "Actualització acabada",
"upgrading_packages": "Actualitzant els paquets", "upgrading_packages": "Actualitzant els paquets...",
"upnp_dev_not_found": "No s'ha trobat cap dispositiu UPnP", "upnp_dev_not_found": "No s'ha trobat cap dispositiu UPnP",
"upnp_disabled": "S'ha desactivat UPnP", "upnp_disabled": "S'ha desactivat UPnP",
"upnp_enabled": "S'ha activat UPnP", "upnp_enabled": "S'ha activat UPnP",
@ -426,9 +426,9 @@
"yunohost_ca_creation_failed": "No s'ha pogut crear l'autoritat de certificació", "yunohost_ca_creation_failed": "No s'ha pogut crear l'autoritat de certificació",
"yunohost_ca_creation_success": "S'ha creat l'autoritat de certificació local.", "yunohost_ca_creation_success": "S'ha creat l'autoritat de certificació local.",
"yunohost_configured": "YunoHost està configurat", "yunohost_configured": "YunoHost està configurat",
"yunohost_installing": "Instal·lació de YunoHost", "yunohost_installing": "Instal·lació de YunoHost...",
"yunohost_not_installed": "YunoHost no està instal·lat correctament. Executeu «yunohost tools postinstall»", "yunohost_not_installed": "YunoHost no està instal·lat correctament. Executeu «yunohost tools postinstall»",
"backup_permission": "Permís de còpia de seguretat per l'aplicació {app:s}", "backup_permission": "Permís de còpia de seguretat per {app:s}",
"group_created": "S'ha creat el grup «{group}»", "group_created": "S'ha creat el grup «{group}»",
"group_creation_failed": "No s'ha pogut crear el grup «{group}»: {error}", "group_creation_failed": "No s'ha pogut crear el grup «{group}»: {error}",
"group_deleted": "S'ha eliminat el grup «{group}»", "group_deleted": "S'ha eliminat el grup «{group}»",
@ -445,11 +445,11 @@
"migration_0011_create_group": "Creant un grup per a cada usuari…", "migration_0011_create_group": "Creant un grup per a cada usuari…",
"migration_0011_done": "Migració completada. Ja podeu gestionar grups d'usuaris.", "migration_0011_done": "Migració completada. Ja podeu gestionar grups d'usuaris.",
"migration_0011_LDAP_update_failed": "Ha fallat l'actualització de LDAP. Error: {error:s}", "migration_0011_LDAP_update_failed": "Ha fallat l'actualització de LDAP. Error: {error:s}",
"migration_0011_migrate_permission": "Fent la migració dels permisos de la configuració de les aplicacions a LDAP", "migration_0011_migrate_permission": "Fent la migració dels permisos de la configuració de les aplicacions a LDAP...",
"migration_0011_migration_failed_trying_to_rollback": "No s'ha pogut fer la migració… s'intenta tornar el sistema a l'estat anterior.", "migration_0011_migration_failed_trying_to_rollback": "No s'ha pogut fer la migració… s'intenta tornar el sistema a l'estat anterior.",
"migration_0011_rollback_success": "S'ha tornat el sistema a l'estat anterior.", "migration_0011_rollback_success": "S'ha tornat el sistema a l'estat anterior.",
"migration_0011_update_LDAP_database": "Actualitzant la base de dades LDAP", "migration_0011_update_LDAP_database": "Actualitzant la base de dades LDAP...",
"migration_0011_update_LDAP_schema": "Actualitzant l'esquema LDAP", "migration_0011_update_LDAP_schema": "Actualitzant l'esquema LDAP...",
"permission_already_exist": "El permís «{permission:s}» ja existeix", "permission_already_exist": "El permís «{permission:s}» ja existeix",
"permission_created": "S'ha creat el permís «{permission:s}»", "permission_created": "S'ha creat el permís «{permission:s}»",
"permission_creation_failed": "No s'ha pogut crear el permís «{permission}»: {error}", "permission_creation_failed": "No s'ha pogut crear el permís «{permission}»: {error}",
@ -474,7 +474,7 @@
"migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»", "migrations_must_provide_explicit_targets": "Heu de proporcionar objectius explícits al utilitzar «--skip» o «--force-rerun»",
"migrations_no_such_migration": "No hi ha cap migració anomenada «{id}»", "migrations_no_such_migration": "No hi ha cap migració anomenada «{id}»",
"migrations_pending_cant_rerun": "Aquestes migracions encara estan pendents, així que no es poden tornar a executar: {ids}", "migrations_pending_cant_rerun": "Aquestes migracions encara estan pendents, així que no es poden tornar a executar: {ids}",
"migrations_running_forward": "Executant la migració {id}", "migrations_running_forward": "Executant la migració {id}...",
"migrations_success_forward": "Migració {id} completada", "migrations_success_forward": "Migració {id} completada",
"apps_already_up_to_date": "Ja estan actualitzades totes les aplicacions", "apps_already_up_to_date": "Ja estan actualitzades totes les aplicacions",
"dyndns_provider_unreachable": "No s'ha pogut connectar amb el proveïdor DynDNS {provider}: o el vostre YunoHost no està ben connectat a Internet o el servidor dynette està caigut.", "dyndns_provider_unreachable": "No s'ha pogut connectar amb el proveïdor DynDNS {provider}: o el vostre YunoHost no està ben connectat a Internet o el servidor dynette està caigut.",
@ -500,7 +500,7 @@
"permission_already_up_to_date": "No s'ha actualitzat el permís perquè la petició d'afegir/eliminar ja corresponent a l'estat actual.", "permission_already_up_to_date": "No s'ha actualitzat el permís perquè la petició d'afegir/eliminar ja corresponent a l'estat actual.",
"permission_currently_allowed_for_all_users": "El permís ha el té el grup de tots els usuaris (all_users) a més d'altres grups. Segurament s'hauria de revocar el permís a «all_users» o eliminar els altres grups als que s'ha atribuït.", "permission_currently_allowed_for_all_users": "El permís ha el té el grup de tots els usuaris (all_users) a més d'altres grups. Segurament s'hauria de revocar el permís a «all_users» o eliminar els altres grups als que s'ha atribuït.",
"permission_require_account": "El permís {permission} només té sentit per als usuaris que tenen un compte, i per tant no es pot activar per als visitants.", "permission_require_account": "El permís {permission} només té sentit per als usuaris que tenen un compte, i per tant no es pot activar per als visitants.",
"app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació", "app_remove_after_failed_install": "Eliminant l'aplicació després que hagi fallat la instal·lació...",
"diagnosis_basesystem_ynh_main_version": "El servidor funciona amb YunoHost {main_version} ({repo})", "diagnosis_basesystem_ynh_main_version": "El servidor funciona amb YunoHost {main_version} ({repo})",
"diagnosis_ram_low": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}. Aneu amb compte.", "diagnosis_ram_low": "El sistema només té {available} ({available_percent}%) de memòria RAM disponibles d'un total de {total}. Aneu amb compte.",
"diagnosis_swap_none": "El sistema no té swap. Hauríeu de considerar afegir un mínim de {recommended} de swap per evitar situacions en les que el sistema es queda sense memòria.", "diagnosis_swap_none": "El sistema no té swap. Hauríeu de considerar afegir un mínim de {recommended} de swap per evitar situacions en les que el sistema es queda sense memòria.",
@ -596,7 +596,7 @@
"diagnosis_description_web": "Web", "diagnosis_description_web": "Web",
"diagnosis_basesystem_hardware_board": "El model de la targeta del servidor és {model}", "diagnosis_basesystem_hardware_board": "El model de la targeta del servidor és {model}",
"diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}", "diagnosis_basesystem_hardware": "L'arquitectura del maquinari del servidor és {virt} {arch}",
"group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà", "group_already_exist_on_system_but_removing_it": "El grup {group} ja existeix en els grups del sistema, però YunoHost l'eliminarà...",
"certmanager_warning_subdomain_dns_record": "El subdomini «{subdomain:s}» no resol a la mateixa adreça IP que «{domain:s}». Algunes funcions no estaran disponibles fins que no s'hagi arreglat i s'hagi regenerat el certificat.", "certmanager_warning_subdomain_dns_record": "El subdomini «{subdomain:s}» no resol a la mateixa adreça IP que «{domain:s}». Algunes funcions no estaran disponibles fins que no s'hagi arreglat i s'hagi regenerat el certificat.",
"domain_cannot_add_xmpp_upload": "No podeu afegir dominis començant per «xmpp-upload.». Aquest tipus de nom està reservat per a la funció de pujada de XMPP integrada a YunoHost.", "domain_cannot_add_xmpp_upload": "No podeu afegir dominis començant per «xmpp-upload.». Aquest tipus de nom està reservat per a la funció de pujada de XMPP integrada a YunoHost.",
"diagnosis_display_tip": "Per veure els problemes que s'han trobat, podeu anar a la secció de Diagnòstic a la pàgina web d'administració, o utilitzar « yunohost diagnostic show --issues » a la línia de comandes.", "diagnosis_display_tip": "Per veure els problemes que s'han trobat, podeu anar a la secció de Diagnòstic a la pàgina web d'administració, o utilitzar « yunohost diagnostic show --issues » a la línia de comandes.",
@ -637,9 +637,9 @@
"diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns proveïdors no permeten configurar el DNS invers (o aquesta funció pot no funcionar…). Si teniu problemes a causa d'això, considereu les solucions següents:<br> - Alguns proveïdors d'accés a internet (ISP) donen l'alternativa de <a href='https://yunohost.org/#/smtp_relay'> utilitzar un relay de servidor de correu electrònic</a> tot i que implica que el relay podrà espiar el trànsit de correus electrònics.<br>- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sobrepassar aquest tipus de limitacions. Mireu <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br>- O es pot <a href='https://yunohost.org/#/isp'>canviar a un proveïdor diferent</a>", "diagnosis_mail_fcrdns_nok_alternatives_4": "Alguns proveïdors no permeten configurar el DNS invers (o aquesta funció pot no funcionar…). Si teniu problemes a causa d'això, considereu les solucions següents:<br> - Alguns proveïdors d'accés a internet (ISP) donen l'alternativa de <a href='https://yunohost.org/#/smtp_relay'> utilitzar un relay de servidor de correu electrònic</a> tot i que implica que el relay podrà espiar el trànsit de correus electrònics.<br>- Una alternativa respectuosa amb la privacitat és utilitzar una VPN *amb una IP pública dedicada* per sobrepassar aquest tipus de limitacions. Mireu <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a><br>- O es pot <a href='https://yunohost.org/#/isp'>canviar a un proveïdor diferent</a>",
"diagnosis_mail_fcrdns_nok_alternatives_6": "Alguns proveïdors no permeten configurar el vostre DNS invers (o la funció no els hi funciona…). Si el vostre DNS invers està correctament configurat per IPv4, podeu intentar deshabilitar l'ús de IPv6 per a enviar correus electrònics utilitzant <cmd>yunohost settings set smtp.allow_ipv6 -v off</cmd>. Nota: aquesta última solució implica que no podreu enviar o rebre correus electrònics cap a els pocs servidors que hi ha que només tenen IPv-6.", "diagnosis_mail_fcrdns_nok_alternatives_6": "Alguns proveïdors no permeten configurar el vostre DNS invers (o la funció no els hi funciona…). Si el vostre DNS invers està correctament configurat per IPv4, podeu intentar deshabilitar l'ús de IPv6 per a enviar correus electrònics utilitzant <cmd>yunohost settings set smtp.allow_ipv6 -v off</cmd>. Nota: aquesta última solució implica que no podreu enviar o rebre correus electrònics cap a els pocs servidors que hi ha que només tenen IPv-6.",
"diagnosis_http_hairpinning_issue_details": "Això és probablement a causa del router del vostre proveïdor d'accés a internet. El que fa, que gent de fora de la xarxa local pugui accedir al servidor sense problemes, però no la gent de dins la xarxa local (com vostè probablement) quan s'utilitza el nom de domini o la IP global. Podreu segurament millorar la situació fent una ullada a <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>", "diagnosis_http_hairpinning_issue_details": "Això és probablement a causa del router del vostre proveïdor d'accés a internet. El que fa, que gent de fora de la xarxa local pugui accedir al servidor sense problemes, però no la gent de dins la xarxa local (com vostè probablement) quan s'utilitza el nom de domini o la IP global. Podreu segurament millorar la situació fent una ullada a <a href='https://yunohost.org/dns_local_network'>https://yunohost.org/dns_local_network</a>",
"backup_archive_cant_retrieve_info_json": "No s'ha pogut carregar la informació de l'arxiu «{archive}» No s'ha pogut obtenir el fitxer info.json (o no és un fitxer json vàlid).", "backup_archive_cant_retrieve_info_json": "No s'ha pogut carregar la informació de l'arxiu «{archive}»... No s'ha pogut obtenir el fitxer info.json (o no és un fitxer json vàlid).",
"backup_archive_corrupted": "Sembla que l'arxiu de la còpia de seguretat «{archive}» està corromput : {error}", "backup_archive_corrupted": "Sembla que l'arxiu de la còpia de seguretat «{archive}» està corromput : {error}",
"certmanager_domain_not_diagnosed_yet": "Encara no hi ha cap resultat de diagnòstic per al domini %s. Torneu a executar el diagnòstic per a les categories «Registres DNS» i «Web» en la secció de diagnòstic per comprovar que el domini està preparat per a Let's Encrypt. (O si sabeu el que esteu fent, utilitzant «--no-checks» per deshabilitar les comprovacions.)", "certmanager_domain_not_diagnosed_yet": "Encara no hi ha cap resultat de diagnòstic per al domini {domain}. Torneu a executar el diagnòstic per a les categories «Registres DNS» i «Web» en la secció de diagnòstic per comprovar que el domini està preparat per a Let's Encrypt. (O si sabeu el que esteu fent, utilitzant «--no-checks» per deshabilitar les comprovacions.)",
"diagnosis_ip_no_ipv6_tip": "Utilitzar una IPv6 no és obligatori per a que funcioni el servidor, però és millor per la salut d'Internet en conjunt. La IPv6 hauria d'estar configurada automàticament pel sistema o pel proveïdor si està disponible. Si no és el cas, pot ser necessari configurar alguns paràmetres més de forma manual tal i com s'explica en la documentació disponible aquí: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>. Si no podeu habilitar IPv6 o us sembla massa tècnic, podeu ignorar aquest avís sense problemes.", "diagnosis_ip_no_ipv6_tip": "Utilitzar una IPv6 no és obligatori per a que funcioni el servidor, però és millor per la salut d'Internet en conjunt. La IPv6 hauria d'estar configurada automàticament pel sistema o pel proveïdor si està disponible. Si no és el cas, pot ser necessari configurar alguns paràmetres més de forma manual tal i com s'explica en la documentació disponible aquí: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>. Si no podeu habilitar IPv6 o us sembla massa tècnic, podeu ignorar aquest avís sense problemes.",
"diagnosis_domain_expiration_not_found": "No s'ha pogut comprovar la data d'expiració d'alguns dominis", "diagnosis_domain_expiration_not_found": "No s'ha pogut comprovar la data d'expiració d'alguns dominis",
"diagnosis_domain_not_found_details": "El domini {domain} no existeix en la base de dades WHOIS o ha expirat!", "diagnosis_domain_not_found_details": "El domini {domain} no existeix en la base de dades WHOIS o ha expirat!",
@ -652,20 +652,29 @@
"restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}", "restore_already_installed_apps": "No s'han pogut restaurar les següents aplicacions perquè ja estan instal·lades: {apps}",
"app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.", "app_packaging_format_not_supported": "No es pot instal·lar aquesta aplicació ja que el format del paquet no és compatible amb la versió de YunoHost del sistema. Hauríeu de considerar actualitzar el sistema.",
"diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant <cmd>yunohost dyndns update --force</cmd>.", "diagnosis_dns_try_dyndns_update_force": "La configuració DNS d'aquest domini hauria de ser gestionada automàticament per YunoHost. Si aquest no és el cas, podeu intentar forçar-ne l'actualització utilitzant <cmd>yunohost dyndns update --force</cmd>.",
"migration_0015_cleaning_up": "Netejant la memòria cau i els paquets que ja no són necessaris", "migration_0015_cleaning_up": "Netejant la memòria cau i els paquets que ja no són necessaris...",
"migration_0015_specific_upgrade": "Començant l'actualització dels paquets del sistema que s'han d'actualitzar de forma independent", "migration_0015_specific_upgrade": "Començant l'actualització dels paquets del sistema que s'han d'actualitzar de forma independent...",
"migration_0015_modified_files": "Tingueu en compte que s'han trobat els següents fitxers que es van modificar manualment i podria ser que es sobreescriguin durant l'actualització: {manually_modified_files}", "migration_0015_modified_files": "Tingueu en compte que s'han trobat els següents fitxers que es van modificar manualment i podria ser que es sobreescriguin durant l'actualització: {manually_modified_files}",
"migration_0015_problematic_apps_warning": "Tingueu en compte que s'han trobat les següents aplicacions que podrien ser problemàtiques. Sembla que aquestes aplicacions no s'han instal·lat des del catàleg d'aplicacions de YunoHost, o no estan marcades com «funcionant». En conseqüència, no es pot garantir que segueixin funcionant després de l'actualització: {problematic_apps}", "migration_0015_problematic_apps_warning": "Tingueu en compte que s'han trobat les següents aplicacions que podrien ser problemàtiques. Sembla que aquestes aplicacions no s'han instal·lat des del catàleg d'aplicacions de YunoHost, o no estan marcades com «funcionant». En conseqüència, no es pot garantir que segueixin funcionant després de l'actualització: {problematic_apps}",
"migration_0015_general_warning": "Tingueu en compte que aquesta migració és una operació delicada. L'equip de YunoHost ha fet tots els possibles per revisar i testejar, però tot i això podria ser que la migració trenqui alguna part del sistema o algunes aplicacions.\n\nPer tant, està recomana:\n - Fer una còpia de seguretat de totes les dades o aplicacions crítiques. Més informació a https://yunohost.org/backup;\n - Ser pacient un cop comenci la migració: en funció de la connexió Internet i del maquinari, podria estar unes hores per actualitzar-ho tot.", "migration_0015_general_warning": "Tingueu en compte que aquesta migració és una operació delicada. L'equip de YunoHost ha fet tots els possibles per revisar i testejar, però tot i això podria ser que la migració trenqui alguna part del sistema o algunes aplicacions.\n\nPer tant, està recomana:\n - Fer una còpia de seguretat de totes les dades o aplicacions crítiques. Més informació a https://yunohost.org/backup;\n - Ser pacient un cop comenci la migració: en funció de la connexió Internet i del maquinari, podria estar unes hores per actualitzar-ho tot.",
"migration_0015_system_not_fully_up_to_date": "El sistema no està completament al dia. Heu de fer una actualització normal abans de fer la migració a Buster.", "migration_0015_system_not_fully_up_to_date": "El sistema no està completament al dia. Heu de fer una actualització normal abans de fer la migració a Buster.",
"migration_0015_not_enough_free_space": "Hi ha poc espai lliure a /var/! HI hauria d'haver un mínim de 1GB lliure per poder fer aquesta migració.", "migration_0015_not_enough_free_space": "Hi ha poc espai lliure a /var/! HI hauria d'haver un mínim de 1GB lliure per poder fer aquesta migració.",
"migration_0015_not_stretch": "La distribució actual de Debian no és Stretch!", "migration_0015_not_stretch": "La distribució actual de Debian no és Stretch!",
"migration_0015_yunohost_upgrade": "Començant l'actualització del nucli de YunoHost", "migration_0015_yunohost_upgrade": "Començant l'actualització del nucli de YunoHost...",
"migration_0015_still_on_stretch_after_main_upgrade": "Alguna cosa ha anat malament durant la actualització principal, sembla que el sistema encara està en Debian Stretch", "migration_0015_still_on_stretch_after_main_upgrade": "Alguna cosa ha anat malament durant la actualització principal, sembla que el sistema encara està en Debian Stretch",
"migration_0015_main_upgrade": "Començant l'actualització principal", "migration_0015_main_upgrade": "Començant l'actualització principal...",
"migration_0015_patching_sources_list": "Apedaçament de source.lists", "migration_0015_patching_sources_list": "Apedaçament de source.lists...",
"migration_0015_start": "Començant la migració a Buster", "migration_0015_start": "Començant la migració a Buster",
"migration_description_0015_migrate_to_buster": "Actualitza els sistema a Debian Buster i YunoHost 4.x", "migration_description_0015_migrate_to_buster": "Actualitza els sistema a Debian Buster i YunoHost 4.x",
"regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.", "regenconf_need_to_explicitly_specify_ssh": "La configuració ssh ha estat modificada manualment, però heu d'especificar explícitament la categoria «ssh» amb --force per fer realment els canvis.",
"migration_0015_weak_certs": "S'han trobat els següents certificats que encara utilitzen algoritmes de signatura febles i s'han d'actualitzar per a ser compatibles amb la propera versió de nginx: {certs}" "migration_0015_weak_certs": "S'han trobat els següents certificats que encara utilitzen algoritmes de signatura febles i s'han d'actualitzar per a ser compatibles amb la propera versió de nginx: {certs}",
"service_description_php7.3-fpm": "Executa aplicacions escrites en PHP amb NGINX",
"migration_0018_failed_to_reset_legacy_rules": "No s'ha pogut restaurar les regles legacy iptables: {error}",
"migration_0018_failed_to_migrate_iptables_rules": "No s'ha pogut migrar les regles legacy iptables a nftables: {error}",
"migration_0017_not_enough_space": "Feu suficient espai disponible en {path} per a realitzar la migració.",
"migration_0017_postgresql_11_not_installed": "PostgreSQL 9.6 està instal·lat, però postgreSQL 11 no? Potser que hagi passat alguna cosa rara en aquest sistema :(...",
"migration_0017_postgresql_96_not_installed": "PostgreSQL no està instal·lat en aquest sistema. No s'ha de realitzar cap operació.",
"migration_description_0018_xtable_to_nftable": "Migrar les regles del trànsit de xarxa al nou sistema nftable",
"migration_description_0017_postgresql_9p6_to_11": "Migrar les bases de dades de PosrgreSQL 9.6 a 11",
"migration_description_0016_php70_to_php73_pools": "Migrar els fitxers de configuració «pool» php7.0-fpm a php7.3"
} }

View file

@ -11,7 +11,7 @@
"app_id_invalid": "Falsche App-ID", "app_id_invalid": "Falsche App-ID",
"app_install_files_invalid": "Diese Dateien können nicht installiert werden", "app_install_files_invalid": "Diese Dateien können nicht installiert werden",
"app_manifest_invalid": "Mit dem App-Manifest stimmt etwas nicht: {error}", "app_manifest_invalid": "Mit dem App-Manifest stimmt etwas nicht: {error}",
"app_not_installed": "Die App {app:s} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}", "app_not_installed": "{app:s} konnte nicht in der Liste installierter Apps gefunden werden: {all_apps}",
"app_removed": "{app:s} wurde entfernt", "app_removed": "{app:s} wurde entfernt",
"app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden, ist die URL korrekt?", "app_sources_fetch_failed": "Quelldateien konnten nicht abgerufen werden, ist die URL korrekt?",
"app_unknown": "Unbekannte App", "app_unknown": "Unbekannte App",
@ -23,8 +23,8 @@
"ask_main_domain": "Hauptdomain", "ask_main_domain": "Hauptdomain",
"ask_new_admin_password": "Neues Verwaltungskennwort", "ask_new_admin_password": "Neues Verwaltungskennwort",
"ask_password": "Passwort", "ask_password": "Passwort",
"backup_app_failed": "Konnte keine Sicherung für die App '{app:s}' erstellen", "backup_app_failed": "Konnte keine Sicherung für {app:s} erstellen",
"backup_archive_app_not_found": "App '{app:s}' konnte in keiner Datensicherung gefunden werden", "backup_archive_app_not_found": "{app:s} konnte in keiner Datensicherung gefunden werden",
"backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits.", "backup_archive_name_exists": "Datensicherung mit dem selben Namen existiert bereits.",
"backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name:s}' gefunden", "backup_archive_name_unknown": "Unbekanntes lokale Datensicherung mit Namen '{name:s}' gefunden",
"backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen", "backup_archive_open_failed": "Kann Sicherungsarchiv nicht öfnen",
@ -38,7 +38,7 @@
"backup_output_directory_forbidden": "Wähle ein anderes Ausgabeverzeichnis. Datensicherungen können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden", "backup_output_directory_forbidden": "Wähle ein anderes Ausgabeverzeichnis. Datensicherungen können nicht in /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var oder in Unterordnern von /home/yunohost.backup/archives erstellt werden",
"backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein", "backup_output_directory_not_empty": "Der gewählte Ausgabeordner sollte leer sein",
"backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden", "backup_output_directory_required": "Für die Datensicherung muss ein Zielverzeichnis angegeben werden",
"backup_running_hooks": "Datensicherunghook wird ausgeführt", "backup_running_hooks": "Datensicherunghook wird ausgeführt...",
"custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_app_url_required": "Es muss eine URL angegeben werden, um deine benutzerdefinierte App {app:s} zu aktualisieren",
"domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden",
"domain_created": "Die Domain wurde angelegt", "domain_created": "Die Domain wurde angelegt",
@ -78,7 +78,7 @@
"iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt", "iptables_unavailable": "iptables kann nicht verwendet werden. Du befindest dich entweder in einem Container oder es wird nicht vom Kernel unterstützt",
"ldap_initialized": "LDAP wurde initialisiert", "ldap_initialized": "LDAP wurde initialisiert",
"mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden", "mail_alias_remove_failed": "E-Mail Alias '{mail:s}' konnte nicht entfernt werden",
"mail_domain_unknown": "Unbekannte Mail Domain '{domain:s}'", "mail_domain_unknown": "Die Domäne '{domain:s}' dieser E-Mail-Adresse ist ungültig. Wähle bitte eine Domäne, welche durch diesen Server verwaltet wird.",
"mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden", "mail_forward_remove_failed": "Mailweiterleitung '{mail:s}' konnte nicht entfernt werden",
"main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden", "main_domain_change_failed": "Die Hauptdomain konnte nicht geändert werden",
"main_domain_changed": "Die Hauptdomain wurde geändert", "main_domain_changed": "Die Hauptdomain wurde geändert",
@ -105,26 +105,26 @@
"restore_nothings_done": "Es wurde nicht wiederhergestellt", "restore_nothings_done": "Es wurde nicht wiederhergestellt",
"restore_running_app_script": "Wiederherstellung wird ausfeührt für App '{app:s}'...", "restore_running_app_script": "Wiederherstellung wird ausfeührt für App '{app:s}'...",
"restore_running_hooks": "Wiederherstellung wird gestartet…", "restore_running_hooks": "Wiederherstellung wird gestartet…",
"service_add_failed": "Der Dienst '{service:s}' kann nicht hinzugefügt werden", "service_add_failed": "Der Dienst '{service:s}' konnte nicht hinzugefügt werden",
"service_added": "Der Service '{service:s}' wurde erfolgreich hinzugefügt", "service_added": "Der Dienst '{service:s}' wurde erfolgreich hinzugefügt",
"service_already_started": "Der Dienst '{service:s}' läuft bereits", "service_already_started": "Der Dienst '{service:s}' läuft bereits",
"service_already_stopped": "Dienst '{service:s}' wurde bereits gestoppt", "service_already_stopped": "Der Dienst '{service:s}' wurde bereits gestoppt",
"service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden", "service_cmd_exec_failed": "Der Befehl '{command:s}' konnte nicht ausgeführt werden",
"service_disable_failed": "Der Dienst '{service:s}' konnte nicht deaktiviert werden", "service_disable_failed": "Der Dienst '{service:s}' konnte nicht deaktiviert werden",
"service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert", "service_disabled": "Der Dienst '{service:s}' wurde erfolgreich deaktiviert",
"service_enable_failed": "Der Dienst '{service:s}' konnte nicht aktiviert werden", "service_enable_failed": "Der Dienst '{service:s}' konnte beim Hochfahren nicht gestartet werden.\n\nKürzlich erstelle Logs des Dienstes: {logs:s}",
"service_enabled": "Der Dienst '{service:s}' wurde erfolgreich aktiviert", "service_enabled": "Der Dienst '{service:s}' wird nun beim Hochfahren des Systems automatisch gestartet.",
"service_remove_failed": "Der Dienst '{service:s}' konnte nicht entfernt werden", "service_remove_failed": "Der Dienst '{service:s}' konnte nicht entfernt werden",
"service_removed": "Der Dienst '{service:s}' wurde erfolgreich entfernt", "service_removed": "Der Dienst '{service:s}' wurde erfolgreich entfernt",
"service_start_failed": "Der Dienst '{service:s}' konnte nicht gestartet werden", "service_start_failed": "Der Dienst '{service:s}' konnte nicht gestartet werden\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
"service_started": "Der Dienst '{service:s}' wurde erfolgreich gestartet", "service_started": "Der Dienst '{service:s}' wurde erfolgreich gestartet",
"service_stop_failed": "Der Dienst '{service:s}' kann nicht gestoppt werden", "service_stop_failed": "Der Dienst '{service:s}' kann nicht gestoppt werden",
"service_stopped": "Der Dienst '{service:s}' wurde erfolgreich beendet", "service_stopped": "Der Dienst '{service:s}' wurde erfolgreich beendet",
"service_unknown": "Unbekannter Dienst '{service:s}'", "service_unknown": "Unbekannter Dienst '{service:s}'",
"ssowat_conf_generated": "Die Konfiguration von SSOwat war erfolgreich", "ssowat_conf_generated": "Die Konfiguration von SSOwat erstellt",
"ssowat_conf_updated": "Die persistente SSOwat Einstellung wurde aktualisiert", "ssowat_conf_updated": "Die Konfiguration von SSOwat aktualisiert",
"system_upgraded": "Das System wurde aktualisiert", "system_upgraded": "System aktualisiert",
"system_username_exists": "Der Benutzername existiert bereits", "system_username_exists": "Der Benutzername existiert bereits in der Liste der System-Benutzer",
"unbackup_app": "App '{app:s}' konnte nicht gespeichert werden", "unbackup_app": "App '{app:s}' konnte nicht gespeichert werden",
"unexpected_error": "Ein unerwarteter Fehler ist aufgetreten", "unexpected_error": "Ein unerwarteter Fehler ist aufgetreten",
"unlimit": "Kein Kontingent", "unlimit": "Kein Kontingent",
@ -154,7 +154,7 @@
"backup_creation_failed": "Konnte Backup-Archiv nicht erstellen", "backup_creation_failed": "Konnte Backup-Archiv nicht erstellen",
"pattern_positive_number": "Muss eine positive Zahl sein", "pattern_positive_number": "Muss eine positive Zahl sein",
"app_not_correctly_installed": "{app:s} scheint nicht korrekt installiert zu sein", "app_not_correctly_installed": "{app:s} scheint nicht korrekt installiert zu sein",
"app_requirements_checking": "Überprüfe notwendige Pakete für {app}", "app_requirements_checking": "Überprüfe notwendige Pakete für {app}...",
"app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein", "app_requirements_unmeet": "Anforderungen für {app} werden nicht erfüllt, das Paket {pkgname} ({version}) muss {spec} sein",
"app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet", "app_unsupported_remote_type": "Für die App wurde ein nicht unterstützer Steuerungstyp verwendet",
"backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})", "backup_archive_broken_link": "Auf das Backup-Archiv konnte nicht zugegriffen werden (ungültiger Link zu {path:s})",
@ -166,18 +166,18 @@
"package_unknown": "Unbekanntes Paket '{pkgname}'", "package_unknown": "Unbekanntes Paket '{pkgname}'",
"certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain:s} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)", "certmanager_attempt_to_replace_valid_cert": "Du versuchst gerade eine richtiges und gültiges Zertifikat der Domain {domain:s} zu überschreiben! (Benutze --force , um diese Nachricht zu umgehen)",
"certmanager_domain_unknown": "Unbekannte Domain '{domain:s}'", "certmanager_domain_unknown": "Unbekannte Domain '{domain:s}'",
"certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain:s} ist kein selbstsigniertes Zertifikat. Bist du dir sicher, dass du es ersetzen willst? (Benutze dafür '--force')", "certmanager_domain_cert_not_selfsigned": "Das Zertifikat der Domain {domain:s} ist kein selbstsigniertes Zertifikat. Sind Sie sich sicher, dass Sie es ersetzen wollen? (Benutzen Sie dafür '--force')",
"certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die Domain {domain:s} ist fehlgeschlagen…", "certmanager_certificate_fetching_or_enabling_failed": "Die Aktivierung des neuen Zertifikats für die {domain:s} ist fehlgeschlagen...",
"certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain:s}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!", "certmanager_attempt_to_renew_nonLE_cert": "Das Zertifikat der Domain '{domain:s}' wurde nicht von Let's Encrypt ausgestellt. Es kann nicht automatisch erneuert werden!",
"certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)", "certmanager_attempt_to_renew_valid_cert": "Das Zertifikat der Domain {domain:s} läuft nicht in Kürze ab! (Benutze --force um diese Nachricht zu umgehen)",
"certmanager_domain_http_not_working": "Es scheint so, dass die Domain {domain:s} nicht über HTTP erreicht werden kann. Bitte überprüfe, ob deine DNS und nginx Konfiguration in Ordnung ist", "certmanager_domain_http_not_working": "Die Domäne {domain:s} scheint über HTTP nicht erreichbar zu sein. Für weitere Informationen überprüfen Sie bitte die Kategorie 'Web' im Diagnose-Bereich. (Wenn Sie wißen was Sie tun, nutzen Sie '--no-checks' um die Überprüfung zu überspringen.)",
"certmanager_error_no_A_record": "Kein DNS 'A' Eintrag für die Domain {domain:s} gefunden. Dein Domainname muss auf diese Maschine weitergeleitet werden, um ein Let's Encrypt Zertifikat installieren zu können! (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )", "certmanager_error_no_A_record": "Kein DNS 'A' Eintrag für die Domain {domain:s} gefunden. Dein Domainname muss auf diese Maschine weitergeleitet werden, um ein Let's Encrypt Zertifikat installieren zu können! (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )",
"certmanager_domain_dns_ip_differs_from_public_ip": "Der DNS 'A' Eintrag der Domain {domain:s} unterscheidet sich von dieser Server-IP. Wenn du gerade deinen A Eintrag verändert hast, warte bitte etwas, damit die Änderungen wirksam werden (du kannst die DNS Propagation mittels Website überprüfen) (Wenn du weißt was du tust, kannst du --no-checks benutzen, um diese Überprüfung zu überspringen. )", "certmanager_domain_dns_ip_differs_from_public_ip": "Die DNS-Einträge der Domäne {domain:s} unterscheiden sich von der IP dieses Servers. Wenn Sie gerade Ihren A-Eintrag verändert haben, warten Sie bitte etwas, damit die Änderungen wirksam werden (Sie können die DNS Propagation mittels Website überprüfen) (Wenn Sie wißen was Sie tun, können Sie --no-checks benutzen, um diese Überprüfung zu überspringen. )",
"certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain:s} zu öffnen (Datei: {file:s}), Grund: {reason:s}", "certmanager_cannot_read_cert": "Es ist ein Fehler aufgetreten, als es versucht wurde das aktuelle Zertifikat für die Domain {domain:s} zu öffnen (Datei: {file:s}), Grund: {reason:s}",
"certmanager_cert_install_success_selfsigned": "Ein selbstsigniertes Zertifikat für die Domain {domain:s} wurde erfolgreich installiert", "certmanager_cert_install_success_selfsigned": "Ein selbstsigniertes Zertifikat für die Domain {domain:s} wurde erfolgreich installiert",
"certmanager_cert_install_success": "Für die Domain {domain:s} wurde erfolgreich ein Let's Encrypt Zertifikat installiert.", "certmanager_cert_install_success": "Für die Domain {domain:s} wurde erfolgreich ein Let's Encrypt Zertifikat installiert.",
"certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert.", "certmanager_cert_renew_success": "Das Let's Encrypt Zertifikat für die Domain {domain:s} wurde erfolgreich erneuert.",
"certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit schon zu viele Zertifikate für die exakt gleiche Domain {domain:s} ausgestellt. Bitte versuche es später nochmal. Besuche https://letsencrypt.org/docs/rate-limits/ für mehr Informationen", "certmanager_hit_rate_limit": "Es wurden innerhalb kurzer Zeit zu viele Zertifikate für dieselbe Domain {domain:s} ausgestellt. Bitte versuchen Sie es später nochmal. Besuchen Sie https://letsencrypt.org/docs/rate-limits/ für mehr Informationen",
"certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden", "certmanager_cert_signing_failed": "Das neue Zertifikat konnte nicht signiert werden",
"certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden", "certmanager_no_cert_file": "Die Zertifikatsdatei für die Domain {domain:s} (Datei: {file:s}) konnte nicht gelesen werden",
"certmanager_conflicting_nginx_file": "Die Domain konnte nicht für die ACME challenge vorbereitet werden: Die nginx Konfigurationsdatei {filepath:s} verursacht Probleme und sollte vorher entfernt werden", "certmanager_conflicting_nginx_file": "Die Domain konnte nicht für die ACME challenge vorbereitet werden: Die nginx Konfigurationsdatei {filepath:s} verursacht Probleme und sollte vorher entfernt werden",
@ -194,15 +194,15 @@
"app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun.", "app_change_url_identical_domains": "Die alte und neue domain/url_path sind identisch: ('{domain:s} {path:s}'). Es gibt nichts zu tun.",
"app_already_up_to_date": "{app:s} ist bereits aktuell", "app_already_up_to_date": "{app:s} ist bereits aktuell",
"backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt", "backup_abstract_method": "Diese Backup-Methode wird noch nicht unterstützt",
"backup_applying_method_tar": "Erstellen des Backup-tar Archives", "backup_applying_method_tar": "Erstellen des Backup-tar Archives...",
"backup_applying_method_copy": "Kopiere alle Dateien ins Backup", "backup_applying_method_copy": "Kopiere alle Dateien ins Backup...",
"app_change_url_no_script": "Die Anwendung '{app_name:s}' unterstützt bisher keine URL-Modifikation. Vielleicht sollte sie aktualisiert werden.", "app_change_url_no_script": "Die Anwendung '{app_name:s}' unterstützt bisher keine URL-Modifikation. Vielleicht sollte sie aktualisiert werden.",
"app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Anwendung genutzt:\n{apps:s}", "app_location_unavailable": "Diese URL ist nicht verfügbar oder wird von einer installierten Anwendung genutzt:\n{apps:s}",
"backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method:s}' auf", "backup_applying_method_custom": "Rufe die benutzerdefinierte Backup-Methode '{method:s}' auf...",
"backup_archive_system_part_not_available": "Der System-Teil '{part:s}' ist in diesem Backup nicht enthalten", "backup_archive_system_part_not_available": "Der System-Teil '{part:s}' ist in diesem Backup nicht enthalten",
"backup_archive_writing_error": "Die Dateien '{source:s} (im Ordner '{dest:s}') konnten nicht in das komprimierte Archiv-Backup '{archive:s}' hinzugefügt werden", "backup_archive_writing_error": "Die Dateien '{source:s} (im Ordner '{dest:s}') konnten nicht in das komprimierte Archiv-Backup '{archive:s}' hinzugefügt werden",
"app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}", "app_change_url_success": "{app:s} URL ist nun {domain:s}{path:s}",
"backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository", "backup_applying_method_borg": "Sende alle Dateien zur Sicherung ins borg-backup repository...",
"global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}", "global_settings_bad_type_for_setting": "Falscher Typ für Einstellung {setting:s}. Empfangen: {received_type:s}, aber erwartet: {expected_type:s}",
"global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting:s}. Habe '{choice:s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices:s}", "global_settings_bad_choice_for_enum": "Falsche Wahl für die Einstellung {setting:s}. Habe '{choice:s}' erhalten, aber es stehen nur folgende Auswahlmöglichkeiten zur Verfügung: {available_choices:s}",
"file_does_not_exist": "Die Datei {path:s} existiert nicht.", "file_does_not_exist": "Die Datei {path:s} existiert nicht.",
@ -212,16 +212,16 @@
"dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.", "dyndns_could_not_check_provide": "Konnte nicht überprüft, ob {provider:s} die Domain(s) {domain:s} bereitstellen kann.",
"domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt Ihnen, was die * empfohlene * Konfiguration ist. Die DNS-Konfiguration wird NICHT für Sie eingerichtet. Es liegt in Ihrer Verantwortung, Ihre DNS-Zone in Ihrem Registrar gemäß dieser Empfehlung zu konfigurieren.", "domain_dns_conf_is_just_a_recommendation": "Dieser Befehl zeigt Ihnen, was die * empfohlene * Konfiguration ist. Die DNS-Konfiguration wird NICHT für Sie eingerichtet. Es liegt in Ihrer Verantwortung, Ihre DNS-Zone in Ihrem Registrar gemäß dieser Empfehlung zu konfigurieren.",
"dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet", "dpkg_lock_not_available": "Dieser Befehl kann momentan nicht ausgeführt werden, da anscheinend ein anderes Programm die Sperre von dpkg (dem Systempaket-Manager) verwendet",
"confirm_app_install_thirdparty": "WARNUNG! Das Installieren von Anwendungen von Drittanbietern kann die Integrität und Sicherheit Deines Systems beeinträchtigen. Du solltest es wahrscheinlich NICHT installieren, es sei denn, Du weisst, was Du tust. Bist du bereit, dieses Risiko einzugehen? [{answers:s}]", "confirm_app_install_thirdparty": "WARNUNG! Das Installieren von Anwendungen von Drittanbietern kann die Integrität und Sicherheit Ihres Systems beeinträchtigen. Sie sollten Sie wahrscheinlich NICHT installieren, es sei denn, Sie wiẞen, was Sie tun. Sind Sie bereit, dieses Risiko einzugehen? [{answers:s}]",
"confirm_app_install_danger": "WARNUNG! Diese Anwendung ist noch experimentell (wenn nicht ausdrücklich \"not working\"/\"nicht funktionsfähig\")! Du solltest es wahrscheinlich NICHT installieren, es sei denn, du weißt, was du tust. Es wird keine Unterstützung geleistet, falls diese Anwendung nicht funktioniert oder dein System zerstört... Falls du bereit bist, dieses Risiko einzugehen, tippe '{answers:s}'", "confirm_app_install_danger": "WARNUNG! Diese Anwendung ist noch experimentell (wenn nicht ausdrücklich \"not working\"/\"nicht funktionsfähig\")! Sie sollten sie wahrscheinlich NICHT installieren, es sei denn, Sie wißen, was Sie tun. Es wird keine Unterstützung geleistet, falls diese Anwendung nicht funktioniert oder Ihr System zerstört... Falls Sie bereit bist, dieses Risiko einzugehen, tippe '{answers:s}'",
"confirm_app_install_warning": "Warnung: Diese Anwendung funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers:s}] ", "confirm_app_install_warning": "Warnung: Diese Anwendung funktioniert möglicherweise, ist jedoch nicht gut in YunoHost integriert. Einige Funktionen wie Single Sign-On und Backup / Restore sind möglicherweise nicht verfügbar. Trotzdem installieren? [{answers:s}] ",
"backup_with_no_restore_script_for_app": "Die App {app:s} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.", "backup_with_no_restore_script_for_app": "{app:s} hat kein Wiederherstellungsskript. Das Backup dieser App kann nicht automatisch wiederhergestellt werden.",
"backup_with_no_backup_script_for_app": "Die App {app:s} hat kein Sicherungsskript. Ignoriere es.", "backup_with_no_backup_script_for_app": "Die App {app:s} hat kein Sicherungsskript. Ignoriere es.",
"backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden", "backup_unable_to_organize_files": "Dateien im Archiv konnten nicht mit der schnellen Methode organisiert werden",
"backup_system_part_failed": "Der Systemteil '{part:s}' konnte nicht gesichert werden", "backup_system_part_failed": "Der Systemteil '{part:s}' konnte nicht gesichert werden",
"backup_permission": "Sicherungsberechtigung für App {app:s}", "backup_permission": "Sicherungsberechtigung für {app:s}",
"backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path:s}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.", "backup_output_symlink_dir_broken": "Ihr Archivverzeichnis '{path:s}' ist ein fehlerhafter Symlink. Vielleicht haben Sie vergessen, das Speichermedium, auf das er verweist, neu zu mounten oder einzustecken.",
"backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten", "backup_mount_archive_for_restore": "Archiv für Wiederherstellung vorbereiten...",
"backup_method_tar_finished": "Tar-Backup-Archiv erstellt", "backup_method_tar_finished": "Tar-Backup-Archiv erstellt",
"backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method:s}' beendet", "backup_method_custom_finished": "Benutzerdefinierte Sicherungsmethode '{method:s}' beendet",
"backup_method_copy_finished": "Sicherungskopie beendet", "backup_method_copy_finished": "Sicherungskopie beendet",
@ -231,17 +231,17 @@
"backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden", "backup_csv_creation_failed": "Die zur Wiederherstellung erforderliche CSV-Datei kann nicht erstellt werden",
"backup_couldnt_bind": "{src:s} konnte nicht an {dest:s} angebunden werden.", "backup_couldnt_bind": "{src:s} konnte nicht an {dest:s} angebunden werden.",
"backup_borg_not_implemented": "Die Borg-Sicherungsmethode ist noch nicht implementiert", "backup_borg_not_implemented": "Die Borg-Sicherungsmethode ist noch nicht implementiert",
"backup_ask_for_copying_if_needed": "Möchten Sie die Sicherung mit {size:s} MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)", "backup_ask_for_copying_if_needed": "Möchten Sie die Sicherung mit {size:s}MB temporär durchführen? (Dieser Weg wird verwendet, da einige Dateien nicht mit einer effizienteren Methode vorbereitet werden konnten.)",
"backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien", "backup_actually_backuping": "Erstellt ein Backup-Archiv aus den gesammelten Dateien...",
"ask_new_path": "Neuer Pfad", "ask_new_path": "Neuer Pfad",
"ask_new_domain": "Neue Domain", "ask_new_domain": "Neue Domain",
"app_upgrade_some_app_failed": "Einige Anwendungen können nicht aktualisiert werden", "app_upgrade_some_app_failed": "Einige Anwendungen können nicht aktualisiert werden",
"app_upgrade_app_name": "{app} wird jetzt aktualisiert", "app_upgrade_app_name": "{app} wird jetzt aktualisiert...",
"app_upgrade_several_apps": "Die folgenden Apps werden aktualisiert: {apps}", "app_upgrade_several_apps": "Die folgenden Apps werden aktualisiert: {apps}",
"app_start_restore": "Anwendung {app} wird wiederhergestellt…", "app_start_restore": "{app} wird wiederhergestellt...",
"app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen", "app_start_backup": "Sammeln von Dateien, die für {app} gesichert werden sollen...",
"app_start_remove": "Anwendung {app} wird entfernt…", "app_start_remove": "{app} wird entfernt...",
"app_start_install": "Anwendung {app} wird installiert…", "app_start_install": "{app} wird installiert...",
"app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}", "app_not_upgraded": "Die App '{failed_app}' konnte nicht aktualisiert werden. Infolgedessen wurden die folgenden App-Upgrades abgebrochen: {apps}",
"app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von der App \"{other_app}\" verwendet", "app_make_default_location_already_used": "Die App \"{app}\" kann nicht als Standard für die Domain \"{domain}\" festgelegt werden. Sie wird bereits von der App \"{other_app}\" verwendet",
"aborting": "Breche ab.", "aborting": "Breche ab.",
@ -300,7 +300,7 @@
"app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.", "app_full_domain_unavailable": "Es tut uns leid, aber diese Anwendung erfordert die Installation auf einer eigenen Domain, aber einige andere Anwendungen sind bereits auf der Domäne'{domain}' installiert. Eine mögliche Lösung ist das Hinzufügen und Verwenden einer Subdomain, die dieser Anwendung zugeordnet ist.",
"app_install_failed": "Installation von {app} fehlgeschlagen: {error}", "app_install_failed": "Installation von {app} fehlgeschlagen: {error}",
"app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten", "app_install_script_failed": "Im Installationsscript ist ein Fehler aufgetreten",
"app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation", "app_remove_after_failed_install": "Entfernen der App nach fehlgeschlagener Installation...",
"app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten", "app_upgrade_script_failed": "Es ist ein Fehler im App-Upgrade-Skript aufgetreten",
"diagnosis_basesystem_host": "Server läuft unter Debian {debian_version}", "diagnosis_basesystem_host": "Server läuft unter Debian {debian_version}",
"diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}", "diagnosis_basesystem_kernel": "Server läuft unter Linux-Kernel {kernel_version}",
@ -329,17 +329,78 @@
"diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!", "diagnosis_found_errors_and_warnings": "Habe {errors} erhebliche(s) Problem(e) (und {warnings} Warnung(en)) in Verbindung mit {category} gefunden!",
"diagnosis_ip_broken_dnsresolution": "Domänen-Namens-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert eine Firewall die DNS Anfragen?", "diagnosis_ip_broken_dnsresolution": "Domänen-Namens-Auflösung scheint aus einem bestimmten Grund nicht zu funktionieren... Blockiert eine Firewall die DNS Anfragen?",
"diagnosis_ip_broken_resolvconf": "Domänen-Namens-Auflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in <code>/etc/resolv.conf</> kein Eintrag auf <code>127.0.0.1</code> zeigt.", "diagnosis_ip_broken_resolvconf": "Domänen-Namens-Auflösung scheint nicht zu funktionieren, was daran liegen könnte, dass in <code>/etc/resolv.conf</> kein Eintrag auf <code>127.0.0.1</code> zeigt.",
"diagnosis_ip_weird_resolvconf_details": "Stattdessen sollte diese Datei ein Softlink auf /etc/resolvconf/run/resolv.conf sein, die auf sich selbst zu 127.0.0.1 zeigt (dnsmasq). Der eigentlich Auflösende sollte in /etc/resolv.dnsmasq.conf konfiguriert werden.", "diagnosis_ip_weird_resolvconf_details": "Die Datei <code>/etc/resolv.conf</code> muss ein Symlink auf <code>/etc/resolvconf/run/resolv.conf</code> sein, welcher auf <code>127.0.0.1</code> (dnsmasq) zeigt. Falls Sie die DNS-Resolver manuell konfigurieren möchten, bearbeiten Sie bitte <code>/etc/resolv.dnsmasq.conf</code>.",
"diagnosis_dns_good_conf": "Gute DNS Konfiguration für Domäne {domain} (Kategorie {category})", "diagnosis_dns_good_conf": "Die DNS-Einträge für die Domäne {domain} (Kategorie {category}) sind korrekt konfiguriert",
"diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))", "diagnosis_ignored_issues": "(+ {nb_ignored} ignorierte(s) Problem(e))",
"diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}", "diagnosis_basesystem_hardware": "Server Hardware Architektur ist {virt} {arch}",
"diagnosis_basesystem_hardware_board": "Server Platinen Modell ist {model}", "diagnosis_basesystem_hardware_board": "Server Platinen Modell ist {model}",
"diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!", "diagnosis_found_errors": "Habe {errors} erhebliche(s) Problem(e) in Verbindung mit {category} gefunden!",
"diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.", "diagnosis_found_warnings": "Habe {warnings} Ding(e) gefunden, die verbessert werden könnten für {category}.",
"diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!", "diagnosis_ip_dnsresolution_working": "Domänen-Namens-Auflösung funktioniert!",
"diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber sei vorsichtig wenn du eine eigene <code>/etc/resolv.conf</code> verwendest.", "diagnosis_ip_weird_resolvconf": "DNS Auflösung scheint zu funktionieren, aber seien Sie vorsichtig wenn Sie eine eigene <code>/etc/resolv.conf</code> verwendest.",
"diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, kannst Du zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues' in der Kommandozeile ausführen.", "diagnosis_display_tip": "Um die gefundenen Probleme zu sehen, können Sie zum Diagnose-Bereich des webadmin gehen, oder 'yunohost diagnosis show --issues' in der Kommandozeile ausführen.",
"backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}", "backup_archive_corrupted": "Das Backup-Archiv '{archive}' scheint beschädigt: {error}",
"backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).", "backup_archive_cant_retrieve_info_json": "Die Informationen für das Archiv '{archive}' konnten nicht geladen werden... Die Datei info.json wurde nicht gefunden (oder ist kein gültiges json).",
"app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der Yunohost-Version unterstützt wird. Denken Sie darüber nach das System zu aktualisieren." "app_packaging_format_not_supported": "Diese App kann nicht installiert werden da das Paketformat nicht von der YunoHost-Version unterstützt wird. Denken Sie darüber nach das System zu aktualisieren.",
"certmanager_domain_not_diagnosed_yet": "Für {domain} gibt es noch keine Diagnose-Resultate. Bitte wiederholen Sie die Diagnose für die Kategorien 'DNS records' und 'Web' im Diagnose-Bereich um zu überprüfen ob die Domain für Let's Encrypt bereit ist. (Oder wenn Sie wissen was Sie tun, verwenden Sie '--no-checks' um diese Überprüfungen abzuschalten.",
"migration_0015_patching_sources_list": "sources.lists wird repariert...",
"migration_0015_start": "Start der Migration auf Buster",
"migration_0011_failed_to_remove_stale_object": "Abgelaufenes Objekt konne nicht entfernt werden. {dn}: {error}",
"migration_0011_update_LDAP_schema": "Das LDAP-Schema aktualisieren...",
"migration_0011_update_LDAP_database": "Die LDAP-Datenbank aktualisieren...",
"migration_0011_migrate_permission": "Berechtigungen der Applikationen von den Einstellungen zu LDAP migrieren...",
"migration_0011_LDAP_update_failed": "LDAP konnte nicht aktualisiert werden. Fehler:n{error:s}",
"migration_0011_create_group": "Eine Gruppe für jeden Benutzer erstellen…",
"migration_description_0015_migrate_to_buster": "Auf Debian Buster und YunoHost 4.x upgraden",
"mail_unavailable": "Diese E-Mail Adresse ist reserviert und wird dem ersten Benutzer automatisch zugewiesen",
"diagnosis_services_conf_broken": "Die Konfiguration für den Dienst {service} ist fehlerhaft!",
"diagnosis_services_running": "Dienst {service} läuft!",
"diagnosis_domain_expires_in": "{domain} läuft in {days} Tagen ab.",
"diagnosis_domain_expiration_error": "Einige Domänen werden SEHR BALD ablaufen!",
"diagnosis_domain_expiration_success": "Deine Domänen sind registriert und werden in nächster Zeit nicht ablaufen.",
"diagnosis_domain_not_found_details": "Die Domäne {domain} existiert nicht in der WHOIS-Datenbank oder sie ist abgelaufen!",
"diagnosis_domain_expiration_not_found": "Konnte die Ablaufdaten für einige Domänen nicht überprüfen.",
"diagnosis_dns_try_dyndns_update_force": "Die DNS-Konfiguration dieser Domäne sollte automatisch von Yunohost verwaltet werden. Andernfalls können Sie mittels <cmd>yunohost dyndns update --force</cmd> ein Update erzwingen.",
"diagnosis_dns_point_to_doc": "Bitte schauen Sie in die Dokumentation unter <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> wenn Sie Hilfe bei der Konfiguration der DNS-Einträge brauchen.",
"diagnosis_dns_discrepancy": "Der folgende DNS-Eintrag scheint nicht den empfohlenen Einstellungen zu entsprechen: <br>Typ: <code>{type}</code><br>Name: <code>{name}</code><br> Aktueller Wert: <code>{current}</code><br> Erwarteter Wert: <code>{value}</code>",
"diagnosis_dns_missing_record": "Gemäß der empfohlenen DNS-Konfiguration sollten Sie einen DNS-Eintrag mit den folgenden Informationen hinzufügen.<br>Typ: <code>{type}</code><br>Name: <code>{name}</code><br>Wert: <code>{value}</code>",
"diagnosis_dns_bad_conf": "Einige DNS-Einträge für die Domäne {domain} fehlen oder sind nicht korrekt (Kategorie {category})",
"diagnosis_ip_local": "Lokale IP: <code>{local}</code>",
"diagnosis_ip_global": "Globale IP: <code>{global}</code>",
"diagnosis_ip_no_ipv6_tip": "Die Verwendung von IPv6 ist nicht Voraussetzung für das Funktionieren Ihres Servers, trägt aber zur Gesundheit des Internet als Ganzes bei. IPv6 sollte normalerweise automatisch von Ihrem Server oder Ihrem Provider konfiguriert werden, sofern verfügbar. Andernfalls müßen Sie einige Dinge manuell konfigurieren. Weitere Informationen finden Sie hier: <a href='https://yunohost.org/#/ipv6'>https://yunohost.org/#/ipv6</a>. Wenn Sie IPv6 nicht aktivieren können oder Ihnen das zu technisch ist, können Sie diese Warnung gefahrlos ignorieren.",
"diagnosis_services_bad_status_tip": "Sie können versuchen, <a href='#/services/{service}'>den Dienst neu zu starten</a>, und wenn das nicht funktioniert, schauen Sie sich <a href='#/services/{service}'>die (Dienst-)Logs in der Verwaltung</a> an (In der Kommandozeile können Sie dies mit <cmd>yunohost service restart {service}</cmd> und <cmd>yunohost service log {service}</cmd> tun).",
"diagnosis_services_bad_status": "Der Dienst {service} ist {status} :(",
"diagnosis_diskusage_verylow": "Der Speicher <code>{mountpoint}</code> (auf Gerät <code>{device}</code>) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von ingesamt {total}). Sie sollten sich ernsthaft überlegen, einigen Seicherplatz frei zu machen!",
"diagnosis_http_ok": "Die Domäne {domain} ist über HTTP von außerhalb des lokalen Netzwerks erreichbar.",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Einige Hosting-Anbieter werden es Ihnen nicht gestatten, den ausgehenden Port 25 zu öffnen, da diese sich nicht um die Netzneutralität kümmern.<br>- Einige davon bieten als Alternative an, <a href='https://yunohost.org/#/smtp_relay'>ein Mailserver-Relay zu verwenden</a>, was jedoch bedeutet, dass das Relay Ihren E-Mail-Verkehr ausspionieren kann.<br>- Eine die Privatsphäre berücksichtigende Alternative ist die Verwendung eines VPN *mit einer dedizierten öffentlichen IP* um solche Einschränkungen zu umgehen. Schauen Sie unter <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a> nach.<br>- Sie können auch in Betracht ziehen, zu einem <a href='https://yunohost.org/#/isp'>netzneutralitätfreundlicheren Anbieter</a> zu wechseln.",
"diagnosis_http_timeout": "Wartezeit wurde beim Versuch, von außen eine Verbindung zum Server aufzubauen, überschritten. Er scheint nicht erreichbar zu sein.<br>1. Die häufigste Ursache für dieses Problem ist daß der Port 80 (und 433) <a href='https://yunohost.org/isp_box_config'>nicht richtig zu Ihrem Server weitergeleitet werden</a>.<br>2. Sie sollten auch sicherstellen, daß der Dienst nginx läuft.<br>3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.",
"service_reloaded_or_restarted": "Der Dienst '{service:s}' wurde erfolgreich neu geladen oder gestartet",
"service_restarted": "Der Dienst '{service:s}' wurde neu gestartet",
"service_regen_conf_is_deprecated": "'yunohost service regen-conf' ist veraltet! Bitte verwenden Sie stattdessen 'yunohost tools regen-conf'.",
"certmanager_warning_subdomain_dns_record": "Die Subdomain '{subdomain:s}' löst nicht dieselbe IP wie '{domain:s} auf. Einige Funktionen werden nicht verfügbar sein, solange Sie dies nicht beheben und das Zertifikat erneuern.",
"diagnosis_ports_ok": "Port {port} ist von außen erreichbar.",
"diagnosis_ram_verylow": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total})",
"diagnosis_mail_outgoing_port_25_blocked_details": "Sie sollten zuerst versuchen den ausgehenden Port 25 auf Ihrer Router-Konfigurationsoberfläche oder Ihrer Hosting-Anbieter-Konfigurationsoberfläche zu öffnen. (Bei einigen Hosting-Anbieter kann es sein, daß Sie verlangen, daß man dafür ein Support-Ticket sendet).",
"diagnosis_mail_ehlo_ok": "Der SMTP-Server ist von von außen erreichbar und darum auch in der Lage E-Mails zu empfangen!",
"diagnosis_mail_ehlo_bad_answer": "Ein nicht-SMTP-Dienst antwortete auf Port 25 per IPv{ipversion}",
"diagnosis_swap_notsomuch": "Das System hat nur {total} Swap. Sie sollten sich überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.",
"diagnosis_swap_ok": "Das System hat {total} Swap!",
"diagnosis_swap_tip": "Wir sind Ihnen sehr dankbar dafür, daß Sie behutsam und sich bewußt sind, dass das Betreiben einer Swap-Partition auf einer SD-Karte oder einem SSD-Speicher das Risiko einer drastischen Verkürzung der Lebenserwartung dieser Platte nach sich zieht.",
"diagnosis_mail_outgoing_port_25_ok": "Der SMTP-Server ist in der Lage E-Mails zu versenden (der ausgehende Port 25 ist nicht blockiert).",
"diagnosis_mail_outgoing_port_25_blocked": "Der SMTP-Server kann keine E-Mails an andere Server senden, weil der ausgehende Port 25 per IPv{ipversion} blockiert ist. Sie können versuchen diesen in der Konfigurations-Oberfläche Ihres Internet-Anbieters (oder Hosters) zu öffnen.",
"diagnosis_mail_ehlo_unreachable": "Der SMTP-Server ist von außen nicht erreichbar per IPv{ipversion}. Er wird nicht in der Lage sein E-Mails zu empfangen.",
"diagnosis_diskusage_low": "Der Speicher <code>{mountpoint}</code> (auf Gerät <code>{device}</code>) hat nur noch {free} ({free_percent}%) freien Speicherplatz (von insgesamt {total}). Seien Sie vorsichtig.",
"diagnosis_ram_low": "Das System hat nur {available} ({available_percent}%) RAM zur Verfügung! (von insgesamt {total}). Seien Sie vorsichtig.",
"service_reload_or_restart_failed": "Der Dienst '{service:s}' konnte nicht erneut geladen oder gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
"diagnosis_domain_expiration_not_found_details": "Die WHOIS-Informationen für die Domäne {domain} scheint keine Informationen über das Ablaufdatum zu enthalten.",
"diagnosis_domain_expiration_warning": "Einige Domänen werden bald ablaufen!",
"diagnosis_diskusage_ok": "Der Speicher <code>{mountpoint}</code> (auf Gerät <code>{device}</code>) hat immer noch {free} ({free_percent}%) freien Speicherplatz übrig(von insgesamt {total})!",
"diagnosis_ram_ok": "Das System hat immer noch {available} ({available_percent}%) RAM zu Verfügung von {total}.",
"diagnosis_swap_none": "Das System hat gar keinen Swap. Sie sollten sich überlegen mindestens {recommended} an Swap einzurichten, um Situationen zu verhindern, in welchen der RAM des Systems knapp wird.",
"diagnosis_mail_ehlo_unreachable_details": "Konnte keine Verbindung zu Ihrem Server auf dem Port 25 herzustellen per IPv{ipversion}. Er scheint nicht erreichbar zu sein.<br>1. Das häufigste Problem ist, dass der Port 25 <a href='https://yunohost.org/isp_box_config'>nicht richtig zu Ihrem Server weitergeleitet ist</a>.<br>2. Sie sollten auch sicherstellen, dass der Postfix-Dienst läuft.<br>3. In komplexeren Umgebungen: Stellen Sie sicher, daß keine Firewall oder Reverse-Proxy stört.",
"diagnosis_mail_ehlo_wrong": "Ein anderer SMTP-Server antwortet auf IPv{ipversion}. Ihr Server wird wahrscheinlich nicht in der Lage sein, E-Mails zu empfangen.",
"migration_description_0018_xtable_to_nftable": "Alte Netzwerkverkehrsregeln zum neuen nftable-System migrieren",
"service_reload_failed": "Der Dienst '{service:s}' konnte nicht erneut geladen werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}",
"service_reloaded": "Der Dienst '{service:s}' wurde erneut geladen",
"service_restart_failed": "Der Dienst '{service:s}' konnte nicht erneut gestartet werden.\n\nKürzlich erstellte Logs des Dienstes: {logs:s}"
} }

View file

@ -30,6 +30,11 @@
"app_label_depreciated": "This command is depreciated !! Please use the new command 'yunohost user permission update' to manage the app label.", "app_label_depreciated": "This command is depreciated !! Please use the new command 'yunohost user permission update' to manage the app label.",
"app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps:s}", "app_location_unavailable": "This URL is either unavailable, or conflicts with the already installed app(s):\n{apps:s}",
"app_manifest_invalid": "Something is wrong with the app manifest: {error}", "app_manifest_invalid": "Something is wrong with the app manifest: {error}",
"app_manifest_install_ask_domain": "Choose the domain where this app should be installed",
"app_manifest_install_ask_path": "Choose the path where this app should be installed",
"app_manifest_install_ask_password": "Choose an administration password for this app",
"app_manifest_install_ask_admin": "Choose an administrator user for this app",
"app_manifest_install_ask_is_public": "Should this app be exposed to anonymous visitors?",
"app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}", "app_not_upgraded": "The app '{failed_app}' failed to upgrade, and as a consequence the following apps' upgrades have been cancelled: {apps}",
"app_not_correctly_installed": "{app:s} seems to be incorrectly installed", "app_not_correctly_installed": "{app:s} seems to be incorrectly installed",
"app_not_installed": "Could not find {app:s} in the list of installed apps: {all_apps}", "app_not_installed": "Could not find {app:s} in the list of installed apps: {all_apps}",
@ -58,7 +63,7 @@
"apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}", "apps_catalog_failed_to_download": "Unable to download the {apps_catalog} app catalog: {error}",
"apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.", "apps_catalog_obsolete_cache": "The app catalog cache is empty or obsolete.",
"apps_catalog_update_success": "The application catalog has been updated!", "apps_catalog_update_success": "The application catalog has been updated!",
"ask_email": "E-mail address", "ask_user_domain": "Domain to use for the user's email address and XMPP account",
"ask_firstname": "First name", "ask_firstname": "First name",
"ask_lastname": "Last name", "ask_lastname": "Last name",
"ask_main_domain": "Main domain", "ask_main_domain": "Main domain",
@ -69,7 +74,6 @@
"backup_abstract_method": "This backup method has yet to be implemented", "backup_abstract_method": "This backup method has yet to be implemented",
"backup_actually_backuping": "Creating a backup archive from the collected files...", "backup_actually_backuping": "Creating a backup archive from the collected files...",
"backup_app_failed": "Could not back up {app:s}", "backup_app_failed": "Could not back up {app:s}",
"backup_applying_method_borg": "Sending all files to backup into borg-backup repository...",
"backup_applying_method_copy": "Copying all files to backup...", "backup_applying_method_copy": "Copying all files to backup...",
"backup_applying_method_custom": "Calling the custom backup method '{method:s}'...", "backup_applying_method_custom": "Calling the custom backup method '{method:s}'...",
"backup_applying_method_tar": "Creating the backup TAR archive...", "backup_applying_method_tar": "Creating the backup TAR archive...",
@ -83,7 +87,6 @@
"backup_archive_system_part_not_available": "System part '{part:s}' unavailable in this backup", "backup_archive_system_part_not_available": "System part '{part:s}' unavailable in this backup",
"backup_archive_writing_error": "Could not add the files '{source:s}' (named in the archive '{dest:s}') to be backed up into the compressed archive '{archive:s}'", "backup_archive_writing_error": "Could not add the files '{source:s}' (named in the archive '{dest:s}') to be backed up into the compressed archive '{archive:s}'",
"backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size:s}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)", "backup_ask_for_copying_if_needed": "Do you want to perform the backup using {size:s}MB temporarily? (This way is used since some files could not be prepared using a more efficient method.)",
"backup_borg_not_implemented": "The Borg backup method is not yet implemented",
"backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected", "backup_cant_mount_uncompress_archive": "Could not mount the uncompressed archive as write protected",
"backup_cleaning_failed": "Could not clean up the temporary backup folder", "backup_cleaning_failed": "Could not clean up the temporary backup folder",
"backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive", "backup_copying_to_organize_the_archive": "Copying {size:s}MB to organize the archive",
@ -97,7 +100,6 @@
"backup_delete_error": "Could not delete '{path:s}'", "backup_delete_error": "Could not delete '{path:s}'",
"backup_deleted": "Backup deleted", "backup_deleted": "Backup deleted",
"backup_hook_unknown": "The backup hook '{hook:s}' is unknown", "backup_hook_unknown": "The backup hook '{hook:s}' is unknown",
"backup_method_borg_finished": "Backup into Borg finished",
"backup_method_copy_finished": "Backup copy finalized", "backup_method_copy_finished": "Backup copy finalized",
"backup_method_custom_finished": "Custom backup method '{method:s}' finished", "backup_method_custom_finished": "Custom backup method '{method:s}' finished",
"backup_method_tar_finished": "TAR backup archive created", "backup_method_tar_finished": "TAR backup archive created",
@ -260,6 +262,7 @@
"diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> and if you're ok, apply the changes with <cmd>yunohost tools regen-conf nginx --force</cmd>.", "diagnosis_http_nginx_conf_not_up_to_date_details": "To fix the situation, inspect the difference with the command line using <cmd>yunohost tools regen-conf nginx --dry-run --with-diff</cmd> and if you're ok, apply the changes with <cmd>yunohost tools regen-conf nginx --force</cmd>.",
"diagnosis_unknown_categories": "The following categories are unknown: {categories}", "diagnosis_unknown_categories": "The following categories are unknown: {categories}",
"diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.", "diagnosis_never_ran_yet": "It looks like this server was setup recently and there's no diagnosis report to show yet. You should start by running a full diagnosis, either from the webadmin or using 'yunohost diagnosis run' from the command line.",
"diagnosis_processes_killed_by_oom_reaper": "Some processes were recently killed by the system because it ran out of memory. This is typically symptomatic of a lack of memory on the system or of a process that ate up to much memory. Summary of the processes killed:\n{kills_summary}",
"domain_cannot_remove_main": "You cannot remove '{domain:s}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n <another-domain>'; here is the list of candidate domains: {other_domains:s}", "domain_cannot_remove_main": "You cannot remove '{domain:s}' since it's the main domain, you first need to set another domain as the main domain using 'yunohost domain main-domain -n <another-domain>'; here is the list of candidate domains: {other_domains:s}",
"domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.", "domain_cannot_add_xmpp_upload": "You cannot add domains starting with 'xmpp-upload.'. This kind of name is reserved for the XMPP upload feature integrated in YunoHost.",
"domain_cannot_remove_main_add_new_one": "You cannot remove '{domain:s}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add <another-domain.com>', then set is as the main domain using 'yunohost domain main-domain -n <another-domain.com>' and then you can remove the domain '{domain:s}' using 'yunohost domain remove {domain:s}'.'", "domain_cannot_remove_main_add_new_one": "You cannot remove '{domain:s}' since it's the main domain and your only domain, you need to first add another domain using 'yunohost domain add <another-domain.com>', then set is as the main domain using 'yunohost domain main-domain -n <another-domain.com>' and then you can remove the domain '{domain:s}' using 'yunohost domain remove {domain:s}'.'",
@ -313,10 +316,6 @@
"global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'", "global_settings_key_doesnt_exists": "The key '{settings_key:s}' does not exist in the global settings, you can see all the available keys by running 'yunohost settings list'",
"global_settings_reset_success": "Previous settings now backed up to {path:s}", "global_settings_reset_success": "Previous settings now backed up to {path:s}",
"global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server", "global_settings_setting_pop3_enabled": "Enable the POP3 protocol for the mail server",
"global_settings_setting_example_bool": "Example boolean option",
"global_settings_setting_example_enum": "Example enum option",
"global_settings_setting_example_int": "Example int option",
"global_settings_setting_example_string": "Example string option",
"global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)", "global_settings_setting_security_nginx_compatibility": "Compatibility vs. security tradeoff for the web server NGINX. Affects the ciphers (and other security-related aspects)",
"global_settings_setting_security_password_admin_strength": "Admin password strength", "global_settings_setting_security_password_admin_strength": "Admin password strength",
"global_settings_setting_security_password_user_strength": "User password strength", "global_settings_setting_security_password_user_strength": "User password strength",
@ -325,6 +324,7 @@
"global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discard it and save it in /etc/yunohost/settings-unknown.json", "global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discard it and save it in /etc/yunohost/settings-unknown.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration", "global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration",
"global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail", "global_settings_setting_smtp_allow_ipv6": "Allow the use of IPv6 to receive and send mail",
"global_settings_setting_backup_compress_tar_archives": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.",
"global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it is not a type supported by the system.", "global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it is 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 long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation of characters (uppercase, lowercase, digits and special characters).", "good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters long—though it is good practice to use a longer password (i.e. a passphrase) and/or to use a variation 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 long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation 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 long—though it is good practice to use a longer password (i.e. a passphrase) and/or to a variation of characters (uppercase, lowercase, digits and special characters).",
@ -355,7 +355,6 @@
"ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it", "ip6tables_unavailable": "You cannot play with ip6tables here. You are either in a container or your kernel does not support it",
"iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it", "iptables_unavailable": "You cannot play with iptables here. You are either in a container or your kernel does not support it",
"log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'", "log_corrupted_md_file": "The YAML metadata file associated with logs is damaged: '{md_file}\nError: {error}'",
"log_category_404": "The log category '{category}' does not exist",
"log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'", "log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'",
"log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by <a href=\"#/tools/logs/{name}\">clicking here</a> to get help", "log_link_to_failed_log": "Could not complete the operation '{desc}'. Please provide the full log of this operation by <a href=\"#/tools/logs/{name}\">clicking here</a> to get help",
@ -391,7 +390,7 @@
"log_user_group_create": "Create '{}' group", "log_user_group_create": "Create '{}' group",
"log_user_group_delete": "Delete '{}' group", "log_user_group_delete": "Delete '{}' group",
"log_user_group_update": "Update '{}' group", "log_user_group_update": "Update '{}' group",
"log_user_update": "Update user info of '{}'", "log_user_update": "Update info for user '{}'",
"log_user_permission_update": "Update accesses for permission '{}'", "log_user_permission_update": "Update accesses for permission '{}'",
"log_user_permission_reset": "Reset permission '{}'", "log_user_permission_reset": "Reset permission '{}'",
"log_domain_main_domain": "Make '{}' the main domain", "log_domain_main_domain": "Make '{}' the main domain",

View file

@ -12,10 +12,10 @@
"app_install_files_invalid": "Fichiers dinstallation incorrects", "app_install_files_invalid": "Fichiers dinstallation incorrects",
"app_manifest_invalid": "Manifeste dapplication incorrect : {error}", "app_manifest_invalid": "Manifeste dapplication incorrect : {error}",
"app_not_correctly_installed": "{app:s} semble être mal installé", "app_not_correctly_installed": "{app:s} semble être mal installé",
"app_not_installed": "Nous navons pas trouvé lapplication « {app:s} » dans la liste des applications installées : {all_apps}", "app_not_installed": "Nous navons pas trouvé {app:s} dans la liste des applications installées : {all_apps}",
"app_not_properly_removed": "{app:s} na pas été supprimé correctement", "app_not_properly_removed": "{app:s} na pas été supprimé correctement",
"app_removed": "{app:s} supprimé", "app_removed": "{app:s} supprimé",
"app_requirements_checking": "Vérification des paquets requis pour {app}", "app_requirements_checking": "Vérification des paquets requis pour {app}...",
"app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}", "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, lURL est-elle correcte ?", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources, lURL est-elle correcte ?",
"app_unknown": "Application inconnue", "app_unknown": "Application inconnue",
@ -28,8 +28,8 @@
"ask_main_domain": "Domaine principal", "ask_main_domain": "Domaine principal",
"ask_new_admin_password": "Nouveau mot de passe dadministration", "ask_new_admin_password": "Nouveau mot de passe dadministration",
"ask_password": "Mot de passe", "ask_password": "Mot de passe",
"backup_app_failed": "Impossible de sauvegarder lapplication '{app:s}'", "backup_app_failed": "Impossible de sauvegarder {app:s}",
"backup_archive_app_not_found": "Lapplication '{app:s}' na pas été trouvée dans larchive de la sauvegarde", "backup_archive_app_not_found": "{app:s} na pas été trouvée dans larchive de la sauvegarde",
"backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà.", "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà.",
"backup_archive_name_unknown": "Larchive locale de sauvegarde nommée '{name:s}' est inconnue", "backup_archive_name_unknown": "Larchive locale de sauvegarde nommée '{name:s}' est inconnue",
"backup_archive_open_failed": "Impossible douvrir larchive de la sauvegarde", "backup_archive_open_failed": "Impossible douvrir larchive de la sauvegarde",
@ -44,7 +44,7 @@
"backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives", "backup_output_directory_forbidden": "Choisissez un répertoire de destination différent. Les sauvegardes ne peuvent pas être créées dans les sous-dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
"backup_output_directory_not_empty": "Le répertoire de destination nest pas vide", "backup_output_directory_not_empty": "Le répertoire de destination nest pas vide",
"backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde", "backup_output_directory_required": "Vous devez spécifier un dossier de destination pour la sauvegarde",
"backup_running_hooks": "Exécution des scripts de sauvegarde", "backup_running_hooks": "Exécution des scripts de sauvegarde...",
"custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app:s}", "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application personnalisée {app:s}",
"domain_cert_gen_failed": "Impossible de générer le certificat", "domain_cert_gen_failed": "Impossible de générer le certificat",
"domain_created": "Le domaine a été créé", "domain_created": "Le domaine a été créé",
@ -63,15 +63,15 @@
"dyndns_cron_removed": "La tâche cron pour le domaine DynDNS enlevée", "dyndns_cron_removed": "La tâche cron pour le domaine DynDNS enlevée",
"dyndns_ip_update_failed": "Impossible de mettre à jour ladresse IP sur le domaine DynDNS", "dyndns_ip_update_failed": "Impossible de mettre à jour ladresse IP sur le domaine DynDNS",
"dyndns_ip_updated": "Mise à jour de votre IP pour le domaine DynDNS", "dyndns_ip_updated": "Mise à jour de votre IP pour le domaine DynDNS",
"dyndns_key_generating": "Génération de la clé DNS, cela peut prendre un certain temps.", "dyndns_key_generating": "Génération de la clé DNS..., cela peut prendre un certain temps.",
"dyndns_key_not_found": "Clé DNS introuvable pour le domaine", "dyndns_key_not_found": "Clé DNS introuvable pour le domaine",
"dyndns_no_domain_registered": "Aucun domaine enregistré avec DynDNS", "dyndns_no_domain_registered": "Aucun domaine enregistré avec DynDNS",
"dyndns_registered": "Domaine DynDNS enregistré", "dyndns_registered": "Domaine DynDNS enregistré",
"dyndns_registration_failed": "Impossible denregistrer le domaine DynDNS : {error:s}", "dyndns_registration_failed": "Impossible denregistrer le domaine DynDNS : {error:s}",
"dyndns_unavailable": "Le domaine {domain:s} est indisponible.", "dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
"executing_command": "Exécution de la commande '{command:s}'", "executing_command": "Exécution de la commande '{command:s}'...",
"executing_script": "Exécution du script '{script:s}'", "executing_script": "Exécution du script '{script:s}'...",
"extracting": "Extraction en cours", "extracting": "Extraction en cours...",
"field_invalid": "Champ incorrect : '{:s}'", "field_invalid": "Champ incorrect : '{:s}'",
"firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reload_failed": "Impossible de recharger le pare-feu",
"firewall_reloaded": "Pare-feu rechargé", "firewall_reloaded": "Pare-feu rechargé",
@ -163,7 +163,7 @@
"certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner cela)", "certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de vouloir remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner cela)",
"certmanager_domain_unknown": "Domaine {domain:s} inconnu", "certmanager_domain_unknown": "Domaine {domain:s} inconnu",
"certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} nest pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)", "certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} nest pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force pour cela)",
"certmanager_certificate_fetching_or_enabling_failed": "Il semble que lactivation du nouveau certificat pour {domain:s} a échoué", "certmanager_certificate_fetching_or_enabling_failed": "Il semble que lactivation du nouveau certificat pour {domain:s} a échoué...",
"certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} nest pas émis par Lets Encrypt. Impossible de le renouveler automatiquement !", "certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} nest pas émis par Lets Encrypt. Impossible de le renouveler automatiquement !",
"certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} nest pas sur le point dexpirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)", "certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} nest pas sur le point dexpirer ! (Vous pouvez utiliser --force si vous savez ce que vous faites)",
"certmanager_domain_http_not_working": "Le domaine {domain:s} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", "certmanager_domain_http_not_working": "Le domaine {domain:s} ne semble pas être accessible via HTTP. Merci de vérifier la catégorie 'Web' dans le diagnostic pour plus d'informations. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
@ -209,13 +209,13 @@
"global_settings_unknown_type": "Situation inattendue : la configuration {setting:s} semble avoir le type {unknown_type:s} mais celui-ci nest pas pris en charge par le système.", "global_settings_unknown_type": "Situation inattendue : la configuration {setting:s} semble avoir le type {unknown_type:s} mais celui-ci nest pas pris en charge par le système.",
"global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key:s}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json", "global_settings_unknown_setting_from_settings_file": "Clé inconnue dans les paramètres : '{setting_key:s}', rejet de cette clé et sauvegarde de celle-ci dans /etc/yunohost/unkown_settings.json",
"backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter", "backup_abstract_method": "Cette méthode de sauvegarde reste à implémenter",
"backup_applying_method_tar": "Création de larchive TAR de la sauvegarde", "backup_applying_method_tar": "Création de larchive TAR de la sauvegarde...",
"backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder", "backup_applying_method_copy": "Copie de tous les fichiers à sauvegarder...",
"backup_applying_method_borg": "Envoi de tous les fichiers à sauvegarder dans le répertoire borg-backup", "backup_applying_method_borg": "Envoi de tous les fichiers à sauvegarder dans le répertoire borg-backup...",
"backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method:s}'", "backup_applying_method_custom": "Appel de la méthode de sauvegarde personnalisée '{method:s}'...",
"backup_archive_system_part_not_available": "La partie '{part:s}' du système nest pas disponible dans cette sauvegarde", "backup_archive_system_part_not_available": "La partie '{part:s}' du système nest pas disponible dans cette sauvegarde",
"backup_archive_writing_error": "Impossible dajouter des fichiers '{source:s}' (nommés dans larchive : '{dest:s}') à sauvegarder dans larchive compressée '{archive:s}'", "backup_archive_writing_error": "Impossible dajouter des fichiers '{source:s}' (nommés dans larchive : '{dest:s}') à sauvegarder dans larchive compressée '{archive:s}'",
"backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size:s} temporairement ? (Cette méthode est utilisée car certains fichiers nont pas pu être préparés avec une méthode plus efficace.)", "backup_ask_for_copying_if_needed": "Voulez-vous effectuer la sauvegarde en utilisant {size:s}Mo temporairement ? (Cette méthode est utilisée car certains fichiers nont pas pu être préparés avec une méthode plus efficace.)",
"backup_borg_not_implemented": "La méthode de sauvegarde Borg nest pas encore implémentée", "backup_borg_not_implemented": "La méthode de sauvegarde Borg nest pas encore implémentée",
"backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de larchive décompressée", "backup_cant_mount_uncompress_archive": "Impossible de monter en lecture seule le dossier de larchive décompressée",
"backup_copying_to_organize_the_archive": "Copie de {size:s} Mo pour organiser larchive", "backup_copying_to_organize_the_archive": "Copie de {size:s} Mo pour organiser larchive",
@ -231,7 +231,7 @@
"backup_system_part_failed": "Impossible de sauvegarder la partie '{part:s}' du système", "backup_system_part_failed": "Impossible de sauvegarder la partie '{part:s}' du système",
"backup_unable_to_organize_files": "Impossible dutiliser la méthode rapide pour organiser les fichiers dans larchive", "backup_unable_to_organize_files": "Impossible dutiliser la méthode rapide pour organiser les fichiers dans larchive",
"backup_with_no_backup_script_for_app": "Lapplication {app:s} na pas de script de sauvegarde. Ignorer.", "backup_with_no_backup_script_for_app": "Lapplication {app:s} na pas de script de sauvegarde. Ignorer.",
"backup_with_no_restore_script_for_app": "Lapplication « {app:s} » na pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.", "backup_with_no_restore_script_for_app": "{app:s} na pas de script de restauration, vous ne pourrez pas restaurer automatiquement la sauvegarde de cette application.",
"global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason:s}", "global_settings_cant_serialize_settings": "Échec de la sérialisation des données de paramétrage car : {reason:s}",
"restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire", "restore_removing_tmp_dir_failed": "Impossible de sauvegarder un ancien dossier temporaire",
"restore_extracting": "Extraction des fichiers nécessaires depuis larchive…", "restore_extracting": "Extraction des fichiers nécessaires depuis larchive…",
@ -253,7 +253,7 @@
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.", "dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur DynDNS {provider:s} ne peut pas fournir le domaine {domain:s}.", "dyndns_domain_not_provided": "Le fournisseur DynDNS {provider:s} ne peut pas fournir le domaine {domain:s}.",
"app_make_default_location_already_used": "Impossible de configurer lapplication '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par lapplication '{other_app}'", "app_make_default_location_already_used": "Impossible de configurer lapplication '{app}' par défaut pour le domaine '{domain}' car il est déjà utilisé par lapplication '{other_app}'",
"app_upgrade_app_name": "Mise à jour de lapplication {app} …", "app_upgrade_app_name": "Mise à jour de {app}...",
"backup_output_symlink_dir_broken": "Votre répertoire darchivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.", "backup_output_symlink_dir_broken": "Votre répertoire darchivage '{path:s}' est un lien symbolique brisé. Peut-être avez-vous oublié de re/monter ou de brancher le support de stockage sur lequel il pointe.",
"migrate_tsig_end": "La migration à HMAC-SHA-512 est terminée", "migrate_tsig_end": "La migration à HMAC-SHA-512 est terminée",
"migrate_tsig_failed": "La migration du domaine DynDNS {domain} à HMAC-SHA-512 a échoué. Annulation des modifications. Erreur : {error_code} - {error}", "migrate_tsig_failed": "La migration du domaine DynDNS {domain} à HMAC-SHA-512 a échoué. Annulation des modifications. Erreur : {error_code} - {error}",
@ -352,15 +352,15 @@
"root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost na pas pu le propager au mot de passe root !", "root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost na pas pu le propager au mot de passe root !",
"aborting": "Annulation.", "aborting": "Annulation.",
"app_not_upgraded": "Lapplication {failed_app} na pas été mise à jour et par conséquence les applications suivantes nont pas été mises à jour : {apps}", "app_not_upgraded": "Lapplication {failed_app} na pas été mise à jour et par conséquence les applications suivantes nont pas été mises à jour : {apps}",
"app_start_install": "Installation de lapplication {app} …", "app_start_install": "Installation de {app}...",
"app_start_remove": "Suppression de lapplication {app} …", "app_start_remove": "Suppression de {app}...",
"app_start_backup": "Collecte des fichiers devant être sauvegardés pour lapplication {app} …", "app_start_backup": "Collecte des fichiers devant être sauvegardés pour {app}...",
"app_start_restore": "Restauration de lapplication {app} …", "app_start_restore": "Restauration de {app}...",
"app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}", "app_upgrade_several_apps": "Les applications suivantes seront mises à jour : {apps}",
"ask_new_domain": "Nouveau domaine", "ask_new_domain": "Nouveau domaine",
"ask_new_path": "Nouveau chemin", "ask_new_path": "Nouveau chemin",
"backup_actually_backuping": "Création dune archive de sauvegarde à partir des fichiers collectés", "backup_actually_backuping": "Création dune archive de sauvegarde à partir des fichiers collectés...",
"backup_mount_archive_for_restore": "Préparation de larchive pour restauration", "backup_mount_archive_for_restore": "Préparation de larchive pour restauration...",
"confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais nest pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que lauthentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. Linstaller quand même ? [{answers:s}] ", "confirm_app_install_warning": "Avertissement : cette application peut fonctionner mais nest pas bien intégrée dans YunoHost. Certaines fonctionnalités telles que lauthentification unique et la sauvegarde/restauration peuvent ne pas être disponibles. Linstaller quand même ? [{answers:s}] ",
"confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS linstaller à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système … Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'", "confirm_app_install_danger": "DANGER ! Cette application est connue pour être encore expérimentale (si elle ne fonctionne pas explicitement) ! Vous ne devriez probablement PAS linstaller à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système … Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
"confirm_app_install_thirdparty": "DANGER! Cette application ne fait pas partie du catalogue d'applications de Yunohost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système ... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'", "confirm_app_install_thirdparty": "DANGER! Cette application ne fait pas partie du catalogue d'applications de Yunohost. L'installation d'applications tierces peut compromettre l'intégrité et la sécurité de votre système. Vous ne devriez probablement PAS l'installer à moins de savoir ce que vous faites. AUCUN SUPPORT ne sera fourni si cette application ne fonctionne pas ou casse votre système ... Si vous êtes prêt à prendre ce risque de toute façon, tapez '{answers:s}'",
@ -429,7 +429,7 @@
"tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à l'administrateur Web. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant la « liste des journaux yunohost » (à partir de la ligne de commande).", "tools_upgrade_special_packages_explanation": "La mise à niveau spécifique à YunoHost se poursuivra en arrière-plan. Veuillez ne pas lancer d'autres actions sur votre serveur pendant les 10 prochaines minutes (selon la vitesse du matériel). Après cela, vous devrez peut-être vous reconnecter à l'administrateur Web. Le journal de mise à niveau sera disponible dans Outils → Journal (dans le webadmin) ou en utilisant la « liste des journaux yunohost » (à partir de la ligne de commande).",
"update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "update_apt_cache_failed": "Impossible de mettre à jour le cache APT (gestionnaire de paquets Debian). Voici un extrait du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
"update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}", "update_apt_cache_warning": "Des erreurs se sont produites lors de la mise à jour du cache APT (gestionnaire de paquets Debian). Voici un extrait des lignes du fichier sources.list qui pourrait vous aider à identifier les lignes problématiques :\n{sourceslist}",
"backup_permission": "Permission de sauvegarde pour lapplication {app:s}", "backup_permission": "Permission de sauvegarde pour {app:s}",
"group_created": "Le groupe '{group}' a été créé", "group_created": "Le groupe '{group}' a été créé",
"group_deleted": "Suppression du groupe '{group}'", "group_deleted": "Suppression du groupe '{group}'",
"group_unknown": "Le groupe {group:s} est inconnu", "group_unknown": "Le groupe {group:s} est inconnu",
@ -501,7 +501,7 @@
"app_install_failed": "Impossible dinstaller {app} : {error}", "app_install_failed": "Impossible dinstaller {app} : {error}",
"app_install_script_failed": "Une erreur est survenue dans le script dinstallation de lapplication", "app_install_script_failed": "Une erreur est survenue dans le script dinstallation de lapplication",
"permission_require_account": "Permission {permission} na de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.", "permission_require_account": "Permission {permission} na de sens que pour les utilisateurs ayant un compte et ne peut donc pas être activé pour les visiteurs.",
"app_remove_after_failed_install": "Supprimer lapplication après léchec de linstallation", "app_remove_after_failed_install": "Supprimer lapplication après léchec de linstallation...",
"diagnosis_display_tip_web": "Vous pouvez aller à la section Diagnostic (dans lécran daccueil) pour voir les problèmes rencontrés.", "diagnosis_display_tip_web": "Vous pouvez aller à la section Diagnostic (dans lécran daccueil) pour voir les problèmes rencontrés.",
"diagnosis_cant_run_because_of_dep": "Impossible dexécuter le diagnostic pour {category} alors quil existe des problèmes importants liés à {dep}.", "diagnosis_cant_run_because_of_dep": "Impossible dexécuter le diagnostic pour {category} alors quil existe des problèmes importants liés à {dep}.",
"diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !", "diagnosis_found_errors": "Trouvé {errors} problème(s) significatif(s) lié(s) à {category} !",
@ -522,13 +522,13 @@
"diagnosis_display_tip_cli": "Vous pouvez exécuter 'yunohost diagnosis show --issues' pour afficher les problèmes détectés.", "diagnosis_display_tip_cli": "Vous pouvez exécuter 'yunohost diagnosis show --issues' pour afficher les problèmes détectés.",
"diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}", "diagnosis_failed_for_category": "Échec du diagnostic pour la catégorie '{category}': {error}",
"diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment!)", "diagnosis_cache_still_valid": "(Le cache est encore valide pour le diagnostic {category}. Il ne sera pas re-diagnostiqué pour le moment!)",
"diagnosis_ignored_issues": "(+ {nb_ignored} problèmes ignorée(s))", "diagnosis_ignored_issues": "(+ {nb_ignored} problème(s) ignoré(s))",
"diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.", "diagnosis_found_warnings": "Trouvé {warnings} objet(s) pouvant être amélioré(s) pour {category}.",
"diagnosis_everything_ok": "Tout semble bien pour {category} !", "diagnosis_everything_ok": "Tout semble bien pour {category} !",
"diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}", "diagnosis_failed": "Échec de la récupération du résultat du diagnostic pour la catégorie '{category}' : {error}",
"diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4 !", "diagnosis_ip_connected_ipv4": "Le serveur est connecté à Internet en IPv4!",
"diagnosis_ip_no_ipv4": "Le serveur ne dispose pas dune adresse IPv4.", "diagnosis_ip_no_ipv4": "Le serveur ne dispose pas dune adresse IPv4.",
"diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6 !", "diagnosis_ip_connected_ipv6": "Le serveur est connecté à Internet en IPv6!",
"diagnosis_ip_no_ipv6": "Le serveur ne dispose pas dune adresse IPv6.", "diagnosis_ip_no_ipv6": "Le serveur ne dispose pas dune adresse IPv6.",
"diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !", "diagnosis_ip_dnsresolution_working": "La résolution de nom de domaine fonctionne !",
"diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque … Un pare-feu bloque-t-il les requêtes DNS ?", "diagnosis_ip_broken_dnsresolution": "La résolution du nom de domaine semble interrompue pour une raison quelconque … Un pare-feu bloque-t-il les requêtes DNS ?",
@ -550,7 +550,7 @@
"diagnosis_regenconf_manually_modified_debian_details": "Cela peut probablement être OK, mais il faut garder un œil dessus …", "diagnosis_regenconf_manually_modified_debian_details": "Cela peut probablement être OK, mais il faut garder un œil dessus …",
"apps_catalog_init_success": "Système de catalogue dapplications initialisé !", "apps_catalog_init_success": "Système de catalogue dapplications initialisé !",
"apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}", "apps_catalog_failed_to_download": "Impossible de télécharger le catalogue des applications {apps_catalog} : {error}",
"diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer de courrier électronique à dautres serveurs.", "diagnosis_mail_outgoing_port_25_blocked": "Le port sortant 25 semble être bloqué. Vous devriez essayer de le débloquer dans le panneau de configuration de votre fournisseur de services Internet (ou hébergeur). En attendant, le serveur ne pourra pas envoyer des courriels à dautres serveurs.",
"domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain:s}' car il sagit du domaine principal et de votre seul domaine. Vous devez dabord ajouter un autre domaine à laide de 'yunohost domain add <another-domain.com>', puis définir comme domaine principal à laide de 'yunohost domain main-domain -n <nom-dun-autre-domaine.com>' et vous pouvez ensuite supprimer le domaine '{domain:s}' à laide de 'yunohost domain remove {domain:s}'.'", "domain_cannot_remove_main_add_new_one": "Vous ne pouvez pas supprimer '{domain:s}' car il sagit du domaine principal et de votre seul domaine. Vous devez dabord ajouter un autre domaine à laide de 'yunohost domain add <another-domain.com>', puis définir comme domaine principal à laide de 'yunohost domain main-domain -n <nom-dun-autre-domaine.com>' et vous pouvez ensuite supprimer le domaine '{domain:s}' à laide de 'yunohost domain remove {domain:s}'.'",
"diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus dinformations.", "diagnosis_security_vulnerable_to_meltdown_details": "Pour résoudre ce problème, vous devez mettre à niveau votre système et redémarrer pour charger le nouveau noyau Linux (ou contacter votre fournisseur de serveur si cela ne fonctionne pas). Voir https://meltdownattack.com/ pour plus dinformations.",
"diagnosis_description_basesystem": "Système de base", "diagnosis_description_basesystem": "Système de base",
@ -596,14 +596,14 @@
"diagnosis_description_web": "Web", "diagnosis_description_web": "Web",
"diagnosis_basesystem_hardware_board": "Le modèle de carte du serveur est {model}", "diagnosis_basesystem_hardware_board": "Le modèle de carte du serveur est {model}",
"diagnosis_basesystem_hardware": "Larchitecture du serveur est {virt} {arch}", "diagnosis_basesystem_hardware": "Larchitecture du serveur est {virt} {arch}",
"group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer", "group_already_exist_on_system_but_removing_it": "Le groupe {group} est déjà présent dans les groupes du système, mais YunoHost va le supprimer...",
"certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain:s}' ne résout pas vers la même adresse IP que '{domain:s}'. Certaines fonctionnalités seront indisponibles tant que vous naurez pas corrigé cela et regénéré le certificat.", "certmanager_warning_subdomain_dns_record": "Le sous-domaine '{subdomain:s}' ne résout pas vers la même adresse IP que '{domain:s}'. Certaines fonctionnalités seront indisponibles tant que vous naurez pas corrigé cela et regénéré le certificat.",
"domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité dupload XMPP intégrée dans YunoHost.", "domain_cannot_add_xmpp_upload": "Vous ne pouvez pas ajouter de domaine commençant par 'xmpp-upload.'. Ce type de nom est réservé à la fonctionnalité dupload XMPP intégrée dans YunoHost.",
"diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des e-mails (le port sortant 25 n'est pas bloqué).", "diagnosis_mail_outgoing_port_25_ok": "Le serveur de messagerie SMTP peut envoyer des courriels (le port sortant 25 n'est pas bloqué).",
"diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez dabord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface dhébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).", "diagnosis_mail_outgoing_port_25_blocked_details": "Vous devez dabord essayer de débloquer le port sortant 25 dans votre interface de routeur Internet ou votre interface dhébergement. (Certains hébergeurs peuvent vous demander de leur envoyer un ticket de support pour cela).",
"diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}", "diagnosis_mail_ehlo_bad_answer": "Un service non SMTP a répondu sur le port 25 en IPv{ipversion}",
"diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond au lieu de votre serveur.", "diagnosis_mail_ehlo_bad_answer_details": "Cela peut être dû à une autre machine qui répond au lieu de votre serveur.",
"diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des e-mails.", "diagnosis_mail_ehlo_wrong": "Un autre serveur de messagerie SMTP répond sur IPv{ipversion}. Votre serveur ne sera probablement pas en mesure de recevoir des courriel.",
"diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de lextérieur en IPv{ipversion}.", "diagnosis_mail_ehlo_could_not_diagnose": "Impossible de diagnostiquer si le serveur de messagerie postfix est accessible de lextérieur en IPv{ipversion}.",
"diagnosis_mail_ehlo_could_not_diagnose_details": "Erreur : {error}", "diagnosis_mail_ehlo_could_not_diagnose_details": "Erreur : {error}",
"diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse nest défini pour IPv{ipversion}. Certains e-mails seront peut-être refusés ou considérés comme des spam.", "diagnosis_mail_fcrdns_dns_missing": "Aucun DNS inverse nest défini pour IPv{ipversion}. Certains e-mails seront peut-être refusés ou considérés comme des spam.",
@ -623,8 +623,8 @@
"diagnosis_ip_local": "IP locale : <code>{local}</code>", "diagnosis_ip_local": "IP locale : <code>{local}</code>",
"diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> si vous avez besoin daide pour configurer les enregistrements DNS.", "diagnosis_dns_point_to_doc": "Veuillez consulter la documentation sur <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a> si vous avez besoin daide pour configurer les enregistrements DNS.",
"diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce quils ne se soucient pas de la neutralité du Net. <br> - Certains dentre eux offrent lalternative d'<a href='https://yunohost.org/#/smtp_relay'>utiliser un serveur de messagerie relai</a> bien que cela implique que le relai sera en mesure despionner votre trafic de messagerie. <br> - Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a> <br> - Vous pouvez également envisager de passer à <a href='https://yunohost.org/#/isp'>un fournisseur plus respectueux de la neutralité du net</a>", "diagnosis_mail_outgoing_port_25_blocked_relay_vpn": "Certains fournisseurs ne vous laisseront pas débloquer le port sortant 25 parce quils ne se soucient pas de la neutralité du Net. <br> - Certains dentre eux offrent lalternative d'<a href='https://yunohost.org/#/smtp_relay'>utiliser un serveur de messagerie relai</a> bien que cela implique que le relai sera en mesure despionner votre trafic de messagerie. <br> - Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a> <br> - Vous pouvez également envisager de passer à <a href='https://yunohost.org/#/isp'>un fournisseur plus respectueux de la neutralité du net</a>",
"diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des e-mails !", "diagnosis_mail_ehlo_ok": "Le serveur de messagerie SMTP est accessible de l'extérieur et peut donc recevoir des courriels!",
"diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de lextérieur en IPv{ipversion}. Il ne pourra pas recevoir de-mails.", "diagnosis_mail_ehlo_unreachable": "Le serveur de messagerie SMTP est inaccessible de lextérieur en IPv{ipversion}. Il ne pourra pas recevoir des courriels.",
"diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible. <br> 1. La cause la plus courante de ce problème est que le port 25 <a href='https://yunohost.org/isp_box_config'>n'est pas correctement redirigé vers votre serveur</a>. <br> 2. Vous devez également vous assurer que le service postfix est en cours d'exécution. <br> 3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.", "diagnosis_mail_ehlo_unreachable_details": "Impossible d'ouvrir une connexion sur le port 25 à votre serveur en IPv{ipversion}. Il semble inaccessible. <br> 1. La cause la plus courante de ce problème est que le port 25 <a href='https://yunohost.org/isp_box_config'>n'est pas correctement redirigé vers votre serveur</a>. <br> 2. Vous devez également vous assurer que le service postfix est en cours d'exécution. <br> 3. Sur les configurations plus complexes: assurez-vous qu'aucun pare-feu ou proxy inversé n'interfère.",
"diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur. <br> EHLO reçu: <code>{wrong_ehlo}</code> <br> Attendu : <code>{right_ehlo}</code> <br> La cause la plus courante ce problème est que le port 25 <a href='https://yunohost.org/isp_box_config'> nest pas correctement redirigé vers votre serveur </a>. Vous pouvez également vous assurer quaucun pare-feu ou proxy inversé ninterfère.", "diagnosis_mail_ehlo_wrong_details": "Le EHLO reçu par le serveur de diagnostique distant en IPv{ipversion} est différent du domaine de votre serveur. <br> EHLO reçu: <code>{wrong_ehlo}</code> <br> Attendu : <code>{right_ehlo}</code> <br> La cause la plus courante ce problème est que le port 25 <a href='https://yunohost.org/isp_box_config'> nest pas correctement redirigé vers votre serveur </a>. Vous pouvez également vous assurer quaucun pare-feu ou proxy inversé ninterfère.",
"diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée …). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes : <br> - Certains FAI fournissent lalternative de <a href='https://yunohost.org/#/smtp_relay'>à laide dun relais de serveur de messagerie</a> bien que cela implique que le relais pourra espionner votre trafic de messagerie. <br> - Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a> <br> - Enfin, il est également possible de <a href='https://yunohost.org/#/isp'>changer de fournisseur</a>", "diagnosis_mail_fcrdns_nok_alternatives_4": "Certains fournisseurs ne vous laisseront pas configurer votre DNS inversé (ou leur fonctionnalité pourrait être cassée …). Si vous rencontrez des problèmes à cause de cela, envisagez les solutions suivantes : <br> - Certains FAI fournissent lalternative de <a href='https://yunohost.org/#/smtp_relay'>à laide dun relais de serveur de messagerie</a> bien que cela implique que le relais pourra espionner votre trafic de messagerie. <br> - Une alternative respectueuse de la vie privée consiste à utiliser un VPN *avec une IP publique dédiée* pour contourner ce type de limites. Voir <a href='https://yunohost.org/#/vpn_advantage'>https://yunohost.org/#/vpn_advantage</a> <br> - Enfin, il est également possible de <a href='https://yunohost.org/#/isp'>changer de fournisseur</a>",
@ -648,7 +648,7 @@
"diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !", "diagnosis_domain_expiration_warning": "Certains domaines vont expirer prochainement !",
"diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !", "diagnosis_domain_expiration_error": "Certains domaines vont expirer TRÈS PROCHAINEMENT !",
"diagnosis_domain_expires_in": "{domain} expire dans {days} jours.", "diagnosis_domain_expires_in": "{domain} expire dans {days} jours.",
"certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine %s. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostique pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)", "certmanager_domain_not_diagnosed_yet": "Il n'y a pas encore de résultat de diagnostic pour le domaine {domain}. Merci de relancer un diagnostic pour les catégories 'Enregistrements DNS' et 'Web' dans la section Diagnostique pour vérifier si le domaine est prêt pour Let's Encrypt. (Ou si vous savez ce que vous faites, utilisez '--no-checks' pour désactiver la vérification.)",
"diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement lespérance de vie du périphérique.", "diagnosis_swap_tip": "Merci d'être prudent et conscient que si vous hébergez une partition SWAP sur une carte SD ou un disque SSD, cela risque de réduire drastiquement lespérance de vie du périphérique.",
"restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}", "restore_already_installed_apps": "Les applications suivantes ne peuvent pas être restaurées car elles sont déjà installées : {apps}",
"regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.", "regenconf_need_to_explicitly_specify_ssh": "La configuration de ssh a été modifiée manuellement. Vous devez explicitement indiquer la mention --force à \"ssh\" pour appliquer les changements.",
@ -667,6 +667,6 @@
"migration_0015_start": "Démarrage de la migration vers Buster", "migration_0015_start": "Démarrage de la migration vers Buster",
"migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x", "migration_description_0015_migrate_to_buster": "Mise à niveau du système vers Debian Buster et YunoHost 4.x",
"diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par Yunohost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant <cmd>yunohost dyndns update --force</cmd>.", "diagnosis_dns_try_dyndns_update_force": "La configuration DNS de ce domaine devrait être automatiquement gérée par Yunohost. Si ce n'est pas le cas, vous pouvez essayer de forcer une mise à jour en utilisant <cmd>yunohost dyndns update --force</cmd>.",
"app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de Yunohost. Vous devriez probablement envisager de mettre à jour votre système.", "app_packaging_format_not_supported": "Cette application ne peut pas être installée car son format n'est pas pris en charge par votre version de YunoHost. Vous devriez probablement envisager de mettre à jour votre système.",
"migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de nginx : {certs}" "migration_0015_weak_certs": "Il a été constaté que les certificats suivants utilisent encore des algorithmes de signature peu robustes et doivent être mis à jour pour être compatibles avec la prochaine version de nginx : {certs}"
} }

View file

@ -1,11 +1,11 @@
{ {
"app_already_installed": "{app:s} è già installata", "app_already_installed": "{app:s} è già installata",
"app_extraction_failed": "Impossibile estrarre i file di installazione", "app_extraction_failed": "Impossibile estrarre i file di installazione",
"app_not_installed": "Impossibile trovare l'applicazione '{app:s}' nell'elenco delle applicazioni installate: {all_apps}", "app_not_installed": "Impossibile trovare l'applicazione {app:s} nell'elenco delle applicazioni installate: {all_apps}",
"app_unknown": "Applicazione sconosciuta", "app_unknown": "Applicazione sconosciuta",
"ask_email": "Indirizzo email", "ask_email": "Indirizzo email",
"ask_password": "Password", "ask_password": "Password",
"backup_archive_name_exists": "Il nome dell'archivio del backup è già esistente", "backup_archive_name_exists": "Il nome dell'archivio del backup è già esistente.",
"backup_created": "Backup completo", "backup_created": "Backup completo",
"backup_invalid_archive": "Archivio di backup non valido", "backup_invalid_archive": "Archivio di backup non valido",
"backup_output_directory_not_empty": "La directory di output non è vuota", "backup_output_directory_not_empty": "La directory di output non è vuota",
@ -37,22 +37,22 @@
"app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?", "app_sources_fetch_failed": "Impossibile riportare i file sorgenti, l'URL è corretto?",
"app_upgrade_failed": "Impossibile aggiornare {app:s}: {error}", "app_upgrade_failed": "Impossibile aggiornare {app:s}: {error}",
"app_upgraded": "{app:s} aggiornata", "app_upgraded": "{app:s} aggiornata",
"app_requirements_checking": "Controllo i pacchetti richiesti per {app}", "app_requirements_checking": "Controllo i pacchetti richiesti per {app}...",
"app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}", "app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}",
"ask_firstname": "Nome", "ask_firstname": "Nome",
"ask_lastname": "Cognome", "ask_lastname": "Cognome",
"ask_main_domain": "Dominio principale", "ask_main_domain": "Dominio principale",
"ask_new_admin_password": "Nuova password dell'amministrazione", "ask_new_admin_password": "Nuova password dell'amministrazione",
"backup_app_failed": "Non è possibile fare il backup dell'applicazione '{app:s}'", "backup_app_failed": "Non è possibile fare il backup {app:s}",
"backup_archive_app_not_found": "L'applicazione '{app:s}' non è stata trovata nel archivio di backup", "backup_archive_app_not_found": "{app:s} non è stata trovata nel archivio di backup",
"app_argument_choice_invalid": "Usa una delle seguenti scelte '{choices:s}' per il parametro '{name:s}'", "app_argument_choice_invalid": "Usa una delle seguenti scelte '{choices:s}' per il parametro '{name:s}'",
"app_argument_invalid": "Scegli un valore valido per il parametro '{name:s}': {error:s}", "app_argument_invalid": "Scegli un valore valido per il parametro '{name:s}': {error:s}",
"app_argument_required": "L'argomento '{name:s}' è requisito", "app_argument_required": "L'argomento '{name:s}' è requisito",
"app_id_invalid": "Identificativo dell'applicazione non valido", "app_id_invalid": "Identificativo dell'applicazione non valido",
"app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato", "app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato",
"backup_archive_broken_link": "Non è possibile accedere al archivio di backup (link rotto verso {path:s})", "backup_archive_broken_link": "Non è possibile accedere all'archivio di backup (link rotto verso {path:s})",
"backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto", "backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto",
"backup_archive_open_failed": "Non è possibile aprire l'archivio di backup", "backup_archive_open_failed": "Impossibile aprire l'archivio di backup",
"backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup", "backup_cleaning_failed": "Non è possibile pulire la directory temporanea di backup",
"backup_creation_failed": "La creazione del backup è fallita", "backup_creation_failed": "La creazione del backup è fallita",
"backup_delete_error": "Impossibile cancellare '{path:s}'", "backup_delete_error": "Impossibile cancellare '{path:s}'",
@ -179,15 +179,15 @@
"app_change_url_success": "L'URL dell'applicazione {app:s} è stato cambiato in {domain:s}{path:s}", "app_change_url_success": "L'URL dell'applicazione {app:s} è stato cambiato in {domain:s}{path:s}",
"app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio non riuscita perché il dominio {domain} è già in uso per l'altra applicazione '{other_app}'", "app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio non riuscita perché il dominio {domain} è già in uso per l'altra applicazione '{other_app}'",
"app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}", "app_location_unavailable": "Questo URL non è più disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}",
"app_upgrade_app_name": "Aggiornamento dell'applicazione {app}…", "app_upgrade_app_name": "Aggiornamento di {app}...",
"app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni", "app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni",
"backup_abstract_method": "Questo metodo di backup non è ancora stato implementato", "backup_abstract_method": "Questo metodo di backup deve essere ancora implementato",
"backup_applying_method_borg": "Inviando tutti i file da salvare nel backup nel deposito borg-backup…", "backup_applying_method_borg": "Invio di tutti i file del backup nel deposito borg-backup...",
"backup_applying_method_copy": "Copiando tutti i files nel 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_custom": "Chiamando il metodo di backup personalizzato '{method:s}'...",
"backup_applying_method_tar": "Creando l'archivio tar del backup…", "backup_applying_method_tar": "Creando l'archivio TAR del backup...",
"backup_archive_system_part_not_available": "La parte di sistema '{part:s}' non è disponibile in questo backup", "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", "backup_archive_writing_error": "Impossibile aggiungere i file '{source:s}' (indicati nell'archivio '{dest:s}') al backup nell'archivio compresso '{archive:s}'",
"backup_ask_for_copying_if_needed": "Alcuni files non possono essere preparati al backup utilizzando il metodo che consente di evitare il consumo temporaneo di spazio nel sistema. Per eseguire il backup, {size:s}MB dovranno essere utilizzati temporaneamente. Sei d'accordo?", "backup_ask_for_copying_if_needed": "Alcuni files non possono essere preparati al backup utilizzando il metodo che consente di evitare il consumo temporaneo di spazio nel sistema. Per eseguire il backup, {size:s}MB dovranno essere utilizzati temporaneamente. Sei d'accordo?",
"backup_borg_not_implemented": "Il metodo di backup Borg non è ancora stato implementato", "backup_borg_not_implemented": "Il metodo di backup Borg non è ancora stato implementato",
"backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa", "backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa",
@ -213,14 +213,14 @@
"aborting": "Annullamento.", "aborting": "Annullamento.",
"admin_password_too_long": "Per favore scegli una password più corta di 127 caratteri", "admin_password_too_long": "Per favore scegli una password più corta di 127 caratteri",
"app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}", "app_not_upgraded": "Impossibile aggiornare le applicazioni '{failed_app}' e di conseguenza l'aggiornamento delle seguenti applicazione è stato cancellato: {apps}",
"app_start_install": "Installando l'applicazione '{app}'…", "app_start_install": "Installando '{app}'...",
"app_start_remove": "Rimozione dell'applicazione {app}…", "app_start_remove": "Rimozione di {app}...",
"app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'", "app_start_backup": "Raccogliendo file da salvare nel backup per '{app}'...",
"app_start_restore": "Ripristino dell'applicazione '{app}'…", "app_start_restore": "Ripristino di '{app}'...",
"app_upgrade_several_apps": "Le seguenti app saranno aggiornate : {apps}", "app_upgrade_several_apps": "Le seguenti applicazioni saranno aggiornate : {apps}",
"ask_new_domain": "Nuovo dominio", "ask_new_domain": "Nuovo dominio",
"ask_new_path": "Nuovo percorso", "ask_new_path": "Nuovo percorso",
"backup_actually_backuping": "Creando un archivio di backup con i file raccolti…", "backup_actually_backuping": "Creazione di un archivio di backup con i file raccolti...",
"backup_mount_archive_for_restore": "Preparando l'archivio per il ripristino…", "backup_mount_archive_for_restore": "Preparando l'archivio per il ripristino…",
"certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain:s}!", "certmanager_cert_install_success_selfsigned": "Certificato autofirmato installato con successo per il dominio {domain:s}!",
"certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain:s}!", "certmanager_cert_renew_success": "Certificato di Let's Encrypt rinnovato con successo per il dominio {domain:s}!",
@ -336,9 +336,18 @@
"migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.", "migration_0003_system_not_fully_up_to_date": "Il tuo sistema non è completamente aggiornato. Per favore prima esegui un aggiornamento normale prima di migrare a stretch.",
"this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/apt (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo dpkg --configure -a`.", "this_action_broke_dpkg": "Questa azione ha danneggiato dpkg/apt (i gestori di pacchetti del sistema)… Puoi provare a risolvere questo problema connettendoti via SSH ed eseguendo `sudo dpkg --configure -a`.",
"app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}", "app_action_broke_system": "Questa azione sembra avere rotto questi servizi importanti: {services}",
"app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione", "app_remove_after_failed_install": "Rimozione dell'applicazione a causa del fallimento dell'installazione...",
"app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione", "app_install_script_failed": "Si è verificato un errore nello script di installazione dell'applicazione",
"app_install_failed": "Impossibile installare {app}:{error}", "app_install_failed": "Impossibile installare {app}:{error}",
"app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono state installate sul dominio '{domain}'. Dovresti invece usare un sotto-dominio dedicato per questa app.", "app_full_domain_unavailable": "Spiacente, questa app deve essere installata su un proprio dominio, ma altre applicazioni sono state installate sul dominio '{domain}'. Dovresti invece usare un sotto-dominio dedicato per questa app.",
"app_upgrade_script_failed": "È stato trovato un errore nello script di aggiornamento dell'applicazione" "app_upgrade_script_failed": "È stato trovato un errore nello script di aggiornamento dell'applicazione",
"apps_already_up_to_date": "Tutte le applicazioni sono aggiornate",
"apps_catalog_init_success": "Catalogo delle applicazioni inizializzato!",
"apps_catalog_updating": "Aggiornamento del catalogo delle applicazioni…",
"apps_catalog_failed_to_download": "Impossibile scaricare il catalogo delle applicazioni {apps_catalog} : {error}",
"apps_catalog_obsolete_cache": "La cache del catalogo della applicazioni è vuoto o obsoleto.",
"apps_catalog_update_success": "Il catalogo delle applicazioni è stato aggiornato!",
"backup_archive_corrupted": "Sembra che l'archivio di backup '{archive}' sia corrotto: {error}",
"backup_archive_cant_retrieve_info_json": "Impossibile caricare informazione per l'archivio '{archive}'... Impossibile scaricare info.json (oppure non è un json valido).",
"app_packaging_format_not_supported": "Quest'applicazione non può essere installata perché il formato non è supportato dalla vostra versione di YunoHost. Dovreste considerare di aggiornare il vostro sistema."
} }

View file

@ -1,3 +1,16 @@
{ {
"password_too_simple_1": "密码长度至少为8个字符" "password_too_simple_1": "密码长度至少为8个字符",
} "backup_created": "备份已创建",
"app_start_remove": "正在删除{app}……",
"admin_password_change_failed": "不能修改密码",
"admin_password_too_long": "请选择一个小于127个字符的密码",
"app_upgrade_failed": "不能升级{app:s}{error}",
"app_id_invalid": "无效 app ID",
"app_unknown": "未知应用",
"admin_password_changed": "管理密码已更改",
"aborting": "正在放弃。",
"admin_password": "管理密码",
"app_start_restore": "正在恢复{app}……",
"action_invalid": "无效操作 '{action:s}'",
"ask_lastname": "姓"
}

View file

@ -0,0 +1,206 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
import os
import sys
import moulinette
from moulinette import m18n
from moulinette.utils.log import configure_logging
from moulinette.interfaces.cli import colorize, get_locale
def is_installed():
return os.path.isfile('/etc/yunohost/installed')
def cli(debug, quiet, output_as, timeout, args, parser):
init_logging(interface="cli", debug=debug, quiet=quiet)
# Check that YunoHost is installed
if not is_installed():
check_command_is_valid_before_postinstall(args)
ret = moulinette.cli(
args,
output_as=output_as,
timeout=timeout,
top_parser=parser
)
sys.exit(ret)
def api(debug, host, port):
init_logging(interface="api", debug=debug)
def is_installed_api():
return {'installed': is_installed()}
# FIXME : someday, maybe find a way to disable route /postinstall if
# postinstall already done ...
ret = moulinette.api(
host=host,
port=port,
routes={('GET', '/installed'): is_installed_api},
)
sys.exit(ret)
def check_command_is_valid_before_postinstall(args):
allowed_if_not_postinstalled = ['tools postinstall',
'tools versions',
'backup list',
'backup restore',
'log display']
if (len(args) < 2 or (args[0] + ' ' + args[1] not in allowed_if_not_postinstalled)):
init_i18n()
print(colorize(m18n.g('error'), 'red') + " " + m18n.n('yunohost_not_installed'))
sys.exit(1)
def init(interface="cli", debug=False, quiet=False, logdir="/var/log/yunohost"):
"""
This is a small util function ONLY meant to be used to initialize a Yunohost
context when ran from tests or from scripts.
"""
init_logging(interface=interface, debug=debug, quiet=quiet, logdir=logdir)
init_i18n()
from moulinette.core import MoulinetteLock
lock = MoulinetteLock("yunohost", timeout=30)
lock.acquire()
return lock
def init_i18n():
# This should only be called when not willing to go through moulinette.cli
# or moulinette.api but still willing to call m18n.n/g...
m18n.load_namespace('yunohost')
m18n.set_locale(get_locale())
def init_logging(interface="cli",
debug=False,
quiet=False,
logdir="/var/log/yunohost"):
logfile = os.path.join(logdir, "yunohost-%s.log" % interface)
if not os.path.isdir(logdir):
os.makedirs(logdir, 0o750)
# ####################################################################### #
# Logging configuration for CLI (or any other interface than api...) #
# ####################################################################### #
if interface != "api":
configure_logging({
'version': 1,
'main_logger': "yunohost",
'disable_existing_loggers': True,
'formatters': {
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'tty': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': 'tty-debug' if debug else '',
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'precise',
'filename': logfile,
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if not quiet else ['file'],
'propagate': False,
},
'moulinette': {
'level': 'DEBUG',
'handlers': [],
'propagate': True,
},
'moulinette.interface': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if not quiet else ['file'],
'propagate': False,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['file', 'tty'] if debug else ['file'],
},
})
# ####################################################################### #
# Logging configuration for API #
# ####################################################################### #
else:
configure_logging({
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'console': {
'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'api': {
'level': 'DEBUG' if debug else 'INFO',
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {
'class': 'logging.handlers.WatchedFileHandler',
'formatter': 'precise',
'filename': logfile,
'filters': ['action'],
},
'console': {
'class': 'logging.StreamHandler',
'formatter': 'console',
'stream': 'ext://sys.stdout',
'filters': ['action'],
},
},
'loggers': {
'yunohost': {
'level': 'DEBUG',
'handlers': ['file', 'api'] + ['console'] if debug else [],
'propagate': False,
},
'moulinette': {
'level': 'DEBUG',
'handlers': [],
'propagate': True,
},
},
'root': {
'level': 'DEBUG',
'handlers': ['file'] + ['console'] if debug else [],
},
})

View file

@ -90,6 +90,8 @@ def app_catalog(full=False, with_categories=False):
"description": infos['manifest']['description'], "description": infos['manifest']['description'],
"level": infos["level"], "level": infos["level"],
} }
else:
infos["manifest"]["arguments"] = _set_default_ask_questions(infos["manifest"]["arguments"])
# Trim info for categories if not using --full # Trim info for categories if not using --full
for category in catalog["categories"]: for category in catalog["categories"]:
@ -109,7 +111,6 @@ def app_catalog(full=False, with_categories=False):
return {"apps": catalog["apps"], "categories": catalog["categories"]} return {"apps": catalog["apps"], "categories": catalog["categories"]}
# Old legacy function... # Old legacy function...
def app_fetchlist(): def app_fetchlist():
logger.warning("'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead") logger.warning("'yunohost app fetchlist' is deprecated. Please use 'yunohost tools update --apps' instead")
@ -169,6 +170,7 @@ def app_info(app, full=False):
return ret return ret
ret["manifest"] = local_manifest ret["manifest"] = local_manifest
ret["manifest"]["arguments"] = _set_default_ask_questions(ret["manifest"]["arguments"])
ret['settings'] = settings ret['settings'] = settings
absolute_app_name, _ = _parse_app_instance_name(app) absolute_app_name, _ = _parse_app_instance_name(app)
@ -182,11 +184,21 @@ def app_info(app, full=False):
def _app_upgradable(app_infos): def _app_upgradable(app_infos):
from packaging import version
# Determine upgradability # Determine upgradability
# In case there is neither update_time nor install_time, we assume the app can/has to be upgraded # In case there is neither update_time nor install_time, we assume the app can/has to be upgraded
if not app_infos.get("from_catalog", None): # Firstly use the version to know if an upgrade is available
app_is_in_catalog = bool(app_infos.get("from_catalog"))
installed_version = version.parse(app_infos.get("version", "0~ynh0"))
version_in_catalog = version.parse(app_infos.get("from_catalog", {}).get("manifest", {}).get("version", "0~ynh0"))
if app_is_in_catalog and '~ynh' in str(installed_version) and '~ynh' in str(version_in_catalog):
if installed_version < version_in_catalog:
return "yes"
if not app_is_in_catalog:
return "url_required" return "url_required"
if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"): if not app_infos["from_catalog"].get("lastUpdate") or not app_infos["from_catalog"].get("git"):
return "url_required" return "url_required"
@ -339,6 +351,7 @@ def app_change_url(operation_logger, app, domain, path):
env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_ID"] = app_id
env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NAME"] = app
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb)
env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
env_dict["YNH_APP_OLD_DOMAIN"] = old_domain env_dict["YNH_APP_OLD_DOMAIN"] = old_domain
env_dict["YNH_APP_OLD_PATH"] = old_path env_dict["YNH_APP_OLD_PATH"] = old_path
@ -402,7 +415,7 @@ def app_change_url(operation_logger, app, domain, path):
hook_callback('post_app_change_url', args=args_list, env=env_dict) hook_callback('post_app_change_url', args=args_list, env=env_dict)
def app_upgrade(app=[], url=None, file=None): def app_upgrade(app=[], url=None, file=None, force=False):
""" """
Upgrade app Upgrade app
@ -412,6 +425,7 @@ def app_upgrade(app=[], url=None, file=None):
url -- Git url to fetch for upgrade url -- Git url to fetch for upgrade
""" """
from packaging import version
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.permission import permission_sync_to_user, user_permission_list from yunohost.permission import permission_sync_to_user, user_permission_list
from yunohost.regenconf import manually_modified_files from yunohost.regenconf import manually_modified_files
@ -452,12 +466,41 @@ def app_upgrade(app=[], url=None, file=None):
elif app_dict["upgradable"] == "url_required": elif app_dict["upgradable"] == "url_required":
logger.warning(m18n.n('custom_app_url_required', app=app_instance_name)) logger.warning(m18n.n('custom_app_url_required', app=app_instance_name))
continue continue
elif app_dict["upgradable"] == "yes": elif app_dict["upgradable"] == "yes" or force:
manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name) manifest, extracted_app_folder = _fetch_app_from_git(app_instance_name)
else: else:
logger.success(m18n.n('app_already_up_to_date', app=app_instance_name)) logger.success(m18n.n('app_already_up_to_date', app=app_instance_name))
continue continue
# Manage upgrade type and avoid any upgrade if there is nothing to do
upgrade_type = "UNKNOWN"
# Get current_version and new version
app_new_version = version.parse(manifest.get("version", "?"))
app_current_version = version.parse(app_dict.get("version", "?"))
if "~ynh" in str(app_current_version) and "~ynh" in str(app_new_version):
if app_current_version >= app_new_version and not force:
# In case of upgrade from file or custom repository
# No new version available
logger.success(m18n.n('app_already_up_to_date', app=app_instance_name))
# Save update time
now = int(time.time())
app_setting(app_instance_name, 'update_time', now)
app_setting(app_instance_name, 'current_revision', manifest.get('remote', {}).get('revision', "?"))
continue
elif app_current_version > app_new_version:
upgrade_type = "DOWNGRADE_FORCED"
elif app_current_version == app_new_version:
upgrade_type = "UPGRADE_FORCED"
else:
app_current_version_upstream, app_current_version_pkg = str(app_current_version).split("~ynh")
app_new_version_upstream, app_new_version_pkg = str(app_new_version).split("~ynh")
if app_current_version_upstream == app_new_version_upstream:
upgrade_type = "UPGRADE_PACKAGE"
elif app_current_version_pkg == app_new_version_pkg:
upgrade_type = "UPGRADE_APP"
else:
upgrade_type = "UPGRADE_FULL"
# Check requirements # Check requirements
_check_manifest_requirements(manifest, app_instance_name=app_instance_name) _check_manifest_requirements(manifest, app_instance_name=app_instance_name)
_assert_system_is_sane_for_app(manifest, "pre") _assert_system_is_sane_for_app(manifest, "pre")
@ -477,6 +520,9 @@ def app_upgrade(app=[], url=None, file=None):
env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb)
env_dict["YNH_APP_LABEL"] = user_permission_list(full=True, ignore_system_perms=True, full_path=False)['permissions'][app_id+".main"]['label'] env_dict["YNH_APP_LABEL"] = user_permission_list(full=True, ignore_system_perms=True, full_path=False)['permissions'][app_id+".main"]['label']
env_dict["YNH_APP_UPGRADE_TYPE"] = upgrade_type
env_dict["YNH_APP_MANIFEST_VERSION"] = str(app_new_version)
env_dict["YNH_APP_CURRENT_VERSION"] = str(app_current_version)
# We'll check that the app didn't brutally edit some system configuration # We'll check that the app didn't brutally edit some system configuration
manually_modified_files_before_install = manually_modified_files() manually_modified_files_before_install = manually_modified_files()
@ -599,7 +645,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.log import OperationLogger from yunohost.log import OperationLogger
from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user, user_permission_update from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user
from yunohost.regenconf import manually_modified_files from yunohost.regenconf import manually_modified_files
# Fetch or extract sources # Fetch or extract sources
@ -709,6 +755,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict["YNH_APP_INSTANCE_NAME"] = app_instance_name
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
env_dict["YNH_APP_LABEL"] = label env_dict["YNH_APP_LABEL"] = label
env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
# Start register change on system # Start register change on system
operation_logger.extra.update({'env': env_dict}) operation_logger.extra.update({'env': env_dict})
@ -819,6 +866,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu
env_dict_remove["YNH_APP_ID"] = app_id env_dict_remove["YNH_APP_ID"] = app_id
env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name env_dict_remove["YNH_APP_INSTANCE_NAME"] = app_instance_name
env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number) env_dict_remove["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
# Execute remove script # Execute remove script
operation_logger_remove = OperationLogger('remove_on_failed_install', operation_logger_remove = OperationLogger('remove_on_failed_install',
@ -1016,6 +1064,7 @@ def app_remove(operation_logger, app):
env_dict["YNH_APP_ID"] = app_id env_dict["YNH_APP_ID"] = app_id
env_dict["YNH_APP_INSTANCE_NAME"] = app env_dict["YNH_APP_INSTANCE_NAME"] = app
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(app_instance_nb)
env_dict["YNH_APP_MANIFEST_VERSION"] = manifest.get("version", "?")
operation_logger.extra.update({'env': env_dict}) operation_logger.extra.update({'env': env_dict})
operation_logger.flush() operation_logger.flush()
@ -2095,12 +2144,62 @@ def _get_manifest_of_app(path):
manifest["arguments"]["install"] = install_arguments manifest["arguments"]["install"] = install_arguments
return manifest
elif os.path.exists(os.path.join(path, "manifest.json")): elif os.path.exists(os.path.join(path, "manifest.json")):
return read_json(os.path.join(path, "manifest.json")) manifest = read_json(os.path.join(path, "manifest.json"))
else: else:
raise YunohostError("There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." % path, raw_msg=True) raise YunohostError("There doesn't seem to be any manifest file in %s ... It looks like an app was not correctly installed/removed." % path, raw_msg=True)
manifest["arguments"] = _set_default_ask_questions(manifest["arguments"])
return manifest
def _set_default_ask_questions(arguments):
# arguments is something like
# { "install": [
# { "name": "domain",
# "type": "domain",
# ....
# },
# { "name": "path",
# "type": "path"
# ...
# },
# ...
# ],
# "upgrade": [ ... ]
# }
# We set a default for any question with these matching (type, name)
# type namei
# N.B. : this is only for install script ... should be reworked for other
# scripts if we supports args for other scripts in the future...
questions_with_default = [("domain", "domain"), # i18n: app_manifest_install_ask_domain
("path", "path"), # i18n: app_manifest_install_ask_path
("password", "password"), # i18n: app_manifest_install_ask_password
("user", "admin"), # i18n: app_manifest_install_ask_admin
("boolean", "is_public")] # i18n: app_manifest_install_ask_is_public
for script_name, arg_list in arguments.items():
# We only support questions for install so far, and for other
if script_name != "install":
continue
for arg in arg_list:
# Do not override 'ask' field if provided by app ?... Or shall we ?
# if "ask" in arg:
# continue
# If this arg corresponds to a question with default ask message...
if any((arg.get("type"), arg["name"]) == question for question in questions_with_default):
# The key is for example "app_manifest_install_ask_domain"
key = "app_manifest_%s_ask_%s" % (script_name, arg["name"])
arg["ask"] = m18n.n(key)
return arguments
def _get_git_last_commit_hash(repository, reference='HEAD'): def _get_git_last_commit_hash(repository, reference='HEAD'):
""" """
@ -2426,7 +2525,7 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions):
or config_panel.json/toml or config_panel.json/toml
""" """
from yunohost.domain import domain_list, _get_maindomain from yunohost.domain import domain_list, _get_maindomain
from yunohost.user import user_list from yunohost.user import user_list, user_info
parsed_answers_dict = OrderedDict() parsed_answers_dict = OrderedDict()
@ -2452,6 +2551,28 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions):
question_value = user_answers[question_name] question_value = user_answers[question_name]
else: else:
if 'ask' in question: if 'ask' in question:
if question_type == 'domain':
question_default = _get_maindomain()
msignals.display(m18n.n('domains_available'))
for domain in domain_list()['domains']:
msignals.display("- {}".format(domain))
elif question_type == 'user':
msignals.display(m18n.n('users_available'))
users = user_list()['users']
for user in users.keys():
msignals.display("- {}".format(user))
root_mail = "root@%s" % _get_maindomain()
for user in users.keys():
if root_mail in user_info(user)["mail-aliases"]:
question_default = user
break
elif question_type == 'password':
msignals.display(m18n.n('good_practices_about_user_password'))
# Retrieve proper ask string # Retrieve proper ask string
text_for_user_input_in_cli = _value_for_locale(question['ask']) text_for_user_input_in_cli = _value_for_locale(question['ask'])
@ -2461,30 +2582,15 @@ def _parse_args_in_yunohost_format(user_answers, argument_questions):
elif question_choices: elif question_choices:
text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question_choices)) text_for_user_input_in_cli += ' [{0}]'.format(' | '.join(question_choices))
if question_default is not None: if question_default is not None:
if question_type == 'boolean': if question_type == 'boolean':
text_for_user_input_in_cli += ' (default: {0})'.format("yes" if question_default == 1 else "no") text_for_user_input_in_cli += ' (default: {0})'.format("yes" if question_default == 1 else "no")
else: else:
text_for_user_input_in_cli += ' (default: {0})'.format(question_default) text_for_user_input_in_cli += ' (default: {0})'.format(question_default)
# Check for a password argument
is_password = True if question_type == 'password' else False is_password = True if question_type == 'password' else False
if question_type == 'domain':
question_default = _get_maindomain()
text_for_user_input_in_cli += ' (default: {0})'.format(question_default)
msignals.display(m18n.n('domains_available'))
for domain in domain_list()['domains']:
msignals.display("- {}".format(domain))
elif question_type == 'user':
msignals.display(m18n.n('users_available'))
for user in user_list()['users'].keys():
msignals.display("- {}".format(user))
elif question_type == 'password':
msignals.display(m18n.n('good_practices_about_user_password'))
try: try:
input_string = msignals.prompt(text_for_user_input_in_cli, is_password) input_string = msignals.prompt(text_for_user_input_in_cli, is_password)
except NotImplementedError: except NotImplementedError:

View file

@ -59,6 +59,7 @@ from yunohost.regenconf import regen_conf
from yunohost.log import OperationLogger from yunohost.log import OperationLogger
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from yunohost.utils.packages import ynh_packages_version from yunohost.utils.packages import ynh_packages_version
from yunohost.settings import settings_get
BACKUP_PATH = '/home/yunohost.backup' BACKUP_PATH = '/home/yunohost.backup'
ARCHIVES_PATH = '%s/archives' % BACKUP_PATH ARCHIVES_PATH = '%s/archives' % BACKUP_PATH
@ -225,8 +226,8 @@ class BackupManager():
backup_manager = BackupManager(name="mybackup", description="bkp things") backup_manager = BackupManager(name="mybackup", description="bkp things")
# Add backup method to apply # Add backup method to apply
backup_manager.add(BackupMethod.create('copy', backup_manager, '/mnt/local_fs')) backup_manager.add('copy', output_directory='/mnt/local_fs')
backup_manager.add(BackupMethod.create('tar', backup_manager, '/mnt/remote_fs')) backup_manager.add('tar', output_directory='/mnt/remote_fs')
# Define targets to be backuped # Define targets to be backuped
backup_manager.set_system_targets(["data"]) backup_manager.set_system_targets(["data"])
@ -239,7 +240,7 @@ class BackupManager():
backup_manager.backup() backup_manager.backup()
""" """
def __init__(self, name=None, description='', work_dir=None): def __init__(self, name=None, description='', methods=[], work_dir=None):
""" """
BackupManager constructor BackupManager constructor
@ -257,7 +258,6 @@ class BackupManager():
self.created_at = int(time.time()) self.created_at = int(time.time())
self.apps_return = {} self.apps_return = {}
self.system_return = {} self.system_return = {}
self.methods = []
self.paths_to_backup = [] self.paths_to_backup = []
self.size_details = { self.size_details = {
'system': {}, 'system': {},
@ -276,6 +276,9 @@ class BackupManager():
self.work_dir = os.path.join(BACKUP_PATH, 'tmp', name) self.work_dir = os.path.join(BACKUP_PATH, 'tmp', name)
self._init_work_dir() self._init_work_dir()
# Initialize backup methods
self.methods = [BackupMethod.create(method, self, repo=work_dir) for method in methods]
# #
# Misc helpers # # Misc helpers #
# #
@ -315,17 +318,6 @@ class BackupManager():
"""Initialize preparation directory """Initialize preparation directory
Ensure the working directory exists and is empty Ensure the working directory exists and is empty
exception:
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 -- (YunohostError)
Raised if the working directory isn't empty, is temporary and can't
be automaticcaly cleaned
(TODO) backup_cant_create_working_directory -- (YunohostError) Raised
if iyunohost can't create the working directory
""" """
# FIXME replace isdir by exists ? manage better the case where the path # FIXME replace isdir by exists ? manage better the case where the path
@ -509,10 +501,6 @@ class BackupManager():
files to backup files to backup
hooks/ -- restore scripts associated to system backup scripts are hooks/ -- restore scripts associated to system backup scripts are
copied here copied here
Exceptions:
"backup_nothings_done" -- (YunohostError) This exception is raised if
nothing has been listed.
""" """
self._collect_system_files() self._collect_system_files()
@ -679,10 +667,6 @@ class BackupManager():
Args: Args:
app -- (string) an app instance name (already installed) to backup app -- (string) an app instance name (already installed) to backup
Exceptions:
backup_app_failed -- Raised at the end if the app backup script
execution failed
""" """
from yunohost.permission import user_permission_list from yunohost.permission import user_permission_list
@ -741,18 +725,6 @@ class BackupManager():
# Actual backup archive creation / method management # # Actual backup archive creation / method management #
# #
def add(self, method):
"""
Add a backup method that will be applied after the files collection step
Args:
method -- (BackupMethod) A backup method. Currently, you can use those:
TarBackupMethod
CopyBackupMethod
CustomBackupMethod
"""
self.methods.append(method)
def backup(self): def backup(self):
"""Apply backup methods""" """Apply backup methods"""
@ -815,7 +787,7 @@ class RestoreManager():
""" """
RestoreManager allow to restore a past backup archive RestoreManager allow to restore a past backup archive
Currently it's a tar.gz file, but it could be another kind of archive Currently it's a tar file, but it could be another kind of archive
Public properties: Public properties:
info (getter)i # FIXME info (getter)i # FIXME
@ -841,14 +813,12 @@ class RestoreManager():
return restore_manager.result return restore_manager.result
""" """
def __init__(self, name, repo=None, method='tar'): def __init__(self, name, method='tar'):
""" """
RestoreManager constructor RestoreManager constructor
Args: Args:
name -- (string) Archive name name -- (string) Archive name
repo -- (string|None) Repository where is this archive, it could be a
path (default: /home/yunohost.backup/archives)
method -- (string) Method name to use to mount the archive method -- (string) Method name to use to mount the archive
""" """
# Retrieve and open the archive # Retrieve and open the archive
@ -876,9 +846,6 @@ class RestoreManager():
def _read_info_files(self): def _read_info_files(self):
""" """
Read the info file from inside an archive Read the info file from inside an archive
Exceptions:
backup_archive_cant_retrieve_info_json -- Raised if we can't read the info
""" """
# Retrieve backup info # Retrieve backup info
info_file = os.path.join(self.work_dir, "info.json") info_file = os.path.join(self.work_dir, "info.json")
@ -923,8 +890,6 @@ class RestoreManager():
""" """
from permission import permission_sync_to_user from permission import permission_sync_to_user
successfull_apps = self.targets.list("apps", include=["Success", "Warning"])
permission_sync_to_user() permission_sync_to_user()
if os.path.ismount(self.work_dir): if os.path.ismount(self.work_dir):
@ -1036,10 +1001,6 @@ class RestoreManager():
Use the mount method from the BackupMethod instance and read info about Use the mount method from the BackupMethod instance and read info about
this archive this archive
Exceptions:
restore_removing_tmp_dir_failed -- Raised if it's not possible to remove
the working directory
""" """
self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.name) self.work_dir = os.path.join(BACKUP_PATH, "tmp", self.name)
@ -1113,11 +1074,6 @@ class RestoreManager():
def assert_enough_free_space(self): def assert_enough_free_space(self):
""" """
Check available disk space Check available disk space
Exceptions:
restore_may_be_not_enough_disk_space -- Raised if there isn't enough
space to cover the security margin space
restore_not_enough_disk_space -- Raised if there isn't enough space
""" """
free_space = free_space_in_directory(BACKUP_PATH) free_space = free_space_in_directory(BACKUP_PATH)
@ -1299,11 +1255,6 @@ class RestoreManager():
Args: Args:
app_instance_name -- (string) The app name to restore (no app with this app_instance_name -- (string) The app name to restore (no app with this
name should be already install) name should be already install)
Exceptions:
restore_already_installed_app -- Raised if an app with this app instance
name already exists
restore_app_failed -- Raised if the restore bash script failed
""" """
from yunohost.user import user_group_list from yunohost.user import user_group_list
from yunohost.permission import permission_create, permission_delete, user_permission_list, permission_sync_to_user from yunohost.permission import permission_create, permission_delete, user_permission_list, permission_sync_to_user
@ -1509,7 +1460,7 @@ class BackupMethod(object):
TarBackupMethod TarBackupMethod
--------------- ---------------
This method compresses all files to backup in a .tar.gz archive. When This method compresses all files to backup in a .tar archive. When
restoring, it untars the required parts. restoring, it untars the required parts.
CustomBackupMethod CustomBackupMethod
@ -1534,7 +1485,24 @@ class BackupMethod(object):
method.mount() method.mount()
""" """
def __init__(self, manager, repo=None): @classmethod
def create(cls, method, manager, **kwargs):
"""
Factory method to create instance of BackupMethod
Args:
method -- (string) The method name of an existing BackupMethod. If the
name is unknown the CustomBackupMethod will be tried
*args -- Specific args for the method, could be the repo target by the
method
Return a BackupMethod instance
"""
known_methods = {c.method_name:c for c in BackupMethod.__subclasses__()}
backup_method = known_methods.get(method, CustomBackupMethod)
return backup_method(manager, method=method, **kwargs)
def __init__(self, manager, repo=None, **kwargs):
""" """
BackupMethod constructors BackupMethod constructors
@ -1619,10 +1587,6 @@ class BackupMethod(object):
def clean(self): def clean(self):
""" """
Umount sub directories of working dirextories and delete it if temporary Umount sub directories of working dirextories and delete it if temporary
Exceptions:
backup_cleaning_failed -- Raise if we were not able to unmount sub
directories of the working directories
""" """
if self.need_mount(): if self.need_mount():
if not _recursive_umount(self.work_dir): if not _recursive_umount(self.work_dir):
@ -1634,9 +1598,6 @@ class BackupMethod(object):
def _check_is_enough_free_space(self): def _check_is_enough_free_space(self):
""" """
Check free space in repository or output directory before to backup Check free space in repository or output directory before to backup
Exceptions:
not_enough_disk_space -- Raise if there isn't enough space.
""" """
# TODO How to do with distant repo or with deduplicated backup ? # TODO How to do with distant repo or with deduplicated backup ?
backup_size = self.manager.size backup_size = self.manager.size
@ -1658,9 +1619,6 @@ class BackupMethod(object):
The usage of binding could be strange for a user because the du -sb The usage of binding could be strange for a user because the du -sb
command will return that the working directory is big. command will return that the working directory is big.
Exceptions:
backup_unable_to_organize_files
""" """
paths_needed_to_be_copied = [] paths_needed_to_be_copied = []
for path in self.manager.paths_to_backup: for path in self.manager.paths_to_backup:
@ -1758,36 +1716,6 @@ class BackupMethod(object):
else: else:
shutil.copy(path['source'], dest) shutil.copy(path['source'], dest)
@classmethod
def create(cls, method, manager, *args):
"""
Factory method to create instance of BackupMethod
Args:
method -- (string) The method name of an existing BackupMethod. If the
name is unknown the CustomBackupMethod will be tried
... -- Specific args for the method, could be the repo target by the
method
Return a BackupMethod instance
"""
if not isinstance(method, basestring):
methods = []
for m in method:
methods.append(BackupMethod.create(m, manager, *args))
return methods
bm_class = {
'copy': CopyBackupMethod,
'tar': TarBackupMethod,
'borg': BorgBackupMethod
}
if method in ["copy", "tar", "borg"]:
return bm_class[method](manager, *args)
else:
return CustomBackupMethod(manager, method=method, *args)
class CopyBackupMethod(BackupMethod): class CopyBackupMethod(BackupMethod):
@ -1796,12 +1724,7 @@ class CopyBackupMethod(BackupMethod):
could be the inverse for restoring could be the inverse for restoring
""" """
def __init__(self, manager, repo=None): method_name = "copy"
super(CopyBackupMethod, self).__init__(manager, repo)
@property
def method_name(self):
return 'copy'
def backup(self): def backup(self):
""" Copy prepared files into a the repo """ """ Copy prepared files into a the repo """
@ -1827,10 +1750,6 @@ class CopyBackupMethod(BackupMethod):
def mount(self): def mount(self):
""" """
Mount the uncompress backup in readonly mode to the working directory Mount the uncompress backup in readonly mode to the working directory
Exceptions:
backup_no_uncompress_archive_dir -- Raised if the repo doesn't exists
backup_cant_mount_uncompress_archive -- Raised if the binding failed
""" """
# FIXME: This code is untested because there is no way to run it from # FIXME: This code is untested because there is no way to run it from
# the ynh cli # the ynh cli
@ -1857,21 +1776,18 @@ class CopyBackupMethod(BackupMethod):
class TarBackupMethod(BackupMethod): class TarBackupMethod(BackupMethod):
""" method_name = "tar"
This class compress all files to backup in archive.
"""
def __init__(self, manager, repo=None):
super(TarBackupMethod, self).__init__(manager, repo)
@property
def method_name(self):
return 'tar'
@property @property
def _archive_file(self): def _archive_file(self):
"""Return the compress archive path"""
return os.path.join(self.repo, self.name + '.tar.gz') if isinstance(self.manager, BackupManager) and settings_get("backup.compress_tar_archives"):
return os.path.join(self.repo, self.name + '.tar.gz')
f = os.path.join(self.repo, self.name + '.tar')
if os.path.exists(f + ".gz"):
f += ".gz"
return f
def backup(self): def backup(self):
""" """
@ -1879,11 +1795,6 @@ class TarBackupMethod(BackupMethod):
It adds the info.json in /home/yunohost.backup/archives and if the It adds the info.json in /home/yunohost.backup/archives and if the
compress archive isn't located here, add a symlink to the archive to. compress archive isn't located here, add a symlink to the archive to.
Exceptions:
backup_archive_open_failed -- Raised if we can't open the archive
backup_creation_failed -- Raised if we can't write in the
compress archive
""" """
if not os.path.exists(self.repo): if not os.path.exists(self.repo):
@ -1894,7 +1805,7 @@ class TarBackupMethod(BackupMethod):
# Open archive file for writing # Open archive file for writing
try: try:
tar = tarfile.open(self._archive_file, "w:gz") tar = tarfile.open(self._archive_file, "w:gz" if self._archive_file.endswith(".gz") else "w")
except: except:
logger.debug("unable to open '%s' for writing", logger.debug("unable to open '%s' for writing",
self._archive_file, exc_info=1) self._archive_file, exc_info=1)
@ -1918,26 +1829,20 @@ class TarBackupMethod(BackupMethod):
# If backuped to a non-default location, keep a symlink of the archive # If backuped to a non-default location, keep a symlink of the archive
# to that location # to that location
link = os.path.join(ARCHIVES_PATH, self.name + '.tar.gz') link = os.path.join(ARCHIVES_PATH, self.name + '.tar')
if not os.path.isfile(link): if not os.path.isfile(link):
os.symlink(self._archive_file, link) os.symlink(self._archive_file, link)
def mount(self): def mount(self):
""" """
Mount the archive. We avoid copy to be able to restore on system without Mount the archive. We avoid intermediate copies to be able to restore on system with low free space.
too many space.
Exceptions:
backup_archive_open_failed -- Raised if the archive can't be open
backup_archive_corrupted -- Raised if the archive appears corrupted
backup_archive_cant_retrieve_info_json -- If the info.json file can't be retrieved
""" """
super(TarBackupMethod, self).mount() super(TarBackupMethod, self).mount()
# Mount the tarball # Mount the tarball
logger.debug(m18n.n("restore_extracting")) logger.debug(m18n.n("restore_extracting"))
try: try:
tar = tarfile.open(self._archive_file, "r:gz") tar = tarfile.open(self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r")
except: except:
logger.debug("cannot open backup archive '%s'", logger.debug("cannot open backup archive '%s'",
self._archive_file, exc_info=1) self._archive_file, exc_info=1)
@ -2006,7 +1911,7 @@ class TarBackupMethod(BackupMethod):
tar.close() tar.close()
def copy(self, file, target): def copy(self, file, target):
tar = tarfile.open(self._archive_file, "r:gz") tar = tarfile.open(self._archive_file, "r:gz" if self._archive_file.endswith(".gz") else "r")
file_to_extract = tar.getmember(file) file_to_extract = tar.getmember(file)
# Remove the path # Remove the path
file_to_extract.name = os.path.basename(file_to_extract.name) file_to_extract.name = os.path.basename(file_to_extract.name)
@ -2014,26 +1919,6 @@ class TarBackupMethod(BackupMethod):
tar.close() tar.close()
class BorgBackupMethod(BackupMethod):
@property
def method_name(self):
return 'borg'
def backup(self):
""" Backup prepared files with borg """
super(CopyBackupMethod, self).backup()
# TODO run borg create command
raise YunohostError('backup_borg_not_implemented')
def mount(self, mnt_path):
raise YunohostError('backup_borg_not_implemented')
def copy(self, file, target):
raise YunohostError('backup_borg_not_implemented')
class CustomBackupMethod(BackupMethod): class CustomBackupMethod(BackupMethod):
""" """
@ -2042,21 +1927,16 @@ class CustomBackupMethod(BackupMethod):
/etc/yunohost/hooks.d/backup_method/ /etc/yunohost/hooks.d/backup_method/
""" """
method_name = "custom"
def __init__(self, manager, repo=None, method=None, **kwargs): def __init__(self, manager, repo=None, method=None, **kwargs):
super(CustomBackupMethod, self).__init__(manager, repo) super(CustomBackupMethod, self).__init__(manager, repo)
self.args = kwargs self.args = kwargs
self.method = method self.method = method
self._need_mount = None self._need_mount = None
@property
def method_name(self):
return 'borg'
def need_mount(self): def need_mount(self):
"""Call the backup_method hook to know if we need to organize files """Call the backup_method hook to know if we need to organize files
Exceptions:
backup_custom_need_mount_error -- Raised if the hook failed
""" """
if self._need_mount is not None: if self._need_mount is not None:
return self._need_mount return self._need_mount
@ -2071,9 +1951,6 @@ class CustomBackupMethod(BackupMethod):
def backup(self): def backup(self):
""" """
Launch a custom script to backup Launch a custom script to backup
Exceptions:
backup_custom_backup_error -- Raised if the custom script failed
""" """
ret = hook_callback('backup_method', [self.method], ret = hook_callback('backup_method', [self.method],
@ -2087,9 +1964,6 @@ class CustomBackupMethod(BackupMethod):
def mount(self): def mount(self):
""" """
Launch a custom script to mount the custom archive Launch a custom script to mount the custom archive
Exceptions:
backup_custom_mount_error -- Raised if the custom script failed
""" """
super(CustomBackupMethod, self).mount() super(CustomBackupMethod, self).mount()
ret = hook_callback('backup_method', [self.method], ret = hook_callback('backup_method', [self.method],
@ -2111,7 +1985,7 @@ class CustomBackupMethod(BackupMethod):
# #
def backup_create(name=None, description=None, methods=[], def backup_create(name=None, description=None, methods=[],
output_directory=None, no_compress=False, output_directory=None,
system=[], apps=[]): system=[], apps=[]):
""" """
Create a backup local archive Create a backup local archive
@ -2121,7 +1995,6 @@ def backup_create(name=None, description=None, methods=[],
description -- Short description of the backup description -- Short description of the backup
method -- Method of backup to use method -- Method of backup to use
output_directory -- Output directory for the backup output_directory -- Output directory for the backup
no_compress -- Do not create an archive file
system -- List of system elements to backup system -- List of system elements to backup
apps -- List of application names to backup apps -- List of application names to backup
""" """
@ -2136,6 +2009,10 @@ def backup_create(name=None, description=None, methods=[],
if name and name in backup_list()['archives']: if name and name in backup_list()['archives']:
raise YunohostError('backup_archive_name_exists') raise YunohostError('backup_archive_name_exists')
# By default we backup using the tar method
if not methods:
methods = ['tar']
# Validate output_directory option # Validate output_directory option
if output_directory: if output_directory:
output_directory = os.path.abspath(output_directory) output_directory = os.path.abspath(output_directory)
@ -2146,20 +2023,12 @@ def backup_create(name=None, description=None, methods=[],
output_directory): output_directory):
raise YunohostError('backup_output_directory_forbidden') raise YunohostError('backup_output_directory_forbidden')
if "copy" in methods:
if not output_directory:
raise YunohostError('backup_output_directory_required')
# Check that output directory is empty # Check that output directory is empty
if os.path.isdir(output_directory) and no_compress and \ elif os.path.isdir(output_directory) and os.listdir(output_directory):
os.listdir(output_directory):
raise YunohostError('backup_output_directory_not_empty') raise YunohostError('backup_output_directory_not_empty')
elif no_compress:
raise YunohostError('backup_output_directory_required')
# Define methods (retro-compat)
if not methods:
if no_compress:
methods = ['copy']
else:
methods = ['tar'] # In future, borg will be the default actions
# If no --system or --apps given, backup everything # If no --system or --apps given, backup everything
if system is None and apps is None: if system is None and apps is None:
@ -2173,23 +2042,12 @@ def backup_create(name=None, description=None, methods=[],
# Create yunohost archives directory if it does not exists # Create yunohost archives directory if it does not exists
_create_archive_dir() _create_archive_dir()
# Prepare files to backup # Initialize backup manager
if no_compress:
backup_manager = BackupManager(name, description,
work_dir=output_directory)
else:
backup_manager = BackupManager(name, description)
# Add backup methods backup_manager = BackupManager(name, description, methods=methods, work_dir=output_directory)
if output_directory:
methods = BackupMethod.create(methods, backup_manager, output_directory)
else:
methods = BackupMethod.create(methods, backup_manager)
for method in methods:
backup_manager.add(method)
# Add backup targets (system and apps) # Add backup targets (system and apps)
backup_manager.set_system_targets(system) backup_manager.set_system_targets(system)
backup_manager.set_apps_targets(apps) backup_manager.set_apps_targets(apps)
@ -2293,9 +2151,17 @@ def backup_list(with_info=False, human_readable=False):
""" """
# Get local archives sorted according to last modification time # Get local archives sorted according to last modification time
archives = sorted(glob("%s/*.tar.gz" % ARCHIVES_PATH), key=lambda x: os.path.getctime(x)) # (we do a realpath() to resolve symlinks)
archives = glob("%s/*.tar.gz" % ARCHIVES_PATH) + glob("%s/*.tar" % ARCHIVES_PATH)
archives = set([os.path.realpath(archive) for archive in archives])
archives = sorted(archives, key=lambda x: os.path.getctime(x))
# Extract only filename without the extension # Extract only filename without the extension
archives = [os.path.basename(f)[:-len(".tar.gz")] for f in archives] def remove_extension(f):
if f.endswith(".tar.gz"):
return os.path.basename(f)[:-len(".tar.gz")]
else:
return os.path.basename(f)[:-len(".tar")]
archives = [remove_extension(f) for f in archives]
if with_info: if with_info:
d = OrderedDict() d = OrderedDict()
@ -2323,11 +2189,13 @@ def backup_info(name, with_details=False, human_readable=False):
human_readable -- Print sizes in human readable format human_readable -- Print sizes in human readable format
""" """
archive_file = '%s/%s.tar.gz' % (ARCHIVES_PATH, name) archive_file = '%s/%s.tar' % (ARCHIVES_PATH, name)
# Check file exist (even if it's a broken symlink) # Check file exist (even if it's a broken symlink)
if not os.path.lexists(archive_file): if not os.path.lexists(archive_file):
raise YunohostError('backup_archive_name_unknown', name=name) archive_file += ".gz"
if not os.path.lexists(archive_file):
raise YunohostError('backup_archive_name_unknown', name=name)
# If symlink, retrieve the real path # If symlink, retrieve the real path
if os.path.islink(archive_file): if os.path.islink(archive_file):
@ -2341,7 +2209,7 @@ def backup_info(name, with_details=False, human_readable=False):
info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name)
if not os.path.exists(info_file): if not os.path.exists(info_file):
tar = tarfile.open(archive_file, "r:gz") tar = tarfile.open(archive_file, "r:gz" if archive_file.endswith(".gz") else "r")
info_dir = info_file + '.d' info_dir = info_file + '.d'
try: try:
@ -2377,7 +2245,7 @@ def backup_info(name, with_details=False, human_readable=False):
# Retrieve backup size # Retrieve backup size
size = info.get('size', 0) size = info.get('size', 0)
if not size: if not size:
tar = tarfile.open(archive_file, "r:gz") tar = tarfile.open(archive_file, "r:gz" if archive_file.endswith(".gz") else "r")
size = reduce(lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y), size = reduce(lambda x, y: getattr(x, 'size', x) + getattr(y, 'size', y),
tar.getmembers()) tar.getmembers())
tar.close() tar.close()
@ -2437,7 +2305,9 @@ def backup_delete(name):
hook_callback('pre_backup_delete', args=[name]) hook_callback('pre_backup_delete', args=[name])
archive_file = '%s/%s.tar.gz' % (ARCHIVES_PATH, name) archive_file = '%s/%s.tar' % (ARCHIVES_PATH, name)
if os.path.exists(archive_file + ".gz"):
archive_file += '.gz'
info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name) info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name)
files_to_delete = [archive_file, info_file] files_to_delete = [archive_file, info_file]

View file

@ -27,7 +27,6 @@ import sys
import shutil import shutil
import pwd import pwd
import grp import grp
import smtplib
import subprocess import subprocess
import glob import glob
@ -467,6 +466,7 @@ Subject: %s
%s %s
""" % (from_, to_, subject_, text) """ % (from_, to_, subject_, text)
import smtplib
smtp = smtplib.SMTP("localhost") smtp = smtplib.SMTP("localhost")
smtp.sendmail(from_, [to_], message) smtp.sendmail(from_, [to_], message)
smtp.quit() smtp.quit()

View file

@ -4,7 +4,7 @@ from shutil import copy2
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.app import _is_installed, _get_app_settings, _set_app_settings, _patch_legacy_php_versions_in_settings from yunohost.app import _is_installed, _patch_legacy_php_versions_in_settings
from yunohost.tools import Migration from yunohost.tools import Migration
from yunohost.service import _run_service_command from yunohost.service import _run_service_command

View file

@ -28,7 +28,7 @@ class MyMigration(Migration):
# Make sure there's a 9.6 cluster # Make sure there's a 9.6 cluster
try: try:
self.runcmd("pg_lsclusters | grep -q '^9.6 '") self.runcmd("pg_lsclusters | grep -q '^9.6 '")
except Exception as e: except Exception:
logger.warning("It looks like there's not active 9.6 cluster, so probably don't need to run this migration") logger.warning("It looks like there's not active 9.6 cluster, so probably don't need to run this migration")
return return
@ -36,7 +36,7 @@ class MyMigration(Migration):
raise YunohostError("migration_0017_not_enough_space", path="/var/lib/postgresql/") raise YunohostError("migration_0017_not_enough_space", path="/var/lib/postgresql/")
self.runcmd("systemctl stop postgresql") self.runcmd("systemctl stop postgresql")
self.runcmd("pg_dropcluster --stop 11 main || true") # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster) self.runcmd("pg_dropcluster --stop 11 main || true") # We do not trigger an exception if the command fails because that probably means cluster 11 doesn't exists, which is fine because it's created during the pg_upgradecluster)
self.runcmd("pg_upgradecluster -m upgrade 9.6 main") self.runcmd("pg_upgradecluster -m upgrade 9.6 main")
self.runcmd("pg_dropcluster --stop 9.6 main") self.runcmd("pg_dropcluster --stop 9.6 main")
self.runcmd("systemctl start postgresql") self.runcmd("systemctl start postgresql")
@ -63,4 +63,3 @@ class MyMigration(Migration):
out = out.strip().split("\n") out = out.strip().split("\n")
return (returncode, out, err) return (returncode, out, err)

View file

@ -106,4 +106,3 @@ class MyMigration(Migration):
out = out.strip().split("\n") out = out.strip().split("\n")
return (returncode, out, err) return (returncode, out, err)

View file

@ -27,7 +27,6 @@
import re import re
import os import os
import time import time
import smtplib
from moulinette import m18n, msettings from moulinette import m18n, msettings
from moulinette.utils import log from moulinette.utils import log
@ -177,7 +176,7 @@ def diagnosis_run(categories=[], force=False, except_if_never_ran_yet=False, ema
code, report = hook_exec(path, args={"force": force}, env=None) code, report = hook_exec(path, args={"force": force}, env=None)
except Exception: except Exception:
import traceback import traceback
logger.error(m18n.n("diagnosis_failed_for_category", category=category, error='\n'+traceback.format_exc())) logger.error(m18n.n("diagnosis_failed_for_category", category=category, error='\n' + traceback.format_exc()))
else: else:
diagnosed_categories.append(category) diagnosed_categories.append(category)
if report != {}: if report != {}:
@ -404,11 +403,11 @@ class Diagnoser():
Diagnoser.i18n(new_report) Diagnoser.i18n(new_report)
add_ignore_flag_to_issues(new_report) add_ignore_flag_to_issues(new_report)
errors = [item for item in new_report["items"] if item["status"] == "ERROR" and not item["ignored"]] errors = [item for item in new_report["items"] if item["status"] == "ERROR" and not item["ignored"]]
warnings = [item for item in new_report["items"] if item["status"] == "WARNING" and not item["ignored"]] warnings = [item for item in new_report["items"] if item["status"] == "WARNING" and not item["ignored"]]
errors_ignored = [item for item in new_report["items"] if item["status"] == "ERROR" and item["ignored"]] errors_ignored = [item for item in new_report["items"] if item["status"] == "ERROR" and item["ignored"]]
warning_ignored = [item for item in new_report["items"] if item["status"] == "WARNING" and item["ignored"]] warning_ignored = [item for item in new_report["items"] if item["status"] == "WARNING" and item["ignored"]]
ignored_msg = " " + m18n.n("diagnosis_ignored_issues", nb_ignored=len(errors_ignored+warning_ignored)) if errors_ignored or warning_ignored else "" ignored_msg = " " + m18n.n("diagnosis_ignored_issues", nb_ignored=len(errors_ignored + warning_ignored)) if errors_ignored or warning_ignored else ""
if errors and warnings: if errors and warnings:
logger.error(m18n.n("diagnosis_found_errors_and_warnings", errors=len(errors), warnings=len(warnings), category=new_report["description"]) + ignored_msg) logger.error(m18n.n("diagnosis_found_errors_and_warnings", errors=len(errors), warnings=len(warnings), category=new_report["description"]) + ignored_msg)
@ -478,6 +477,7 @@ class Diagnoser():
meta_data.update(item.get("data", {})) meta_data.update(item.get("data", {}))
html_tags = re.compile(r'<[^>]+>') html_tags = re.compile(r'<[^>]+>')
def m18n_(info): def m18n_(info):
if not isinstance(info, tuple) and not isinstance(info, list): if not isinstance(info, tuple) and not isinstance(info, list):
info = (info, {}) info = (info, {})
@ -486,7 +486,7 @@ class Diagnoser():
# In cli, we remove the html tags # In cli, we remove the html tags
if msettings.get("interface") != "api" or force_remove_html_tags: if msettings.get("interface") != "api" or force_remove_html_tags:
s = s.replace("<cmd>", "'").replace("</cmd>", "'") s = s.replace("<cmd>", "'").replace("</cmd>", "'")
s = html_tags.sub('', s.replace("<br>","\n")) s = html_tags.sub('', s.replace("<br>", "\n"))
else: else:
s = s.replace("<cmd>", "<code class='cmd'>").replace("</cmd>", "</code>") s = s.replace("<cmd>", "<code class='cmd'>").replace("</cmd>", "</code>")
# Make it so that links open in new tabs # Make it so that links open in new tabs
@ -583,6 +583,7 @@ Subject: %s
%s %s
""" % (from_, to_, subject_, disclaimer, content) """ % (from_, to_, subject_, disclaimer, content)
import smtplib
smtp = smtplib.SMTP("localhost") smtp = smtplib.SMTP("localhost")
smtp.sendmail(from_, [to_], message) smtp.sendmail(from_, [to_], message)
smtp.quit() smtp.quit()

View file

@ -89,7 +89,7 @@ def domain_add(operation_logger, domain, dyndns=False):
raise YunohostError('domain_exists') raise YunohostError('domain_exists')
operation_logger.start() operation_logger.start()
# Lower domain to avoid some edge cases issues # Lower domain to avoid some edge cases issues
# See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765 # See: https://forum.yunohost.org/t/invalid-domain-causes-diagnosis-web-to-fail-fr-on-demand/11765
domain = domain.lower() domain = domain.lower()
@ -708,17 +708,17 @@ def _get_DKIM(domain):
if is_legacy_format: if is_legacy_format:
dkim = re.match(( dkim = re.match((
r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+' r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+'
'[^"]*"v=(?P<v>[^";]+);' '[^"]*"v=(?P<v>[^";]+);'
'[\s"]*k=(?P<k>[^";]+);' r'[\s"]*k=(?P<k>[^";]+);'
'[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S '[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S
) )
else: else:
dkim = re.match(( dkim = re.match((
r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+' r'^(?P<host>[a-z_\-\.]+)[\s]+([0-9]+[\s]+)?IN[\s]+TXT[\s]+'
'[^"]*"v=(?P<v>[^";]+);' '[^"]*"v=(?P<v>[^";]+);'
'[\s"]*h=(?P<h>[^";]+);' r'[\s"]*h=(?P<h>[^";]+);'
'[\s"]*k=(?P<k>[^";]+);' r'[\s"]*k=(?P<k>[^";]+);'
'[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S '[\s"]*p=(?P<p>[^";]+)'), dkim_content, re.M | re.S
) )
if not dkim: if not dkim:

View file

@ -24,7 +24,6 @@
Manage firewall rules Manage firewall rules
""" """
import os import os
import sys
import yaml import yaml
import miniupnpc import miniupnpc

View file

@ -196,8 +196,7 @@ def hook_list(action, list_by='name', show_info=False):
else: else:
_append_folder(result, HOOK_FOLDER) _append_folder(result, HOOK_FOLDER)
except OSError: except OSError:
logger.debug("No default hook for action '%s' in %s", pass
action, HOOK_FOLDER)
try: try:
# Append custom hooks # Append custom hooks
@ -207,8 +206,7 @@ def hook_list(action, list_by='name', show_info=False):
else: else:
_append_folder(result, CUSTOM_HOOK_FOLDER) _append_folder(result, CUSTOM_HOOK_FOLDER)
except OSError: except OSError:
logger.debug("No custom hook for action '%s' in %s", pass
action, CUSTOM_HOOK_FOLDER)
return {'hooks': result} return {'hooks': result}
@ -270,9 +268,9 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
# Validate callbacks # Validate callbacks
if not callable(pre_callback): if not callable(pre_callback):
pre_callback = lambda name, priority, path, args: args def pre_callback(name, priority, path, args): return args
if not callable(post_callback): if not callable(post_callback):
post_callback = lambda name, priority, path, succeed: None def post_callback(name, priority, path, succeed): return None
# Iterate over hooks and execute them # Iterate over hooks and execute them
for priority in sorted(hooks_dict): for priority in sorted(hooks_dict):
@ -283,7 +281,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
hook_args = pre_callback(name=name, priority=priority, hook_args = pre_callback(name=name, priority=priority,
path=path, args=args) path=path, args=args)
hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env, hook_return = hook_exec(path, args=hook_args, chdir=chdir, env=env,
no_trace=no_trace, raise_on_error=True)[1] no_trace=no_trace, raise_on_error=True)[1]
except YunohostError as e: except YunohostError as e:
state = 'failed' state = 'failed'
hook_return = {} hook_return = {}
@ -293,9 +291,9 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
else: else:
post_callback(name=name, priority=priority, path=path, post_callback(name=name, priority=priority, path=path,
succeed=True) succeed=True)
if not name in result: if name not in result:
result[name] = {} result[name] = {}
result[name][path] = {'state' : state, 'stdreturn' : hook_return } result[name][path] = {'state': state, 'stdreturn': hook_return}
return result return result
@ -446,17 +444,17 @@ def _hook_exec_python(path, args, env, loggers):
dir_ = os.path.dirname(path) dir_ = os.path.dirname(path)
name = os.path.splitext(os.path.basename(path))[0] name = os.path.splitext(os.path.basename(path))[0]
if not dir_ in sys.path: if dir_ not in sys.path:
sys.path = [dir_] + sys.path sys.path = [dir_] + sys.path
module = import_module(name) module = import_module(name)
ret = module.main(args, env, loggers) ret = module.main(args, env, loggers)
# # Assert that the return is a (int, dict) tuple # # Assert that the return is a (int, dict) tuple
assert isinstance(ret, tuple) \ assert isinstance(ret, tuple) \
and len(ret) == 2 \ and len(ret) == 2 \
and isinstance(ret[0],int) \ and isinstance(ret[0], int) \
and isinstance(ret[1],dict), \ and isinstance(ret[1], dict), \
"Module %s did not return a (int, dict) tuple !" % module "Module %s did not return a (int, dict) tuple !" % module
return ret return ret

View file

@ -27,9 +27,10 @@
import os import os
import re import re
import yaml import yaml
import collections import glob
import psutil
from datetime import datetime from datetime import datetime, timedelta
from logging import FileHandler, getLogger, Formatter from logging import FileHandler, getLogger, Formatter
from moulinette import m18n, msettings from moulinette import m18n, msettings
@ -41,8 +42,6 @@ from moulinette.utils.filesystem import read_file, read_yaml
CATEGORIES_PATH = '/var/log/yunohost/categories/' CATEGORIES_PATH = '/var/log/yunohost/categories/'
OPERATIONS_PATH = '/var/log/yunohost/categories/operation/' OPERATIONS_PATH = '/var/log/yunohost/categories/operation/'
#CATEGORIES = ['operation', 'history', 'package', 'system', 'access', 'service', 'app']
CATEGORIES = ['operation']
METADATA_FILE_EXT = '.yml' METADATA_FILE_EXT = '.yml'
LOG_FILE_EXT = '.log' LOG_FILE_EXT = '.log'
RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user'] RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user']
@ -50,80 +49,89 @@ RELATED_CATEGORIES = ['app', 'domain', 'group', 'service', 'user']
logger = getActionLogger('yunohost.log') logger = getActionLogger('yunohost.log')
def log_list(category=[], limit=None, with_details=False): def log_list(limit=None, with_details=False, with_suboperations=False):
""" """
List available logs List available logs
Keyword argument: Keyword argument:
limit -- Maximum number of logs limit -- Maximum number of logs
with_details -- Include details (e.g. if the operation was a success). Likely to increase the command time as it needs to open and parse the metadata file for each log... So try to use this in combination with --limit. with_details -- Include details (e.g. if the operation was a success).
Likely to increase the command time as it needs to open and parse the
metadata file for each log...
with_suboperations -- Include operations that are not the "main"
operation but are sub-operations triggered by another ongoing operation
... (e.g. initializing groups/permissions when installing an app)
""" """
categories = category operations = {}
is_api = msettings.get('interface') == 'api'
# In cli we just display `operation` logs by default logs = filter(lambda x: x.endswith(METADATA_FILE_EXT),
if not categories: os.listdir(OPERATIONS_PATH))
categories = CATEGORIES logs = list(reversed(sorted(logs)))
result = collections.OrderedDict() if limit is not None:
for category in categories: logs = logs[:limit]
result[category] = []
category_path = os.path.join(CATEGORIES_PATH, category) for log in logs:
if not os.path.exists(category_path):
logger.debug(m18n.n('log_category_404', category=category)) base_filename = log[:-len(METADATA_FILE_EXT)]
md_path = os.path.join(OPERATIONS_PATH, log)
entry = {
"name": base_filename,
"path": md_path,
"description": _get_description_from_name(base_filename),
}
try:
entry["started_at"] = _get_datetime_from_name(base_filename)
except ValueError:
pass
try:
metadata = read_yaml(md_path)
except Exception as e:
# If we can't read the yaml for some reason, report an error and ignore this entry...
logger.error(m18n.n('log_corrupted_md_file', md_file=md_path, error=e))
continue continue
logs = filter(lambda x: x.endswith(METADATA_FILE_EXT), if with_details:
os.listdir(category_path)) entry["success"] = metadata.get("success", "?") if metadata else "?"
logs = list(reversed(sorted(logs))) entry["parent"] = metadata.get("parent")
if limit is not None: if with_suboperations:
logs = logs[:limit] entry["parent"] = metadata.get("parent")
entry["suboperations"] = []
elif metadata.get("parent") is not None:
continue
for log in logs: operations[base_filename] = entry
base_filename = log[:-len(METADATA_FILE_EXT)] # When displaying suboperations, we build a tree-like structure where
md_filename = log # "suboperations" is a list of suboperations (each of them may also have a list of
md_path = os.path.join(category_path, md_filename) # "suboperations" suboperations etc...
if with_suboperations:
log = base_filename.split("-") suboperations = [o for o in operations.values() if o["parent"] is not None]
for suboperation in suboperations:
entry = { parent = operations.get(suboperation["parent"])
"name": base_filename, if not parent:
"path": md_path, continue
} parent["suboperations"].append(suboperation)
entry["description"] = _get_description_from_name(base_filename) operations = [o for o in operations.values() if o["parent"] is None]
try: else:
log_datetime = datetime.strptime(" ".join(log[:2]), operations = [o for o in operations.values()]
"%Y%m%d %H%M%S")
except ValueError:
pass
else:
entry["started_at"] = log_datetime
if with_details:
try:
metadata = read_yaml(md_path)
except Exception as e:
# If we can't read the yaml for some reason, report an error and ignore this entry...
logger.error(m18n.n('log_corrupted_md_file', md_file=md_path, error=e))
continue
entry["success"] = metadata.get("success", "?") if metadata else "?"
result[category].append(entry)
operations = list(reversed(sorted(operations, key=lambda o: o["name"])))
# Reverse the order of log when in cli, more comfortable to read (avoid # Reverse the order of log when in cli, more comfortable to read (avoid
# unecessary scrolling) # unecessary scrolling)
is_api = msettings.get('interface') == 'api'
if not is_api: if not is_api:
for category in result: operations = list(reversed(operations))
result[category] = list(reversed(result[category]))
return result return {"operation": operations}
def log_display(path, number=None, share=False, filter_irrelevant=False): def log_display(path, number=None, share=False, filter_irrelevant=False, with_suboperations=False):
""" """
Display a log file enriched with metadata if any. Display a log file enriched with metadata if any.
@ -163,10 +171,7 @@ def log_display(path, number=None, share=False, filter_irrelevant=False):
abs_path = path abs_path = path
log_path = None log_path = None
if not path.startswith('/'): if not path.startswith('/'):
for category in CATEGORIES: abs_path = os.path.join(OPERATIONS_PATH, path)
abs_path = os.path.join(CATEGORIES_PATH, category, path)
if os.path.exists(abs_path) or os.path.exists(abs_path + METADATA_FILE_EXT):
break
if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT): if os.path.exists(abs_path) and not path.endswith(METADATA_FILE_EXT):
log_path = abs_path log_path = abs_path
@ -225,14 +230,54 @@ def log_display(path, number=None, share=False, filter_irrelevant=False):
if 'log_path' in metadata: if 'log_path' in metadata:
log_path = metadata['log_path'] log_path = metadata['log_path']
if with_suboperations:
def suboperations():
try:
log_start = _get_datetime_from_name(base_filename)
except ValueError:
return
for filename in os.listdir(OPERATIONS_PATH):
if not filename.endswith(METADATA_FILE_EXT):
continue
# We first retrict search to a ~48h time window to limit the number
# of .yml we look into
try:
date = _get_datetime_from_name(base_filename)
except ValueError:
continue
if (date < log_start) or (date > log_start + timedelta(hours=48)):
continue
try:
submetadata = read_yaml(os.path.join(OPERATIONS_PATH, filename))
except Exception:
continue
if submetadata.get("parent") == base_filename:
yield {
"name": filename[:-len(METADATA_FILE_EXT)],
"description": _get_description_from_name(filename[:-len(METADATA_FILE_EXT)]),
"success": submetadata.get("success", "?")
}
metadata["suboperations"] = list(suboperations())
# Display logs if exist # Display logs if exist
if os.path.exists(log_path): if os.path.exists(log_path):
from yunohost.service import _tail from yunohost.service import _tail
if number: if number and filters:
logs = _tail(log_path, int(number * 4))
elif number:
logs = _tail(log_path, int(number)) logs = _tail(log_path, int(number))
else: else:
logs = read_file(log_path) logs = read_file(log_path)
logs = _filter_lines(logs, filters) logs = _filter_lines(logs, filters)
if number:
logs = logs[-number:]
infos['log_path'] = log_path infos['log_path'] = log_path
infos['logs'] = logs infos['logs'] = logs
@ -360,6 +405,8 @@ class OperationLogger(object):
This class record logs and metadata like context or start time/end time. This class record logs and metadata like context or start time/end time.
""" """
_instances = []
def __init__(self, operation, related_to=None, **kwargs): def __init__(self, operation, related_to=None, **kwargs):
# TODO add a way to not save password on app installation # TODO add a way to not save password on app installation
self.operation = operation self.operation = operation
@ -370,6 +417,8 @@ class OperationLogger(object):
self.logger = None self.logger = None
self._name = None self._name = None
self.data_to_redact = [] self.data_to_redact = []
self.parent = self.parent_logger()
self._instances.append(self)
for filename in ["/etc/yunohost/mysql", "/etc/yunohost/psql"]: for filename in ["/etc/yunohost/mysql", "/etc/yunohost/psql"]:
if os.path.exists(filename): if os.path.exists(filename):
@ -380,6 +429,54 @@ class OperationLogger(object):
if not os.path.exists(self.path): if not os.path.exists(self.path):
os.makedirs(self.path) os.makedirs(self.path)
def parent_logger(self):
# If there are other operation logger instances
for instance in reversed(self._instances):
# Is one of these operation logger started but not yet done ?
if instance.started_at is not None and instance.ended_at is None:
# We are a child of the first one we found
return instance.name
# If no lock exists, we are probably in tests or yunohost is used as a
# lib ... let's not really care about that case and assume we're the
# root logger then.
if not os.path.exists("/var/run/moulinette_yunohost.lock"):
return None
locks = read_file("/var/run/moulinette_yunohost.lock").strip().split("\n")
# If we're the process with the lock, we're the root logger
if locks == [] or str(os.getpid()) in locks:
return None
# If we get here, we are in a yunohost command called by a yunohost
# (maybe indirectly from an app script for example...)
#
# The strategy is :
# 1. list 20 most recent log files
# 2. iterate over the PID of parent processes
# 3. see if parent process has some log file open (being actively
# written in)
# 4. if among those file, there's an operation log file, we use the id
# of the most recent file
recent_operation_logs = sorted(glob.iglob(OPERATIONS_PATH + "*.log"), key=os.path.getctime, reverse=True)[:20]
proc = psutil.Process().parent()
while proc is not None:
# We use proc.open_files() to list files opened / actively used by this proc
# We only keep files matching a recent yunohost operation log
active_logs = sorted([f.path for f in proc.open_files() if f.path in recent_operation_logs], key=os.path.getctime, reverse=True)
if active_logs != []:
# extra the log if from the full path
return os.path.basename(active_logs[0])[:-4]
else:
proc = proc.parent()
continue
# If nothing found, assume we're the root operation logger
return None
def start(self): def start(self):
""" """
Start to record logs that change the system Start to record logs that change the system
@ -466,6 +563,7 @@ class OperationLogger(object):
data = { data = {
'started_at': self.started_at, 'started_at': self.started_at,
'operation': self.operation, 'operation': self.operation,
'parent': self.parent,
'yunohost_version': get_ynh_package_version("yunohost")["version"], 'yunohost_version': get_ynh_package_version("yunohost")["version"],
} }
if self.related_to is not None: if self.related_to is not None:
@ -502,8 +600,10 @@ class OperationLogger(object):
self.ended_at = datetime.utcnow() self.ended_at = datetime.utcnow()
self._error = error self._error = error
self._success = error is None self._success = error is None
if self.logger is not None: if self.logger is not None:
self.logger.removeHandler(self.file_handler) self.logger.removeHandler(self.file_handler)
self.file_handler.close()
is_api = msettings.get('interface') == 'api' is_api = msettings.get('interface') == 'api'
desc = _get_description_from_name(self.name) desc = _get_description_from_name(self.name)
@ -536,6 +636,15 @@ class OperationLogger(object):
self.error(m18n.n('log_operation_unit_unclosed_properly')) self.error(m18n.n('log_operation_unit_unclosed_properly'))
def _get_datetime_from_name(name):
# Filenames are expected to follow the format:
# 20200831-170740-short_description-and-stuff
raw_datetime = " ".join(name.split("-")[:2])
return datetime.strptime(raw_datetime, "%Y%m%d %H%M%S")
def _get_description_from_name(name): def _get_description_from_name(name):
""" """
Return the translated description from the filename Return the translated description from the filename

View file

@ -31,7 +31,6 @@ import random
from moulinette import m18n from moulinette import m18n
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from yunohost.user import user_list
from yunohost.log import is_unit_operation from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user') logger = getActionLogger('yunohost.user')

View file

@ -21,7 +21,6 @@
import os import os
import yaml import yaml
import json
import subprocess import subprocess
import shutil import shutil
import hashlib import hashlib
@ -31,7 +30,6 @@ from datetime import datetime
from moulinette import m18n from moulinette import m18n
from moulinette.utils import log, filesystem from moulinette.utils import log, filesystem
from moulinette.utils.filesystem import read_file
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from yunohost.log import is_unit_operation from yunohost.log import is_unit_operation
@ -49,7 +47,7 @@ logger = log.getActionLogger('yunohost.regenconf')
# FIXME : check for all reference of 'service' close to operation_logger stuff # FIXME : check for all reference of 'service' close to operation_logger stuff
@is_unit_operation([('names', 'configuration')]) @is_unit_operation([('names', 'configuration')])
def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False, def regen_conf(operation_logger, names=[], with_diff=False, force=False, dry_run=False,
list_pending=False): list_pending=False):
""" """
Regenerate the configuration file(s) Regenerate the configuration file(s)

View file

@ -346,16 +346,20 @@ def _get_and_format_service_status(service, infos):
'configuration': "unknown", 'configuration': "unknown",
} }
translation_key = "service_description_%s" % service # Try to get description directly from services.yml
description = infos.get("description") description = infos.get("description")
# If no description was there, try to get it from the .json locales
if not description: if not description:
translation_key = "service_description_%s" % service
description = m18n.n(translation_key) description = m18n.n(translation_key)
# that mean that we don't have a translation for this string # If descrption is still equal to the translation key,
# that's the only way to test for that for now # that mean that we don't have a translation for this string
# if we don't have it, uses the one provided by systemd # that's the only way to test for that for now
if description.decode('utf-8') == translation_key: # if we don't have it, uses the one provided by systemd
description = str(raw_status.get("Description", "")) if description.decode('utf-8') == translation_key:
description = str(raw_status.get("Description", ""))
output = { output = {
'status': str(raw_status.get("SubState", "unknown")), 'status': str(raw_status.get("SubState", "unknown")),
@ -650,7 +654,6 @@ def _tail(file, n):
avg_line_length = 74 avg_line_length = 74
to_read = n to_read = n
try: try:
if file.endswith(".gz"): if file.endswith(".gz"):
import gzip import gzip

View file

@ -15,6 +15,7 @@ logger = getActionLogger('yunohost.settings')
SETTINGS_PATH = "/etc/yunohost/settings.json" SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json" SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
def is_boolean(value): def is_boolean(value):
""" """
Ensure a string value is intended as a boolean Ensure a string value is intended as a boolean
@ -53,24 +54,22 @@ def is_boolean(value):
# * enum (in the form of a python list) # * enum (in the form of a python list)
DEFAULTS = OrderedDict([ 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"]}),
# Password Validation # Password Validation
# -1 disabled, 0 alert if listed, 1 8-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": 1}), ("security.password.admin.strength", {"type": "int", "default": 1}),
("security.password.user.strength", {"type": "int", "default": 1}), ("security.password.user.strength", {"type": "int", "default": 1}),
("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}), ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
("security.ssh.compatibility", {"type": "enum", "default": "modern", ("security.ssh.compatibility", {"type": "enum", "default": "modern",
"choices": ["intermediate", "modern"]}), "choices": ["intermediate", "modern"]}),
("security.nginx.compatibility", {"type": "enum", "default": "intermediate", ("security.nginx.compatibility", {"type": "enum", "default": "intermediate",
"choices": ["intermediate", "modern"]}), "choices": ["intermediate", "modern"]}),
("security.postfix.compatibility", {"type": "enum", "default": "intermediate", ("security.postfix.compatibility", {"type": "enum", "default": "intermediate",
"choices": ["intermediate", "modern"]}), "choices": ["intermediate", "modern"]}),
("pop3.enabled", {"type": "bool", "default": False}), ("pop3.enabled", {"type": "bool", "default": False}),
("smtp.allow_ipv6", {"type": "bool", "default": True}), ("smtp.allow_ipv6", {"type": "bool", "default": True}),
("backup.compress_tar_archives", {"type": "bool", "default": False}),
]) ])
@ -208,12 +207,19 @@ def settings_reset_all():
def _get_settings(): def _get_settings():
def get_setting_description(key):
if key.startswith("example"):
# (This is for dummy stuff used during unit tests)
return "Dummy %s setting" % key.split(".")[-1]
return m18n.n("global_settings_setting_%s" % key.replace(".", "_"))
settings = {} settings = {}
for key, value in DEFAULTS.copy().items(): for key, value in DEFAULTS.copy().items():
settings[key] = value settings[key] = value
settings[key]["value"] = value["default"] settings[key]["value"] = value["default"]
settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_")) settings[key]["description"] = get_setting_description(key)
if not os.path.exists(SETTINGS_PATH): if not os.path.exists(SETTINGS_PATH):
return settings return settings
@ -242,7 +248,7 @@ def _get_settings():
for key, value in local_settings.items(): for key, value in local_settings.items():
if key in settings: if key in settings:
settings[key] = value settings[key] = value
settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_")) settings[key]["description"] = get_setting_description(key)
else: else:
logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file', logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file',
setting_key=key)) setting_key=key))
@ -316,17 +322,20 @@ def reconfigure_nginx(setting_name, old_value, new_value):
if old_value != new_value: if old_value != new_value:
service_regen_conf(names=['nginx']) service_regen_conf(names=['nginx'])
@post_change_hook("security.ssh.compatibility") @post_change_hook("security.ssh.compatibility")
def reconfigure_ssh(setting_name, old_value, new_value): def reconfigure_ssh(setting_name, old_value, new_value):
if old_value != new_value: if old_value != new_value:
service_regen_conf(names=['ssh']) service_regen_conf(names=['ssh'])
@post_change_hook("smtp.allow_ipv6") @post_change_hook("smtp.allow_ipv6")
@post_change_hook("security.postfix.compatibility") @post_change_hook("security.postfix.compatibility")
def reconfigure_postfix(setting_name, old_value, new_value): def reconfigure_postfix(setting_name, old_value, new_value):
if old_value != new_value: if old_value != new_value:
service_regen_conf(names=['postfix']) service_regen_conf(names=['postfix'])
@post_change_hook("pop3.enabled") @post_change_hook("pop3.enabled")
def reconfigure_dovecot(setting_name, old_value, new_value): def reconfigure_dovecot(setting_name, old_value, new_value):
dovecot_package = 'dovecot-pop3d' dovecot_package = 'dovecot-pop3d'

View file

@ -1,12 +1,11 @@
import os import os
import pytest import pytest
import sys import sys
import moulinette
import moulinette
from moulinette import m18n from moulinette import m18n
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from contextlib import contextmanager from contextlib import contextmanager
sys.path.append("..") sys.path.append("..")
@ -19,10 +18,12 @@ def clone_test_app(request):
else: else:
os.system("cd %s/apps && git pull > /dev/null 2>&1" % cwd) os.system("cd %s/apps && git pull > /dev/null 2>&1" % cwd)
def get_test_apps_dir(): def get_test_apps_dir():
cwd = os.path.split(os.path.realpath(__file__))[0] cwd = os.path.split(os.path.realpath(__file__))[0]
return os.path.join(cwd, "apps") return os.path.join(cwd, "apps")
@contextmanager @contextmanager
def message(mocker, key, **kwargs): def message(mocker, key, **kwargs):
mocker.spy(m18n, "n") mocker.spy(m18n, "n")
@ -39,7 +40,6 @@ def raiseYunohostError(mocker, key, **kwargs):
assert e_info._excinfo[1].kwargs == kwargs assert e_info._excinfo[1].kwargs == kwargs
def pytest_addoption(parser): def pytest_addoption(parser):
parser.addoption("--yunodebug", action="store_true", default=False) parser.addoption("--yunodebug", action="store_true", default=False)
@ -57,12 +57,15 @@ def new_translate(self, key, *args, **kwargs):
raise KeyError("Unable to retrieve key %s for default locale !" % key) raise KeyError("Unable to retrieve key %s for default locale !" % key)
return old_translate(self, key, *args, **kwargs) return old_translate(self, key, *args, **kwargs)
moulinette.core.Translator.translate = new_translate moulinette.core.Translator.translate = new_translate
def new_m18nn(self, key, *args, **kwargs): def new_m18nn(self, key, *args, **kwargs):
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs) return self._namespaces[self._current_namespace].translate(key, *args, **kwargs)
moulinette.core.Moulinette18n.n = new_m18nn moulinette.core.Moulinette18n.n = new_m18nn
# #
@ -71,65 +74,7 @@ moulinette.core.Moulinette18n.n = new_m18nn
def pytest_cmdline_main(config): def pytest_cmdline_main(config):
"""Configure logging and initialize the moulinette"""
# Define loggers handlers
handlers = set(['tty'])
root_handlers = set(handlers)
# Define loggers level sys.path.insert(0, "/usr/lib/moulinette/")
level = 'DEBUG' import yunohost
if config.option.yunodebug: yunohost.init(debug=config.option.yunodebug)
tty_level = 'DEBUG'
else:
tty_level = 'INFO'
# Custom logging configuration
logging = {
'version': 1,
'disable_existing_loggers': True,
'formatters': {
'tty-debug': {
'format': '%(relativeCreated)-4d %(fmessage)s'
},
'precise': {
'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s'
},
},
'filters': {
'action': {
'()': 'moulinette.utils.log.ActionFilter',
},
},
'handlers': {
'tty': {
'level': tty_level,
'class': 'moulinette.interfaces.cli.TTYHandler',
'formatter': '',
},
},
'loggers': {
'yunohost': {
'level': level,
'handlers': handlers,
'propagate': False,
},
'moulinette': {
'level': level,
'handlers': [],
'propagate': True,
},
'moulinette.interface': {
'level': level,
'handlers': handlers,
'propagate': False,
},
},
'root': {
'level': level,
'handlers': root_handlers,
},
}
# Initialize moulinette
moulinette.init(logging_config=logging, _from_source=False)
moulinette.m18n.load_namespace('yunohost')

View file

@ -40,21 +40,21 @@ def test_parse_args_in_yunohost_format_empty():
def test_parse_args_in_yunohost_format_string(): def test_parse_args_in_yunohost_format_string():
questions = [{"name": "some_string", "type": "string",}] questions = [{"name": "some_string", "type": "string", }]
answers = {"some_string": "some_value"} answers = {"some_string": "some_value"}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_string_default_type(): def test_parse_args_in_yunohost_format_string_default_type():
questions = [{"name": "some_string",}] questions = [{"name": "some_string", }]
answers = {"some_string": "some_value"} answers = {"some_string": "some_value"}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_string_no_input(): def test_parse_args_in_yunohost_format_string_no_input():
questions = [{"name": "some_string",}] questions = [{"name": "some_string", }]
answers = {} answers = {}
with pytest.raises(YunohostError): with pytest.raises(YunohostError):
@ -62,7 +62,7 @@ def test_parse_args_in_yunohost_format_string_no_input():
def test_parse_args_in_yunohost_format_string_input(): def test_parse_args_in_yunohost_format_string_input():
questions = [{"name": "some_string", "ask": "some question",}] questions = [{"name": "some_string", "ask": "some question", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
@ -72,7 +72,7 @@ def test_parse_args_in_yunohost_format_string_input():
@pytest.mark.skip # that shit should work x( @pytest.mark.skip # that shit should work x(
def test_parse_args_in_yunohost_format_string_input_no_ask(): def test_parse_args_in_yunohost_format_string_input_no_ask():
questions = [{"name": "some_string",}] questions = [{"name": "some_string", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
@ -81,14 +81,14 @@ def test_parse_args_in_yunohost_format_string_input_no_ask():
def test_parse_args_in_yunohost_format_string_no_input_optional(): def test_parse_args_in_yunohost_format_string_no_input_optional():
questions = [{"name": "some_string", "optional": True,}] questions = [{"name": "some_string", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("", "string")}) expected_result = OrderedDict({"some_string": ("", "string")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_string_optional_with_input(): def test_parse_args_in_yunohost_format_string_optional_with_input():
questions = [{"name": "some_string", "ask": "some question", "optional": True,}] questions = [{"name": "some_string", "ask": "some question", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
@ -98,7 +98,7 @@ def test_parse_args_in_yunohost_format_string_optional_with_input():
@pytest.mark.skip # this should work without ask @pytest.mark.skip # this should work without ask
def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask(): def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask():
questions = [{"name": "some_string", "optional": True,}] questions = [{"name": "some_string", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
@ -108,7 +108,7 @@ def test_parse_args_in_yunohost_format_string_optional_with_input_without_ask():
def test_parse_args_in_yunohost_format_string_no_input_default(): def test_parse_args_in_yunohost_format_string_no_input_default():
questions = [ questions = [
{"name": "some_string", "ask": "some question", "default": "some_value",} {"name": "some_string", "ask": "some question", "default": "some_value", }
] ]
answers = {} answers = {}
expected_result = OrderedDict({"some_string": ("some_value", "string")}) expected_result = OrderedDict({"some_string": ("some_value", "string")})
@ -117,7 +117,7 @@ def test_parse_args_in_yunohost_format_string_no_input_default():
def test_parse_args_in_yunohost_format_string_input_test_ask(): def test_parse_args_in_yunohost_format_string_input_test_ask():
ask_text = "some question" ask_text = "some question"
questions = [{"name": "some_string", "ask": ask_text,}] questions = [{"name": "some_string", "ask": ask_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -128,7 +128,7 @@ def test_parse_args_in_yunohost_format_string_input_test_ask():
def test_parse_args_in_yunohost_format_string_input_test_ask_with_default(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_default():
ask_text = "some question" ask_text = "some question"
default_text = "some example" default_text = "some example"
questions = [{"name": "some_string", "ask": ask_text, "default": default_text,}] questions = [{"name": "some_string", "ask": ask_text, "default": default_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -140,7 +140,7 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_default():
def test_parse_args_in_yunohost_format_string_input_test_ask_with_example(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_example():
ask_text = "some question" ask_text = "some question"
example_text = "some example" example_text = "some example"
questions = [{"name": "some_string", "ask": ask_text, "example": example_text,}] questions = [{"name": "some_string", "ask": ask_text, "example": example_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -153,7 +153,7 @@ def test_parse_args_in_yunohost_format_string_input_test_ask_with_example():
def test_parse_args_in_yunohost_format_string_input_test_ask_with_help(): def test_parse_args_in_yunohost_format_string_input_test_ask_with_help():
ask_text = "some question" ask_text = "some question"
help_text = "some_help" help_text = "some_help"
questions = [{"name": "some_string", "ask": ask_text, "help": help_text,}] questions = [{"name": "some_string", "ask": ask_text, "help": help_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -188,7 +188,7 @@ def test_parse_args_in_yunohost_format_string_with_choice_bad():
def test_parse_args_in_yunohost_format_string_with_choice_ask(): def test_parse_args_in_yunohost_format_string_with_choice_ask():
ask_text = "some question" ask_text = "some question"
choices = ["fr", "en", "es", "it", "ru"] choices = ["fr", "en", "es", "it", "ru"]
questions = [{"name": "some_string", "ask": ask_text, "choices": choices,}] questions = [{"name": "some_string", "ask": ask_text, "choices": choices, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="ru") as prompt: with patch.object(msignals, "prompt", return_value="ru") as prompt:
@ -214,14 +214,14 @@ def test_parse_args_in_yunohost_format_string_with_choice_default():
def test_parse_args_in_yunohost_format_password(): def test_parse_args_in_yunohost_format_password():
questions = [{"name": "some_password", "type": "password",}] questions = [{"name": "some_password", "type": "password", }]
answers = {"some_password": "some_value"} answers = {"some_password": "some_value"}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_password_no_input(): def test_parse_args_in_yunohost_format_password_no_input():
questions = [{"name": "some_password", "type": "password",}] questions = [{"name": "some_password", "type": "password", }]
answers = {} answers = {}
with pytest.raises(YunohostError): with pytest.raises(YunohostError):
@ -229,7 +229,7 @@ def test_parse_args_in_yunohost_format_password_no_input():
def test_parse_args_in_yunohost_format_password_input(): def test_parse_args_in_yunohost_format_password_input():
questions = [{"name": "some_password", "type": "password", "ask": "some question",}] questions = [{"name": "some_password", "type": "password", "ask": "some question", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
@ -239,7 +239,7 @@ def test_parse_args_in_yunohost_format_password_input():
@pytest.mark.skip # that shit should work x( @pytest.mark.skip # that shit should work x(
def test_parse_args_in_yunohost_format_password_input_no_ask(): def test_parse_args_in_yunohost_format_password_input_no_ask():
questions = [{"name": "some_password", "type": "password",}] questions = [{"name": "some_password", "type": "password", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
@ -248,7 +248,7 @@ def test_parse_args_in_yunohost_format_password_input_no_ask():
def test_parse_args_in_yunohost_format_password_no_input_optional(): def test_parse_args_in_yunohost_format_password_no_input_optional():
questions = [{"name": "some_password", "type": "password", "optional": True,}] questions = [{"name": "some_password", "type": "password", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("", "password")}) expected_result = OrderedDict({"some_password": ("", "password")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -272,7 +272,7 @@ def test_parse_args_in_yunohost_format_password_optional_with_input():
@pytest.mark.skip # this should work without ask @pytest.mark.skip # this should work without ask
def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask(): def test_parse_args_in_yunohost_format_password_optional_with_input_without_ask():
questions = [{"name": "some_password", "type": "password", "optional": True,}] questions = [{"name": "some_password", "type": "password", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_password": ("some_value", "password")}) expected_result = OrderedDict({"some_password": ("some_value", "password")})
@ -316,7 +316,7 @@ def test_parse_args_in_yunohost_format_password_no_input_example():
def test_parse_args_in_yunohost_format_password_input_test_ask(): def test_parse_args_in_yunohost_format_password_input_test_ask():
ask_text = "some question" ask_text = "some question"
questions = [{"name": "some_password", "type": "password", "ask": ask_text,}] questions = [{"name": "some_password", "type": "password", "ask": ask_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -365,14 +365,14 @@ def test_parse_args_in_yunohost_format_password_input_test_ask_with_help():
def test_parse_args_in_yunohost_format_path(): def test_parse_args_in_yunohost_format_path():
questions = [{"name": "some_path", "type": "path",}] questions = [{"name": "some_path", "type": "path", }]
answers = {"some_path": "some_value"} answers = {"some_path": "some_value"}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_path_no_input(): def test_parse_args_in_yunohost_format_path_no_input():
questions = [{"name": "some_path", "type": "path",}] questions = [{"name": "some_path", "type": "path", }]
answers = {} answers = {}
with pytest.raises(YunohostError): with pytest.raises(YunohostError):
@ -380,7 +380,7 @@ def test_parse_args_in_yunohost_format_path_no_input():
def test_parse_args_in_yunohost_format_path_input(): def test_parse_args_in_yunohost_format_path_input():
questions = [{"name": "some_path", "type": "path", "ask": "some question",}] questions = [{"name": "some_path", "type": "path", "ask": "some question", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
@ -390,7 +390,7 @@ def test_parse_args_in_yunohost_format_path_input():
@pytest.mark.skip # that shit should work x( @pytest.mark.skip # that shit should work x(
def test_parse_args_in_yunohost_format_path_input_no_ask(): def test_parse_args_in_yunohost_format_path_input_no_ask():
questions = [{"name": "some_path", "type": "path",}] questions = [{"name": "some_path", "type": "path", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
@ -399,7 +399,7 @@ def test_parse_args_in_yunohost_format_path_input_no_ask():
def test_parse_args_in_yunohost_format_path_no_input_optional(): def test_parse_args_in_yunohost_format_path_no_input_optional():
questions = [{"name": "some_path", "type": "path", "optional": True,}] questions = [{"name": "some_path", "type": "path", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("", "path")}) expected_result = OrderedDict({"some_path": ("", "path")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -407,7 +407,7 @@ def test_parse_args_in_yunohost_format_path_no_input_optional():
def test_parse_args_in_yunohost_format_path_optional_with_input(): def test_parse_args_in_yunohost_format_path_optional_with_input():
questions = [ questions = [
{"name": "some_path", "ask": "some question", "type": "path", "optional": True,} {"name": "some_path", "ask": "some question", "type": "path", "optional": True, }
] ]
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
@ -418,7 +418,7 @@ def test_parse_args_in_yunohost_format_path_optional_with_input():
@pytest.mark.skip # this should work without ask @pytest.mark.skip # this should work without ask
def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask(): def test_parse_args_in_yunohost_format_path_optional_with_input_without_ask():
questions = [{"name": "some_path", "type": "path", "optional": True,}] questions = [{"name": "some_path", "type": "path", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_path": ("some_value", "path")}) expected_result = OrderedDict({"some_path": ("some_value", "path")})
@ -442,7 +442,7 @@ def test_parse_args_in_yunohost_format_path_no_input_default():
def test_parse_args_in_yunohost_format_path_input_test_ask(): def test_parse_args_in_yunohost_format_path_input_test_ask():
ask_text = "some question" ask_text = "some question"
questions = [{"name": "some_path", "type": "path", "ask": ask_text,}] questions = [{"name": "some_path", "type": "path", "ask": ask_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value="some_value") as prompt: with patch.object(msignals, "prompt", return_value="some_value") as prompt:
@ -454,7 +454,7 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_default():
ask_text = "some question" ask_text = "some question"
default_text = "some example" default_text = "some example"
questions = [ questions = [
{"name": "some_path", "type": "path", "ask": ask_text, "default": default_text,} {"name": "some_path", "type": "path", "ask": ask_text, "default": default_text, }
] ]
answers = {} answers = {}
@ -468,7 +468,7 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_example():
ask_text = "some question" ask_text = "some question"
example_text = "some example" example_text = "some example"
questions = [ questions = [
{"name": "some_path", "type": "path", "ask": ask_text, "example": example_text,} {"name": "some_path", "type": "path", "ask": ask_text, "example": example_text, }
] ]
answers = {} answers = {}
@ -483,7 +483,7 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_help():
ask_text = "some question" ask_text = "some question"
help_text = "some_help" help_text = "some_help"
questions = [ questions = [
{"name": "some_path", "type": "path", "ask": ask_text, "help": help_text,} {"name": "some_path", "type": "path", "ask": ask_text, "help": help_text, }
] ]
answers = {} answers = {}
@ -494,89 +494,89 @@ def test_parse_args_in_yunohost_format_path_input_test_ask_with_help():
def test_parse_args_in_yunohost_format_boolean(): def test_parse_args_in_yunohost_format_boolean():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
answers = {"some_boolean": "y"} answers = {"some_boolean": "y"}
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
def test_parse_args_in_yunohost_format_boolean_all_yes(): def test_parse_args_in_yunohost_format_boolean_all_yes():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "y"}, questions) _parse_args_in_yunohost_format({"some_boolean": "y"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) _parse_args_in_yunohost_format({"some_boolean": "Y"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) _parse_args_in_yunohost_format({"some_boolean": "yes"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) _parse_args_in_yunohost_format({"some_boolean": "Yes"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) _parse_args_in_yunohost_format({"some_boolean": "YES"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "1"}, questions) _parse_args_in_yunohost_format({"some_boolean": "1"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": 1}, questions) _parse_args_in_yunohost_format({"some_boolean": 1}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": True}, questions) _parse_args_in_yunohost_format({"some_boolean": True}, questions) ==
== expected_result expected_result
) )
def test_parse_args_in_yunohost_format_boolean_all_no(): def test_parse_args_in_yunohost_format_boolean_all_no():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) expected_result = OrderedDict({"some_boolean": (0, "boolean")})
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "n"}, questions) _parse_args_in_yunohost_format({"some_boolean": "n"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "N"}, questions) _parse_args_in_yunohost_format({"some_boolean": "N"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "no"}, questions) _parse_args_in_yunohost_format({"some_boolean": "no"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "No"}, questions) _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "No"}, questions) _parse_args_in_yunohost_format({"some_boolean": "No"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": "0"}, questions) _parse_args_in_yunohost_format({"some_boolean": "0"}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": 0}, questions) _parse_args_in_yunohost_format({"some_boolean": 0}, questions) ==
== expected_result expected_result
) )
assert ( assert (
_parse_args_in_yunohost_format({"some_boolean": False}, questions) _parse_args_in_yunohost_format({"some_boolean": False}, questions) ==
== expected_result expected_result
) )
# XXX apparently boolean are always False (0) by default, I'm not sure what to think about that # XXX apparently boolean are always False (0) by default, I'm not sure what to think about that
def test_parse_args_in_yunohost_format_boolean_no_input(): def test_parse_args_in_yunohost_format_boolean_no_input():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) expected_result = OrderedDict({"some_boolean": (0, "boolean")})
@ -584,7 +584,7 @@ def test_parse_args_in_yunohost_format_boolean_no_input():
def test_parse_args_in_yunohost_format_boolean_bad_input(): def test_parse_args_in_yunohost_format_boolean_bad_input():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
answers = {"some_boolean": "stuff"} answers = {"some_boolean": "stuff"}
with pytest.raises(YunohostError): with pytest.raises(YunohostError):
@ -592,7 +592,7 @@ def test_parse_args_in_yunohost_format_boolean_bad_input():
def test_parse_args_in_yunohost_format_boolean_input(): def test_parse_args_in_yunohost_format_boolean_input():
questions = [{"name": "some_boolean", "type": "boolean", "ask": "some question",}] questions = [{"name": "some_boolean", "type": "boolean", "ask": "some question", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (1, "boolean")}) expected_result = OrderedDict({"some_boolean": (1, "boolean")})
@ -606,7 +606,7 @@ def test_parse_args_in_yunohost_format_boolean_input():
@pytest.mark.skip # we should work @pytest.mark.skip # we should work
def test_parse_args_in_yunohost_format_boolean_input_no_ask(): def test_parse_args_in_yunohost_format_boolean_input_no_ask():
questions = [{"name": "some_boolean", "type": "boolean",}] questions = [{"name": "some_boolean", "type": "boolean", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": ("some_value", "boolean")}) expected_result = OrderedDict({"some_boolean": ("some_value", "boolean")})
@ -615,7 +615,7 @@ def test_parse_args_in_yunohost_format_boolean_input_no_ask():
def test_parse_args_in_yunohost_format_boolean_no_input_optional(): def test_parse_args_in_yunohost_format_boolean_no_input_optional():
questions = [{"name": "some_boolean", "type": "boolean", "optional": True,}] questions = [{"name": "some_boolean", "type": "boolean", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false expected_result = OrderedDict({"some_boolean": (0, "boolean")}) # default to false
assert _parse_args_in_yunohost_format(answers, questions) == expected_result assert _parse_args_in_yunohost_format(answers, questions) == expected_result
@ -638,7 +638,7 @@ def test_parse_args_in_yunohost_format_boolean_optional_with_input():
def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask(): def test_parse_args_in_yunohost_format_boolean_optional_with_input_without_ask():
questions = [{"name": "some_boolean", "type": "boolean", "optional": True,}] questions = [{"name": "some_boolean", "type": "boolean", "optional": True, }]
answers = {} answers = {}
expected_result = OrderedDict({"some_boolean": (0, "boolean")}) expected_result = OrderedDict({"some_boolean": (0, "boolean")})
@ -677,7 +677,7 @@ def test_parse_args_in_yunohost_format_boolean_bad_default():
def test_parse_args_in_yunohost_format_boolean_input_test_ask(): def test_parse_args_in_yunohost_format_boolean_input_test_ask():
ask_text = "some question" ask_text = "some question"
questions = [{"name": "some_boolean", "type": "boolean", "ask": ask_text,}] questions = [{"name": "some_boolean", "type": "boolean", "ask": ask_text, }]
answers = {} answers = {}
with patch.object(msignals, "prompt", return_value=0) as prompt: with patch.object(msignals, "prompt", return_value=0) as prompt:
@ -704,7 +704,7 @@ def test_parse_args_in_yunohost_format_boolean_input_test_ask_with_default():
def test_parse_args_in_yunohost_format_domain_empty(): def test_parse_args_in_yunohost_format_domain_empty():
questions = [{"name": "some_domain", "type": "domain",}] questions = [{"name": "some_domain", "type": "domain", }]
answers = {} answers = {}
with patch.object( with patch.object(
@ -719,7 +719,7 @@ def test_parse_args_in_yunohost_format_domain_empty():
def test_parse_args_in_yunohost_format_domain(): def test_parse_args_in_yunohost_format_domain():
main_domain = "my_main_domain.com" main_domain = "my_main_domain.com"
domains = [main_domain] domains = [main_domain]
questions = [{"name": "some_domain", "type": "domain",}] questions = [{"name": "some_domain", "type": "domain", }]
answers = {"some_domain": main_domain} answers = {"some_domain": main_domain}
expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
@ -735,7 +735,7 @@ def test_parse_args_in_yunohost_format_domain_two_domains():
other_domain = "some_other_domain.tld" other_domain = "some_other_domain.tld"
domains = [main_domain, other_domain] domains = [main_domain, other_domain]
questions = [{"name": "some_domain", "type": "domain",}] questions = [{"name": "some_domain", "type": "domain", }]
answers = {"some_domain": other_domain} answers = {"some_domain": other_domain}
expected_result = OrderedDict({"some_domain": (other_domain, "domain")}) expected_result = OrderedDict({"some_domain": (other_domain, "domain")})
@ -758,7 +758,7 @@ def test_parse_args_in_yunohost_format_domain_two_domains_wrong_answer():
other_domain = "some_other_domain.tld" other_domain = "some_other_domain.tld"
domains = [main_domain, other_domain] domains = [main_domain, other_domain]
questions = [{"name": "some_domain", "type": "domain",}] questions = [{"name": "some_domain", "type": "domain", }]
answers = {"some_domain": "doesnt_exist.pouet"} answers = {"some_domain": "doesnt_exist.pouet"}
with patch.object( with patch.object(
@ -774,7 +774,7 @@ def test_parse_args_in_yunohost_format_domain_two_domains_default_no_ask():
other_domain = "some_other_domain.tld" other_domain = "some_other_domain.tld"
domains = [main_domain, other_domain] domains = [main_domain, other_domain]
questions = [{"name": "some_domain", "type": "domain",}] questions = [{"name": "some_domain", "type": "domain", }]
answers = {} answers = {}
expected_result = OrderedDict({"some_domain": (main_domain, "domain")}) expected_result = OrderedDict({"some_domain": (main_domain, "domain")})
@ -831,7 +831,7 @@ def test_parse_args_in_yunohost_format_user_empty():
} }
} }
questions = [{"name": "some_user", "type": "user",}] questions = [{"name": "some_user", "type": "user", }]
answers = {} answers = {}
with patch.object(user, "user_list", return_value={"users": users}): with patch.object(user, "user_list", return_value={"users": users}):
@ -852,7 +852,7 @@ def test_parse_args_in_yunohost_format_user():
} }
} }
questions = [{"name": "some_user", "type": "user",}] questions = [{"name": "some_user", "type": "user", }]
answers = {"some_user": username} answers = {"some_user": username}
expected_result = OrderedDict({"some_user": (username, "user")}) expected_result = OrderedDict({"some_user": (username, "user")})
@ -883,7 +883,7 @@ def test_parse_args_in_yunohost_format_user_two_users():
}, },
} }
questions = [{"name": "some_user", "type": "user",}] questions = [{"name": "some_user", "type": "user", }]
answers = {"some_user": other_user} answers = {"some_user": other_user}
expected_result = OrderedDict({"some_user": (other_user, "user")}) expected_result = OrderedDict({"some_user": (other_user, "user")})
@ -919,7 +919,7 @@ def test_parse_args_in_yunohost_format_user_two_users_wrong_answer():
}, },
} }
questions = [{"name": "some_user", "type": "user",}] questions = [{"name": "some_user", "type": "user", }]
answers = {"some_user": "doesnt_exist.pouet"} answers = {"some_user": "doesnt_exist.pouet"}
with patch.object(user, "user_list", return_value={"users": users}): with patch.object(user, "user_list", return_value={"users": users}):
@ -1002,7 +1002,7 @@ def test_parse_args_in_yunohost_format_app_empty():
} }
] ]
questions = [{"name": "some_app", "type": "app",}] questions = [{"name": "some_app", "type": "app", }]
answers = {} answers = {}
with patch.object(app, "app_list", return_value={"apps": apps}): with patch.object(app, "app_list", return_value={"apps": apps}):
@ -1012,7 +1012,7 @@ def test_parse_args_in_yunohost_format_app_empty():
def test_parse_args_in_yunohost_format_app_no_apps(): def test_parse_args_in_yunohost_format_app_no_apps():
apps = [] apps = []
questions = [{"name": "some_app", "type": "app",}] questions = [{"name": "some_app", "type": "app", }]
answers = {} answers = {}
with patch.object(app, "app_list", return_value={"apps": apps}): with patch.object(app, "app_list", return_value={"apps": apps}):
@ -1041,7 +1041,7 @@ def test_parse_args_in_yunohost_format_app():
} }
] ]
questions = [{"name": "some_app", "type": "app",}] questions = [{"name": "some_app", "type": "app", }]
answers = {"some_app": app_name} answers = {"some_app": app_name}
expected_result = OrderedDict({"some_app": (app_name, "app")}) expected_result = OrderedDict({"some_app": (app_name, "app")})
@ -1072,7 +1072,7 @@ def test_parse_args_in_yunohost_format_app_two_apps():
}, },
] ]
questions = [{"name": "some_app", "type": "app",}] questions = [{"name": "some_app", "type": "app", }]
answers = {"some_app": other_app} answers = {"some_app": other_app}
expected_result = OrderedDict({"some_app": (other_app, "app")}) expected_result = OrderedDict({"some_app": (other_app, "app")})
@ -1112,7 +1112,7 @@ def test_parse_args_in_yunohost_format_app_two_apps_wrong_answer():
}, },
] ]
questions = [{"name": "some_app", "type": "app",}] questions = [{"name": "some_app", "type": "app", }]
answers = {"some_app": "doesnt_exist"} answers = {"some_app": "doesnt_exist"}
with pytest.raises(YunohostError): with pytest.raises(YunohostError):

View file

@ -6,7 +6,7 @@ import glob
import shutil import shutil
from moulinette import m18n from moulinette import m18n
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml, mkdir from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from yunohost.app import (_initialize_apps_catalog_system, from yunohost.app import (_initialize_apps_catalog_system,
@ -37,10 +37,12 @@ DUMMY_APP_CATALOG = """{
} }
""" """
class AnyStringWith(str): class AnyStringWith(str):
def __eq__(self, other): def __eq__(self, other):
return self in other return self in other
def setup_function(function): def setup_function(function):
# Clear apps catalog cache # Clear apps catalog cache
@ -165,6 +167,7 @@ def test_apps_catalog_update_404(mocker):
_update_apps_catalog() _update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_failed_to_download") m18n.n.assert_any_call("apps_catalog_failed_to_download")
def test_apps_catalog_update_timeout(mocker): def test_apps_catalog_update_timeout(mocker):
# Initialize ... # Initialize ...
@ -237,7 +240,6 @@ def test_apps_catalog_load_with_empty_cache(mocker):
m18n.n.assert_any_call("apps_catalog_obsolete_cache") m18n.n.assert_any_call("apps_catalog_obsolete_cache")
m18n.n.assert_any_call("apps_catalog_update_success") m18n.n.assert_any_call("apps_catalog_update_success")
# Cache shouldn't be empty anymore empty # Cache shouldn't be empty anymore empty
assert glob.glob(APPS_CATALOG_CACHE + "/*") assert glob.glob(APPS_CATALOG_CACHE + "/*")

View file

@ -17,6 +17,7 @@ from yunohost.hook import CUSTOM_HOOK_FOLDER
# Get main domain # Get main domain
maindomain = "" maindomain = ""
def setup_function(function): def setup_function(function):
global maindomain global maindomain
@ -62,7 +63,7 @@ def setup_function(function):
if "with_permission_app_installed" in markers: if "with_permission_app_installed" in markers:
assert not app_is_installed("permissions_app") assert not app_is_installed("permissions_app")
user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh") user_create("alice", "Alice", "White", maindomain, "test123Ynh")
install_app("permissions_app_ynh", "/urlpermissionapp" install_app("permissions_app_ynh", "/urlpermissionapp"
"&admin=alice") "&admin=alice")
assert app_is_installed("permissions_app") assert app_is_installed("permissions_app")
@ -99,6 +100,7 @@ def check_LDAP_db_integrity_call():
yield yield
check_LDAP_db_integrity() check_LDAP_db_integrity()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def check_permission_for_apps_call(): def check_permission_for_apps_call():
check_permission_for_apps() check_permission_for_apps()
@ -109,6 +111,7 @@ def check_permission_for_apps_call():
# Helpers # # Helpers #
# #
def app_is_installed(app): def app_is_installed(app):
if app == "permissions_app": if app == "permissions_app":
@ -193,22 +196,22 @@ def add_archive_wordpress_from_2p4():
os.system("mkdir -p /home/yunohost.backup/archives") os.system("mkdir -p /home/yunohost.backup/archives")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json") + \ os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.info.json")
" /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json") + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.info.json")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz") + \ os.system("cp " + os.path.join(get_test_apps_dir(), "backup_wordpress_from_2p4/backup.tar.gz")
" /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz") + " /home/yunohost.backup/archives/backup_wordpress_from_2p4.tar.gz")
def add_archive_system_from_2p4(): def add_archive_system_from_2p4():
os.system("mkdir -p /home/yunohost.backup/archives") os.system("mkdir -p /home/yunohost.backup/archives")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json") + \ os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.info.json")
" /home/yunohost.backup/archives/backup_system_from_2p4.info.json") + " /home/yunohost.backup/archives/backup_system_from_2p4.info.json")
os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz") + \ os.system("cp " + os.path.join(get_test_apps_dir(), "backup_system_from_2p4/backup.tar.gz")
" /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz") + " /home/yunohost.backup/archives/backup_system_from_2p4.tar.gz")
# #
# System backup # # System backup #
@ -318,7 +321,7 @@ def test_backup_script_failure_handling(monkeypatch, mocker):
# with the expected error message key # with the expected error message key
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec) monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
with message(mocker, 'backup_app_failed', app='backup_recommended_app'): with message(mocker, 'backup_app_failed', app='backup_recommended_app'):
with raiseYunohostError(mocker, 'backup_nothings_done'): with raiseYunohostError(mocker, 'backup_nothings_done'):
backup_create(system=None, apps=["backup_recommended_app"]) backup_create(system=None, apps=["backup_recommended_app"])
@ -384,7 +387,7 @@ def test_backup_with_different_output_directory(mocker):
output_directory="/opt/test_backup_output_directory", output_directory="/opt/test_backup_output_directory",
name="backup") name="backup")
assert os.path.exists("/opt/test_backup_output_directory/backup.tar.gz") assert os.path.exists("/opt/test_backup_output_directory/backup.tar")
archives = backup_list()["archives"] archives = backup_list()["archives"]
assert len(archives) == 1 assert len(archives) == 1
@ -396,13 +399,13 @@ def test_backup_with_different_output_directory(mocker):
@pytest.mark.clean_opt_dir @pytest.mark.clean_opt_dir
def test_backup_with_no_compress(mocker): def test_backup_using_copy_method(mocker):
# Create the backup # Create the backup
with message(mocker, "backup_created"): with message(mocker, "backup_created"):
backup_create(system=["conf_nginx"], apps=None, backup_create(system=["conf_nginx"], apps=None,
output_directory="/opt/test_backup_output_directory", output_directory="/opt/test_backup_output_directory",
no_compress=True, methods=["copy"],
name="backup") name="backup")
assert os.path.exists("/opt/test_backup_output_directory/info.json") assert os.path.exists("/opt/test_backup_output_directory/info.json")
@ -511,6 +514,7 @@ def test_backup_and_restore_with_ynh_restore(mocker):
_test_backup_and_restore_app(mocker, "backup_recommended_app") _test_backup_and_restore_app(mocker, "backup_recommended_app")
@pytest.mark.with_permission_app_installed @pytest.mark.with_permission_app_installed
def test_backup_and_restore_permission_app(mocker): def test_backup_and_restore_permission_app(mocker):
@ -560,7 +564,7 @@ def _test_backup_and_restore_app(mocker, app):
# Uninstall the app # Uninstall the app
app_remove(app) app_remove(app)
assert not app_is_installed(app) assert not app_is_installed(app)
assert app+".main" not in user_permission_list()['permissions'] assert app + ".main" not in user_permission_list()['permissions']
# Restore the app # Restore the app
with message(mocker, "restore_complete"): with message(mocker, "restore_complete"):
@ -571,7 +575,7 @@ def _test_backup_and_restore_app(mocker, app):
# Check permission # Check permission
per_list = user_permission_list()['permissions'] per_list = user_permission_list()['permissions']
assert app+".main" in per_list assert app + ".main" in per_list
# #
# Some edge cases # # Some edge cases #
@ -589,6 +593,7 @@ def test_restore_archive_with_no_json(mocker):
with raiseYunohostError(mocker, 'backup_archive_cant_retrieve_info_json'): with raiseYunohostError(mocker, 'backup_archive_cant_retrieve_info_json'):
backup_restore(name="badbackup", force=True) backup_restore(name="badbackup", force=True)
@pytest.mark.with_wordpress_archive_from_2p4 @pytest.mark.with_wordpress_archive_from_2p4
def test_restore_archive_with_bad_archive(mocker): def test_restore_archive_with_bad_archive(mocker):
@ -617,9 +622,9 @@ def test_restore_archive_with_custom_hook(mocker):
# Restore system with custom hook # Restore system with custom hook
with message(mocker, "restore_complete"): with message(mocker, "restore_complete"):
backup_restore(name=backup_list()["archives"][0], backup_restore(name=backup_list()["archives"][0],
system=[], system=[],
apps=None, apps=None,
force=True) force=True)
os.system("rm %s/99-yolo" % custom_restore_hook_folder) os.system("rm %s/99-yolo" % custom_restore_hook_folder)

View file

@ -13,6 +13,7 @@ from yunohost.utils.error import YunohostError
# Get main domain # Get main domain
maindomain = "" maindomain = ""
def setup_function(function): def setup_function(function):
global maindomain global maindomain
maindomain = _get_maindomain() maindomain = _get_maindomain()

View file

@ -1,3 +1,4 @@
import socket
import requests import requests
import pytest import pytest
import string import string
@ -7,11 +8,11 @@ import shutil
from conftest import message, raiseYunohostError, get_test_apps_dir from conftest import message, raiseYunohostError, get_test_apps_dir
from yunohost.app import app_install, app_upgrade, app_remove, app_change_url, app_list, app_map, _installed_apps, APPS_SETTING_PATH, _set_app_settings, _get_app_settings from yunohost.app import app_install, app_upgrade, app_remove, app_change_url, app_map, _installed_apps, APPS_SETTING_PATH, _set_app_settings, _get_app_settings
from yunohost.user import user_list, user_create, user_delete, \ from yunohost.user import user_list, user_create, user_delete, \
user_group_list, user_group_delete user_group_list, user_group_delete
from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \ from yunohost.permission import user_permission_update, user_permission_list, user_permission_reset, \
permission_create, permission_delete, permission_url permission_create, permission_delete, permission_url
from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list
# Get main domain # Get main domain
@ -21,7 +22,6 @@ dummy_password = "test123Ynh"
# Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address. # Dirty patch of DNS resolution. Force the DNS to 127.0.0.1 address even if dnsmasq have the public address.
# Mainly used for 'can_access_webpage' function # Mainly used for 'can_access_webpage' function
import socket
prv_getaddrinfo = socket.getaddrinfo prv_getaddrinfo = socket.getaddrinfo
@ -62,7 +62,7 @@ def _clear_dummy_app_settings():
if os.path.exists(app_setting_path): if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path) shutil.rmtree(app_setting_path)
def clean_user_groups_permission(): def clean_user_groups_permission():
for u in user_list()['users']: for u in user_list()['users']:
user_delete(u) user_delete(u)
@ -97,6 +97,7 @@ def setup_function(function):
dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, '', ('127.0.0.1', 443))]} dns_cache = {(maindomain, 443, 0, 1): [(2, 1, 6, '', ('127.0.0.1', 443))]}
for domain in other_domains: for domain in other_domains:
dns_cache[(domain, 443, 0, 1)] = [(2, 1, 6, '', ('127.0.0.1', 443))] dns_cache[(domain, 443, 0, 1)] = [(2, 1, 6, '', ('127.0.0.1', 443))]
def new_getaddrinfo(*args): def new_getaddrinfo(*args):
try: try:
return dns_cache[args] return dns_cache[args]
@ -106,8 +107,8 @@ def setup_function(function):
return res return res
socket.getaddrinfo = new_getaddrinfo socket.getaddrinfo = new_getaddrinfo
user_create("alice", "Alice", "White", "alice@" + maindomain, dummy_password) user_create("alice", "Alice", "White", maindomain, dummy_password)
user_create("bob", "Bob", "Snow", "bob@" + maindomain, dummy_password) user_create("bob", "Bob", "Snow", maindomain, dummy_password)
_permission_create_with_dummy_app(permission="wiki.main", url="/", additional_urls=['/whatever','/idontnow'], auth_header=False, _permission_create_with_dummy_app(permission="wiki.main", url="/", additional_urls=['/whatever','/idontnow'], auth_header=False,
label="Wiki", show_tile=True, label="Wiki", show_tile=True,
allowed=["all_users"], protected=False, sync_perm=False, allowed=["all_users"], protected=False, sync_perm=False,
@ -118,7 +119,6 @@ def setup_function(function):
allowed=["alice"], domain=maindomain, path='/blog') allowed=["alice"], domain=maindomain, path='/blog')
_permission_create_with_dummy_app(permission="blog.api", allowed=["visitors"], protected=True, sync_perm=True) _permission_create_with_dummy_app(permission="blog.api", allowed=["visitors"], protected=True, sync_perm=True)
def teardown_function(function): def teardown_function(function):
clean_user_groups_permission() clean_user_groups_permission()
global other_domains global other_domains
@ -934,9 +934,9 @@ def test_permission_app_propagation_on_ssowat():
# alice gotta be allowed on the main permission to access the admin tho # alice gotta be allowed on the main permission to access the admin tho
user_permission_update("permissions_app.main", remove="bob", add="all_users") user_permission_update("permissions_app.main", remove="bob", add="all_users")
assert not can_access_webpage(app_webroot+"/admin", logged_as=None) assert not can_access_webpage(app_webroot + "/admin", logged_as=None)
assert can_access_webpage(app_webroot+"/admin", logged_as="alice") assert can_access_webpage(app_webroot + "/admin", logged_as="alice")
assert not can_access_webpage(app_webroot+"/admin", logged_as="bob") assert not can_access_webpage(app_webroot + "/admin", logged_as="bob")
@pytest.mark.other_domains(number=1) @pytest.mark.other_domains(number=1)

View file

@ -9,6 +9,7 @@ TEST_DOMAIN_NGINX_CONFIG = "/etc/nginx/conf.d/%s.conf" % TEST_DOMAIN
TEST_DOMAIN_DNSMASQ_CONFIG = "/etc/dnsmasq.d/%s" % TEST_DOMAIN TEST_DOMAIN_DNSMASQ_CONFIG = "/etc/dnsmasq.d/%s" % TEST_DOMAIN
SSHD_CONFIG = "/etc/ssh/sshd_config" SSHD_CONFIG = "/etc/ssh/sshd_config"
def setup_function(function): def setup_function(function):
_force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG]) _force_clear_hashes([TEST_DOMAIN_NGINX_CONFIG])
@ -169,7 +170,7 @@ def test_stale_hashes_if_file_manually_deleted():
# ... Anyway, the proper way to write these tests would be to use a dummy # ... Anyway, the proper way to write these tests would be to use a dummy
# regen-conf hook just for tests but meh I'm lazy # regen-conf hook just for tests but meh I'm lazy
# #
#def test_stale_hashes_if_file_manually_modified(): # def test_stale_hashes_if_file_manually_modified():
# """ # """
# Same as other test, but manually delete the file in between and check # Same as other test, but manually delete the file in between and check
# behavior # behavior

View file

@ -64,11 +64,13 @@ def test_service_add():
service_add("dummyservice", description="A dummy service to run tests") service_add("dummyservice", description="A dummy service to run tests")
assert "dummyservice" in service_status().keys() assert "dummyservice" in service_status().keys()
def test_service_add_real_service(): def test_service_add_real_service():
service_add("networking") service_add("networking")
assert "networking" in service_status().keys() assert "networking" in service_status().keys()
def test_service_remove(): def test_service_remove():
service_add("dummyservice", description="A dummy service to run tests") service_add("dummyservice", description="A dummy service to run tests")

View file

@ -6,7 +6,12 @@ from yunohost.utils.error import YunohostError
from yunohost.settings import settings_get, settings_list, _get_settings, \ from yunohost.settings import settings_get, settings_list, _get_settings, \
settings_set, settings_reset, settings_reset_all, \ settings_set, settings_reset, settings_reset_all, \
SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH SETTINGS_PATH_OTHER_LOCATION, SETTINGS_PATH, DEFAULTS
DEFAULTS["example.bool"] = {"type": "bool", "default": True}
DEFAULTS["example.int"] = {"type": "int", "default": 42}
DEFAULTS["example.string"] = {"type": "string", "default": "yolo swag"}
DEFAULTS["example.enum"] = {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}
def setup_function(function): def setup_function(function):
@ -22,7 +27,7 @@ def test_settings_get_bool():
def test_settings_get_full_bool(): def test_settings_get_full_bool():
assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Example boolean option"} assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Dummy bool setting"}
def test_settings_get_int(): def test_settings_get_int():
@ -30,7 +35,7 @@ def test_settings_get_int():
def test_settings_get_full_int(): def test_settings_get_full_int():
assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Example int option"} assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Dummy int setting"}
def test_settings_get_string(): def test_settings_get_string():
@ -38,7 +43,7 @@ def test_settings_get_string():
def test_settings_get_full_string(): def test_settings_get_full_string():
assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Example string option"} assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Dummy string setting"}
def test_settings_get_enum(): def test_settings_get_enum():
@ -46,7 +51,7 @@ def test_settings_get_enum():
def test_settings_get_full_enum(): 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"]} assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Dummy enum setting", "choices": ["a", "b", "c"]}
def test_settings_get_doesnt_exists(): def test_settings_get_doesnt_exists():
@ -60,10 +65,11 @@ def test_settings_list():
def test_settings_set(): def test_settings_set():
settings_set("example.bool", False) settings_set("example.bool", False)
assert settings_get("example.bool") == False assert settings_get("example.bool") is False
settings_set("example.bool", "on") settings_set("example.bool", "on")
assert settings_get("example.bool") == True assert settings_get("example.bool") is True
def test_settings_set_int(): def test_settings_set_int():
settings_set("example.int", 21) settings_set("example.int", 21)
@ -114,7 +120,7 @@ def test_settings_set_bad_value_enum():
def test_settings_list_modified(): def test_settings_list_modified():
settings_set("example.int", 21) settings_set("example.int", 21)
assert settings_list()["example.int"] == {'default': 42, 'description': 'Example int option', 'type': 'int', 'value': 21} assert settings_list()["example.int"] == {'default': 42, 'description': 'Dummy int setting', 'type': 'int', 'value': 21}
def test_reset(): def test_reset():

View file

@ -3,7 +3,7 @@ import pytest
from conftest import message, raiseYunohostError from conftest import message, raiseYunohostError
from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \ from yunohost.user import user_list, user_info, user_create, user_delete, user_update, \
user_group_list, user_group_create, user_group_delete, user_group_update user_group_list, user_group_create, user_group_delete, user_group_update
from yunohost.domain import _get_maindomain from yunohost.domain import _get_maindomain
from yunohost.tests.test_permission import check_LDAP_db_integrity from yunohost.tests.test_permission import check_LDAP_db_integrity
@ -25,10 +25,10 @@ def setup_function(function):
global maindomain global maindomain
maindomain = _get_maindomain() maindomain = _get_maindomain()
user_create("alice", "Alice", "White", "alice@" + maindomain, "test123Ynh") user_create("alice", "Alice", "White", maindomain, "test123Ynh")
user_create("bob", "Bob", "Snow", "bob@" + maindomain, "test123Ynh") user_create("bob", "Bob", "Snow", maindomain, "test123Ynh")
user_create("jack", "Jack", "Black", "jack@" + maindomain, "test123Ynh") user_create("jack", "Jack", "Black", maindomain, "test123Ynh")
user_group_create("dev") user_group_create("dev")
user_group_create("apps") user_group_create("apps")
@ -79,7 +79,7 @@ def test_list_groups():
def test_create_user(mocker): def test_create_user(mocker):
with message(mocker, "user_created"): with message(mocker, "user_created"):
user_create("albert", "Albert", "Good", "alber@" + maindomain, "test123Ynh") user_create("albert", "Albert", "Good", maindomain, "test123Ynh")
group_res = user_group_list()['groups'] group_res = user_group_list()['groups']
assert "albert" in user_list()['users'] assert "albert" in user_list()['users']
@ -123,25 +123,26 @@ def test_del_group(mocker):
# #
def test_create_user_with_mail_address_already_taken(mocker):
with raiseYunohostError(mocker, "user_creation_failed"):
user_create("alice2", "Alice", "White", "alice@" + maindomain, "test123Ynh")
def test_create_user_with_password_too_simple(mocker): def test_create_user_with_password_too_simple(mocker):
with raiseYunohostError(mocker, "password_listed"): with raiseYunohostError(mocker, "password_listed"):
user_create("other", "Alice", "White", "other@" + maindomain, "12") user_create("other", "Alice", "White", maindomain, "12")
def test_create_user_already_exists(mocker): def test_create_user_already_exists(mocker):
with raiseYunohostError(mocker, "user_already_exists"): with raiseYunohostError(mocker, "user_already_exists"):
user_create("alice", "Alice", "White", "other@" + maindomain, "test123Ynh") user_create("alice", "Alice", "White", maindomain, "test123Ynh")
def test_create_user_with_domain_that_doesnt_exists(mocker):
with raiseYunohostError(mocker, "domain_unknown"):
user_create("alice", "Alice", "White", "doesnt.exists", "test123Ynh")
def test_update_user_with_mail_address_already_taken(mocker): def test_update_user_with_mail_address_already_taken(mocker):
with raiseYunohostError(mocker, "user_update_failed"): with raiseYunohostError(mocker, "user_update_failed"):
user_update("bob", add_mailalias="alice@" + maindomain) user_update("bob", add_mailalias="alice@" + maindomain)
def test_update_user_with_mail_address_with_unknown_domain(mocker):
with raiseYunohostError(mocker, "mail_domain_unknown"):
user_update("alice", add_mailalias="alice@doesnt.exists")
def test_del_user_that_does_not_exist(mocker): def test_del_user_that_does_not_exist(mocker):
with raiseYunohostError(mocker, "user_unknown"): with raiseYunohostError(mocker, "user_unknown"):

View file

@ -26,10 +26,8 @@
import re import re
import os import os
import yaml import yaml
import json
import subprocess import subprocess
import pwd import pwd
import socket
from importlib import import_module from importlib import import_module
from moulinette import msignals, m18n from moulinette import msignals, m18n
@ -37,8 +35,8 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output from moulinette.utils.process import check_output, call_async_output
from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
from yunohost.app import _update_apps_catalog, app_info, app_upgrade, app_ssowatconf, app_list, _initialize_apps_catalog_system from yunohost.app import _update_apps_catalog, app_info, app_upgrade, _initialize_apps_catalog_system
from yunohost.domain import domain_add, domain_list from yunohost.domain import domain_add
from yunohost.dyndns import _dyndns_available, _dyndns_provides from yunohost.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp from yunohost.firewall import firewall_upnp
from yunohost.service import service_start, service_enable from yunohost.service import service_start, service_enable
@ -53,9 +51,11 @@ MIGRATIONS_STATE_PATH = "/etc/yunohost/migrations.yaml"
logger = getActionLogger('yunohost.tools') logger = getActionLogger('yunohost.tools')
def tools_versions(): def tools_versions():
return ynh_packages_version() return ynh_packages_version()
def tools_ldapinit(): def tools_ldapinit():
""" """
YunoHost LDAP initialization YunoHost LDAP initialization
@ -118,7 +118,7 @@ def tools_ldapinit():
if not os.path.isdir('/home/{0}'.format("admin")): if not os.path.isdir('/home/{0}'.format("admin")):
logger.warning(m18n.n('user_home_creation_failed'), logger.warning(m18n.n('user_home_creation_failed'),
exc_info=1) exc_info=1)
logger.success(m18n.n('ldap_initialized')) logger.success(m18n.n('ldap_initialized'))
@ -148,7 +148,7 @@ def tools_adminpw(new_password, check_strength=True):
ldap = _get_ldap_interface() ldap = _get_ldap_interface()
try: try:
ldap.update("cn=admin", {"userPassword": [ new_hash ], }) ldap.update("cn=admin", {"userPassword": [new_hash], })
except: except:
logger.exception('unable to change admin password') logger.exception('unable to change admin password')
raise YunohostError('admin_password_change_failed') raise YunohostError('admin_password_change_failed')
@ -601,7 +601,6 @@ def tools_upgrade(operation_logger, apps=None, system=False, allow_yunohost_upgr
logger.debug("Running apt command :\n{}".format(dist_upgrade)) logger.debug("Running apt command :\n{}".format(dist_upgrade))
def is_relevant(l): def is_relevant(l):
irrelevants = [ irrelevants = [
"service sudo-ldap already provided", "service sudo-ldap already provided",
@ -938,7 +937,7 @@ def _migrate_legacy_migration_json():
# Extract the list of migration ids # Extract the list of migration ids
from . import data_migrations from . import data_migrations
migrations_path = data_migrations.__path__[0] migrations_path = data_migrations.__path__[0]
migration_files = filter(lambda x: re.match("^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)) migration_files = filter(lambda x: re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path))
# (here we remove the .py extension and make sure the ids are sorted) # (here we remove the .py extension and make sure the ids are sorted)
migration_ids = sorted([f.rsplit(".", 1)[0] for f in migration_files]) migration_ids = sorted([f.rsplit(".", 1)[0] for f in migration_files])
@ -987,7 +986,7 @@ def _get_migrations_list():
# (in particular, pending migrations / not already ran are not listed # (in particular, pending migrations / not already ran are not listed
states = tools_migrations_state()["migrations"] states = tools_migrations_state()["migrations"]
for migration_file in filter(lambda x: re.match("^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)): for migration_file in filter(lambda x: re.match(r"^\d+_[a-zA-Z0-9_]+\.py$", x), os.listdir(migrations_path)):
m = _load_migration(migration_file) m = _load_migration(migration_file)
m.state = states.get(m.id, "pending") m.state = states.get(m.id, "pending")
migrations.append(m) migrations.append(m)
@ -1006,7 +1005,7 @@ def _get_migration_by_name(migration_name):
raise AssertionError("Unable to find migration with name %s" % migration_name) raise AssertionError("Unable to find migration with name %s" % migration_name)
migrations_path = data_migrations.__path__[0] migrations_path = data_migrations.__path__[0]
migrations_found = filter(lambda x: re.match("^\d+_%s\.py$" % migration_name, x), os.listdir(migrations_path)) migrations_found = filter(lambda x: re.match(r"^\d+_%s\.py$" % migration_name, x), os.listdir(migrations_path))
assert len(migrations_found) == 1, "Unable to find migration with name %s" % migration_name assert len(migrations_found) == 1, "Unable to find migration with name %s" % migration_name
@ -1050,7 +1049,7 @@ class Migration(object):
# Those are to be implemented by daughter classes # Those are to be implemented by daughter classes
mode = "auto" mode = "auto"
dependencies = [] # List of migration ids required before running this migration dependencies = [] # List of migration ids required before running this migration
@property @property
def disclaimer(self): def disclaimer(self):

View file

@ -27,16 +27,14 @@ import os
import re import re
import pwd import pwd
import grp import grp
import json
import crypt import crypt
import random import random
import string import string
import subprocess import subprocess
import copy import copy
from moulinette import m18n from moulinette import msignals, msettings, m18n
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_json, write_to_json, read_yaml, write_to_yaml
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from yunohost.service import service_status from yunohost.service import service_status
@ -46,16 +44,7 @@ logger = getActionLogger('yunohost.user')
def user_list(fields=None): def user_list(fields=None):
"""
List users
Keyword argument:
filter -- LDAP filter used to search
offset -- Starting number for user fetching
limit -- Maximum number of user fetched
fields -- fields to fetch
"""
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
user_attrs = { user_attrs = {
@ -105,20 +94,9 @@ def user_list(fields=None):
@is_unit_operation([('username', 'user')]) @is_unit_operation([('username', 'user')])
def user_create(operation_logger, username, firstname, lastname, mail, password, def user_create(operation_logger, username, firstname, lastname, domain, password,
mailbox_quota="0"): mailbox_quota="0", mail=None):
"""
Create user
Keyword argument:
firstname
lastname
username -- Must be unique
mail -- Main mail address must be unique
password
mailbox_quota -- Mailbox size quota
"""
from yunohost.domain import domain_list, _get_maindomain from yunohost.domain import domain_list, _get_maindomain
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.password import assert_password_is_strong_enough
@ -127,6 +105,30 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
# Ensure sufficiently complex password # Ensure sufficiently complex password
assert_password_is_strong_enough("user", password) assert_password_is_strong_enough("user", password)
if mail is not None:
logger.warning("Packagers ! Using --mail in 'yunohost user create' is deprecated ... please use --domain instead.")
domain = mail.split("@")[-1]
# Validate domain used for email address/xmpp account
if domain is None:
if msettings.get('interface') == 'api':
raise YunohostError('Invalide usage, specify domain argument')
else:
# On affiche les differents domaines possibles
msignals.display(m18n.n('domains_available'))
for domain in domain_list()['domains']:
msignals.display("- {}".format(domain))
maindomain = _get_maindomain()
domain = msignals.prompt(m18n.n('ask_user_domain') + ' (default: %s)' % maindomain)
if not domain:
domain = maindomain
# Check that the domain exists
if domain not in domain_list()['domains']:
raise YunohostError('domain_unknown', domain)
mail = username + '@' + domain
ldap = _get_ldap_interface() ldap = _get_ldap_interface()
if username in user_list()["users"]: if username in user_list()["users"]:
@ -158,10 +160,6 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
if mail in aliases: if mail in aliases:
raise YunohostError('mail_unavailable') raise YunohostError('mail_unavailable')
# Check that the mail domain exists
if mail.split("@")[1] not in domain_list()['domains']:
raise YunohostError('mail_domain_unknown', domain=mail.split("@")[1])
operation_logger.start() operation_logger.start()
# Get random UID/GID # Get random UID/GID
@ -176,6 +174,7 @@ def user_create(operation_logger, username, firstname, lastname, mail, password,
# Adapt values for LDAP # Adapt values for LDAP
fullname = '%s %s' % (firstname, lastname) fullname = '%s %s' % (firstname, lastname)
attr_dict = { attr_dict = {
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'], 'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount', 'userPermissionYnh'],
'givenName': [firstname], 'givenName': [firstname],
@ -239,7 +238,6 @@ def user_delete(operation_logger, username, purge=False):
""" """
from yunohost.hook import hook_callback from yunohost.hook import hook_callback
from yunohost.utils.ldap import _get_ldap_interface from yunohost.utils.ldap import _get_ldap_interface
from yunohost.permission import permission_sync_to_user
if username not in user_list()["users"]: if username not in user_list()["users"]:
raise YunohostError('user_unknown', user=username) raise YunohostError('user_unknown', user=username)

View file

@ -33,8 +33,8 @@ class YunohostError(MoulinetteError):
""" """
def __init__(self, key, raw_msg=False, *args, **kwargs): def __init__(self, key, raw_msg=False, *args, **kwargs):
self.key = key # Saving the key is useful for unit testing self.key = key # Saving the key is useful for unit testing
self.kwargs = kwargs # Saving the key is useful for unit testing self.kwargs = kwargs # Saving the key is useful for unit testing
if raw_msg: if raw_msg:
msg = key msg = key
else: else:

View file

@ -29,19 +29,20 @@ from yunohost.utils.error import YunohostError
# to avoid re-authenticating in case we call _get_ldap_authenticator multiple times # to avoid re-authenticating in case we call _get_ldap_authenticator multiple times
_ldap_interface = None _ldap_interface = None
def _get_ldap_interface(): def _get_ldap_interface():
global _ldap_interface global _ldap_interface
if _ldap_interface is None: if _ldap_interface is None:
conf = { "vendor": "ldap", conf = {"vendor": "ldap",
"name": "as-root", "name": "as-root",
"parameters": { 'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi', "parameters": {'uri': 'ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi',
'base_dn': 'dc=yunohost,dc=org', 'base_dn': 'dc=yunohost,dc=org',
'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth' }, 'user_rdn': 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth'},
"extra": {} "extra": {}
} }
try: try:
_ldap_interface = ldap.Authenticator(**conf) _ldap_interface = ldap.Authenticator(**conf)
@ -82,4 +83,5 @@ def _destroy_ldap_interface():
if _ldap_interface is not None: if _ldap_interface is not None:
del _ldap_interface del _ldap_interface
atexit.register(_destroy_ldap_interface) atexit.register(_destroy_ldap_interface)

View file

@ -58,6 +58,7 @@ def get_public_ip_from_remote_server(protocol=4):
# If we are indeed connected in ipv4 or ipv6, we should find a default route # If we are indeed connected in ipv4 or ipv6, we should find a default route
routes = check_output("ip -%s route show table all" % protocol).split("\n") routes = check_output("ip -%s route show table all" % protocol).split("\n")
def is_default_route(r): def is_default_route(r):
# Typically the default route starts with "default" # Typically the default route starts with "default"
# But of course IPv6 is more complex ... e.g. on internet cube there's # But of course IPv6 is more complex ... e.g. on internet cube there's

View file

@ -63,7 +63,7 @@ class PasswordValidator(object):
settings = json.load(open('/etc/yunohost/settings.json', "r")) settings = json.load(open('/etc/yunohost/settings.json', "r"))
setting_key = "security.password." + profile + ".strength" setting_key = "security.password." + profile + ".strength"
self.validation_strength = int(settings[setting_key]["value"]) self.validation_strength = int(settings[setting_key]["value"])
except Exception as e: except Exception:
# Fallback to default value if we can't fetch settings for some reason # Fallback to default value if we can't fetch settings for some reason
self.validation_strength = 1 self.validation_strength = 1
@ -83,11 +83,7 @@ class PasswordValidator(object):
# on top (at least not the moulinette ones) # on top (at least not the moulinette ones)
# because the moulinette needs to be correctly initialized # because the moulinette needs to be correctly initialized
# as well as modules available in python's path. # as well as modules available in python's path.
import logging
from yunohost.utils.error import YunohostError from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
logger = logging.getLogger('yunohost.utils.password')
status, msg = self.validation_summary(password) status, msg = self.validation_summary(password)
if status == "error": if status == "error":

View file

@ -10,6 +10,7 @@ from yunohost.utils.error import YunohostError
logger = logging.getLogger('yunohost.utils.yunopaste') logger = logging.getLogger('yunohost.utils.yunopaste')
def yunopaste(data): def yunopaste(data):
paste_server = "https://paste.yunohost.org" paste_server = "https://paste.yunohost.org"
@ -44,7 +45,6 @@ def anonymize(data):
data = data.replace(domain.replace(".", "%2e"), redact.replace(".", "%2e")) data = data.replace(domain.replace(".", "%2e"), redact.replace(".", "%2e"))
return data return data
# First, let's replace every occurence of the main domain by "domain.tld" # First, let's replace every occurence of the main domain by "domain.tld"
# This should cover a good fraction of the info leaked # This should cover a good fraction of the info leaked
main_domain = _get_maindomain() main_domain = _get_maindomain()

View file

@ -48,7 +48,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# helper function - make signed requests # helper function - make signed requests
def _send_signed_request(url, payload, err_msg, depth=0): def _send_signed_request(url, payload, err_msg, depth=0):
payload64 = _b64(json.dumps(payload).encode('utf8')) payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8'))
new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce'] new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce']
protected = {"url": url, "alg": alg, "nonce": new_nonce} protected = {"url": url, "alg": alg, "nonce": new_nonce}
protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']}) protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']})
@ -63,12 +63,12 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# helper function - poll until complete # helper function - poll until complete
def _poll_until_not(url, pending_statuses, err_msg): def _poll_until_not(url, pending_statuses, err_msg):
while True: result, t0 = None, time.time()
result, _, _ = _do_request(url, err_msg=err_msg) while result is None or result['status'] in pending_statuses:
if result['status'] in pending_statuses: assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout
time.sleep(2) time.sleep(0 if result is None else 2)
continue result, _, _ = _send_signed_request(url, None, err_msg)
return result return result
# parse account key to get public key # parse account key to get public key
log.info("Parsing account key...") log.info("Parsing account key...")
@ -93,7 +93,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8')) common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
if common_name is not None: if common_name is not None:
domains.add(common_name.group(1)) domains.add(common_name.group(1))
subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) subject_alt_names = re.search(r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
if subject_alt_names is not None: if subject_alt_names is not None:
for san in subject_alt_names.group(1).split(", "): for san in subject_alt_names.group(1).split(", "):
if san.startswith("DNS:"): if san.startswith("DNS:"):
@ -123,7 +123,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# get the authorizations that need to be completed # get the authorizations that need to be completed
for auth_url in order['authorizations']: for auth_url in order['authorizations']:
authorization, _, _ = _do_request(auth_url, err_msg="Error getting challenges") authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges")
domain = authorization['identifier']['value'] domain = authorization['identifier']['value']
log.info("Verifying {0}...".format(domain)) log.info("Verifying {0}...".format(domain))
@ -138,9 +138,8 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
# check that the file is in place # check that the file is in place
try: try:
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token)
assert(disable_check or _do_request(wellknown_url)[0] == keyauthorization) assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization)
except (AssertionError, ValueError) as e: except (AssertionError, ValueError) as e:
os.remove(wellknown_path)
raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e)) raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e))
# say the challenge is done # say the challenge is done
@ -148,6 +147,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain)) authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain))
if authorization['status'] != "valid": if authorization['status'] != "valid":
raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization)) raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization))
os.remove(wellknown_path)
log.info("{0} verified!".format(domain)) log.info("{0} verified!".format(domain))
# finalize the order with the csr # finalize the order with the csr
@ -161,7 +161,7 @@ def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check
raise ValueError("Order failed: {0}".format(order)) raise ValueError("Order failed: {0}".format(order))
# download the certificate # download the certificate
certificate_pem, _, _ = _do_request(order['certificate'], err_msg="Certificate download failed") certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed")
log.info("Certificate signed!") log.info("Certificate signed!")
return certificate_pem return certificate_pem

View file

@ -1,4 +1,3 @@
import re
import json import json
import glob import glob
from collections import OrderedDict from collections import OrderedDict
@ -14,6 +13,6 @@ for locale_file in locale_files:
print(locale_file) print(locale_file)
this_locale = json.loads(open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict) this_locale = json.loads(open(locale_folder + locale_file).read(), object_pairs_hook=OrderedDict)
this_locale_fixed = {k:v for k, v in this_locale.items() if k in reference} this_locale_fixed = {k: v for k, v in this_locale.items() if k in reference}
json.dump(this_locale_fixed, open(locale_folder + locale_file, "w"), indent=4, ensure_ascii=False) json.dump(this_locale_fixed, open(locale_folder + locale_file, "w"), indent=4, ensure_ascii=False)

View file

@ -1,4 +1,5 @@
import yaml import yaml
def test_yaml_syntax(): def test_yaml_syntax():
yaml.load(open("data/actionsmap/yunohost.yml")) yaml.load(open("data/actionsmap/yunohost.yml"))

View file

@ -23,8 +23,10 @@ def find_expected_string_keys():
# Try to find : # Try to find :
# m18n.n( "foo" # m18n.n( "foo"
# YunohostError("foo" # YunohostError("foo"
# # i18n: foo
p1 = re.compile(r'm18n\.n\(\s*[\"\'](\w+)[\"\']') p1 = re.compile(r'm18n\.n\(\s*[\"\'](\w+)[\"\']')
p2 = re.compile(r'YunohostError\([\'\"](\w+)[\'\"]') p2 = re.compile(r'YunohostError\([\'\"](\w+)[\'\"]')
p3 = re.compile(r'# i18n: [\'\"]?(\w+)[\'\"]?')
python_files = glob.glob("src/yunohost/*.py") python_files = glob.glob("src/yunohost/*.py")
python_files.extend(glob.glob("src/yunohost/utils/*.py")) python_files.extend(glob.glob("src/yunohost/utils/*.py"))
@ -42,6 +44,8 @@ def find_expected_string_keys():
if m.endswith("_"): if m.endswith("_"):
continue continue
yield m yield m
for m in p3.findall(content):
yield m
# For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries) # For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
# Also we expect to have "diagnosis_description_<name>" for each diagnosis # Also we expect to have "diagnosis_description_<name>" for each diagnosis
@ -112,7 +116,7 @@ def find_expected_string_keys():
# Hardcoded expected keys ... # Hardcoded expected keys ...
yield "admin_password" # Not sure that's actually used nowadays... yield "admin_password" # Not sure that's actually used nowadays...
for method in ["tar", "copy", "borg", "custom"]: for method in ["tar", "copy", "custom"]:
yield "backup_applying_method_%s" % method yield "backup_applying_method_%s" % method
yield "backup_method_%s_finished" % method yield "backup_method_%s_finished" % method

View file

@ -7,6 +7,6 @@ deps =
py{27,37}-{lint,invalidcode}: flake8 py{27,37}-{lint,invalidcode}: flake8
py37-black: black py37-black: black
commands = commands =
py{27,37}-lint: flake8 src doc data tests py{27,37}-lint: flake8 src doc data tests --ignore E402,E501 --exclude src/yunohost/vendor
py{27,37}-invalidcode: flake8 src data --exclude src/yunohost/tests --select F --ignore F401,F841 py{27,37}-invalidcode: flake8 src data --exclude src/yunohost/tests,src/yunohost/vendor --select F
py37-black: black --check --diff src doc data tests py37-black: black --check --diff src doc data tests