diff --git a/.gitignore b/.gitignore index 726bba63a..6dd427aba 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,4 @@ pip-log.txt .mr.developer.cfg # moulinette lib -lib/yunohost/locales +src/yunohost/locales diff --git a/README.md b/README.md new file mode 100644 index 000000000..6e928e9ce --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +Please report issues here (no registration needed): +https://dev.yunohost.org/projects/yunohost/issues diff --git a/bin/yunohost b/bin/yunohost index 6800a4a9c..3f947364b 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -7,27 +7,28 @@ import os # Either we are in a development environment or not IN_DEVEL = False -# Either cache has to be used inside the moulinette or not -USE_CACHE = True - -# Either the output has to be encoded as a JSON encoded string or not -PRINT_JSON = False - -# Either the output has to printed for scripting usage or not -PRINT_PLAIN = False - # Level for which loggers will log LOGGERS_LEVEL = 'INFO' +TTY_LOG_LEVEL = 'SUCCESS' # Handlers that will be used by loggers # - file: log to the file LOG_DIR/LOG_FILE -# - console: log to stderr -LOGGERS_HANDLERS = ['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') + # Initialization & helpers functions ----------------------------------- @@ -40,83 +41,121 @@ def _die(message, title='Error:'): print('%s %s' % (colorize(title, 'red'), message)) sys.exit(1) -def _check_in_devel(): - """Check and load if needed development environment""" - global IN_DEVEL, LOG_DIR - basedir = os.path.abspath('%s/../' % os.path.dirname(__file__)) - if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL: - # Add base directory to python path - sys.path.insert(0, basedir) +def _parse_cli_args(): + """Parse additional arguments for the cli""" + import argparse - # Update global variables - IN_DEVEL = True - LOG_DIR = '%s/log' % basedir + 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', + choices=['json', 'plain'], default=None, + help="Output result in another format", + ) + parser.add_argument('--debug', + action='store_true', default=False, + help="Log and print debug messages", + ) + parser.add_argument('--verbose', + action='store_true', default=False, + help="Be more verbose in the output", + ) + parser.add_argument('--quiet', + action='store_true', default=False, + help="Don't produce any output", + ) + parser.add_argument('--admin-password', + default=None, dest='password', metavar='PASSWORD', + help="The admin password to use to authenticate", + ) + # deprecated arguments + parser.add_argument('--plain', + action='store_true', default=False, help=argparse.SUPPRESS + ) + parser.add_argument('--json', + action='store_true', default=False, help=argparse.SUPPRESS + ) -def _parse_argv(): - """Parse additional arguments and return remaining ones""" - argv = list(sys.argv) - argv.pop(0) + opts, args = parser.parse_known_args() - if '--no-cache' in argv: - global USE_CACHE - USE_CACHE = False - argv.remove('--no-cache') - if '--json' in argv: - global PRINT_JSON - PRINT_JSON = True - argv.remove('--json') - if '--plain' in argv: - global PRINT_PLAIN - PRINT_PLAIN = True - argv.remove('--plain') - if '--debug' in argv: - global LOGGERS_LEVEL - LOGGERS_LEVEL = 'DEBUG' - argv.remove('--debug') - if '--verbose' in argv: - global LOGGERS_HANDLERS - if 'console' not in LOGGERS_HANDLERS: - LOGGERS_HANDLERS.append('console') - argv.remove('--verbose') - return argv + # output compatibility + if opts.plain: + opts.output_as = 'plain' + elif opts.json: + opts.output_as = 'json' -def _init_moulinette(): + return (parser, opts, args) + +def _init_moulinette(debug=False, verbose=False, quiet=False): """Configure logging and initialize the moulinette""" from moulinette import init + # Define loggers handlers + handlers = set(LOGGERS_HANDLERS) + if quiet and 'tty' in handlers: + handlers.remove('tty') + elif verbose and 'tty' not in handlers: + handlers.append('tty') + + root_handlers = set(handlers) + if not debug: + root_handlers.remove('tty') + + # Define loggers level + level = LOGGERS_LEVEL + tty_level = TTY_LOG_LEVEL + if verbose: + tty_level = 'INFO' + if debug: + tty_level = level = 'DEBUG' + # Custom logging configuration logging = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { - 'simple': { - 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s' + 'tty-debug': { + 'format': '%(relativeCreated)-4d %(fmessage)s' }, 'precise': { - 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s' + 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + }, + }, + 'filters': { + 'action': { + '()': 'moulinette.utils.log.ActionFilter', }, }, 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - 'stream': 'ext://sys.stderr', + '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': { - 'moulinette': { - 'level': LOGGERS_LEVEL, - 'handlers': LOGGERS_HANDLERS, - }, 'yunohost': { - 'level': LOGGERS_LEVEL, - 'handlers': LOGGERS_HANDLERS, + 'level': level, + 'handlers': handlers, + 'propagate': False, }, + 'moulinette': { + 'level': level, + 'handlers': [], + 'propagate': True, + }, + }, + 'root': { + 'level': level, + 'handlers': root_handlers, }, } @@ -144,9 +183,8 @@ def _retrieve_namespaces(): # Main action ---------------------------------------------------------- if __name__ == '__main__': - _check_in_devel() - args = _parse_argv() - _init_moulinette() + parser, opts, args = _parse_cli_args() + _init_moulinette(opts.debug, opts.verbose, opts.quiet) # Check that YunoHost is installed if not os.path.isfile('/etc/yunohost/installed') and \ @@ -163,6 +201,8 @@ if __name__ == '__main__': # Execute the action from moulinette import cli - ret = cli(_retrieve_namespaces(), args, use_cache=USE_CACHE, - print_json=PRINT_JSON, print_plain=PRINT_PLAIN) + ret = cli(_retrieve_namespaces(), args, + use_cache=opts.use_cache, output_as=opts.output_as, + password=opts.password, parser_kwargs={'top_parser': parser} + ) sys.exit(ret) diff --git a/bin/yunohost-api b/bin/yunohost-api index 84f38c661..470f61c66 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -7,24 +7,32 @@ import os.path # Either we are in a development environment or not IN_DEVEL = False -# Either cache has to be used inside the moulinette or not -USE_CACHE = True - -# Either WebSocket has to be installed by the moulinette or not -USE_WEBSOCKET = True +# Default server configuration +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 6787 # Level for which loggers will log LOGGERS_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'] +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') + # Initialization & helpers functions ----------------------------------- @@ -37,79 +45,112 @@ def _die(message, title='Error:'): print('%s %s' % (colorize(title, 'red'), message)) sys.exit(1) -def _check_in_devel(): - """Check and load if needed development environment""" - global IN_DEVEL, LOG_DIR - basedir = os.path.abspath('%s/../' % os.path.dirname(__file__)) - if os.path.isdir('%s/moulinette' % basedir) and not IN_DEVEL: - # Add base directory to python path - sys.path.insert(0, basedir) +def _parse_api_args(): + """Parse main arguments for the api""" + import argparse - # Update global variables - IN_DEVEL = True - LOG_DIR = '%s/log' % basedir + parser = argparse.ArgumentParser(add_help=False, + description="Run the YunoHost API to manage your server.", + ) + srv_group = parser.add_argument_group('server configuration') + srv_group.add_argument('-h', '--host', + action='store', default=DEFAULT_HOST, + help="Host to listen on (default: %s)" % DEFAULT_HOST, + ) + srv_group.add_argument('-p', '--port', + action='store', default=DEFAULT_PORT, type=int, + 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.add_argument('--no-cache', + action='store_false', default=True, dest='use_cache', + help="Don't use actions map cache", + ) + glob_group.add_argument('--debug', + action='store_true', default=False, + 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', + action='help', help="Show this help message and exit", + ) -def _parse_argv(): - """Parse additional arguments and return remaining ones""" - argv = list(sys.argv) - argv.pop(0) + return parser.parse_args() - if '--no-cache' in argv: - global USE_CACHE - USE_CACHE = False - argv.remove('--no-cache') - if '--no-websocket' in argv: - global USE_WEBSOCKET - USE_WEBSOCKET = False - argv.remove('--no-websocket') - if '--debug' in argv: - global LOGGERS_LEVEL - LOGGERS_LEVEL = 'DEBUG' - argv.remove('--debug') - if '--verbose' in argv: - global LOGGERS_HANDLERS - if 'console' not in LOGGERS_HANDLERS: - LOGGERS_HANDLERS.append('console') - argv.remove('--verbose') - return argv - -def _init_moulinette(): +def _init_moulinette(use_websocket=True, debug=False, verbose=False): """Configure logging and initialize the moulinette""" from moulinette import init + # 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 + if debug: + level = 'DEBUG' + # Custom logging configuration logging = { 'version': 1, 'disable_existing_loggers': True, 'formatters': { - 'simple': { - 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s - %(message)s' + 'console': { + 'format': '%(relativeCreated)-5d %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' }, 'precise': { - 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(message)s' + 'format': '%(asctime)-15s %(levelname)-8s %(name)s %(funcName)s - %(fmessage)s' + }, + }, + 'filters': { + 'action': { + '()': 'moulinette.utils.log.ActionFilter', }, }, 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - 'stream': 'ext://sys.stderr', + 'api': { + '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': { - 'moulinette': { - 'level': LOGGERS_LEVEL, - 'handlers': LOGGERS_HANDLERS, - }, 'yunohost': { - 'level': LOGGERS_LEVEL, - 'handlers': LOGGERS_HANDLERS, + 'level': level, + 'handlers': handlers, + 'propagate': False, }, + 'moulinette': { + 'level': level, + 'handlers': [], + 'propagate': True, + }, + }, + 'root': { + 'level': level, + 'handlers': root_handlers, }, } @@ -150,20 +191,18 @@ def is_installed(): # Main action ---------------------------------------------------------- if __name__ == '__main__': - _check_in_devel() - _parse_argv() - _init_moulinette() + opts = _parse_api_args() + _init_moulinette(opts.use_websocket, opts.debug, opts.verbose) - from moulinette import (api, MoulinetteError) + # Run the server + from moulinette import api, MoulinetteError from yunohost import get_versions - try: - # Run the server - api(_retrieve_namespaces(), port=6787, + ret = api(_retrieve_namespaces(), + host=opts.host, port=opts.port, routes={ ('GET', '/installed'): is_installed, ('GET', '/version'): get_versions, }, - use_cache=USE_CACHE, use_websocket=USE_WEBSOCKET) - except MoulinetteError as e: - _die(e.strerror, m18n.g('error')) - sys.exit(0) + use_cache=opts.use_cache, use_websocket=opts.use_websocket + ) + sys.exit(ret) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 125858d35..80a74251b 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -291,6 +291,18 @@ domain: extra: pattern: *pattern_domain + ### domain_dns_conf() + dns-conf: + action_help: Generate DNS configuration for a domain + api: GET /domains//dns + configuration: + lock: false + authenticate: + - api + arguments: + domain: + help: Target domain + ### domain_info() # info: # action_help: Get domain informations @@ -318,10 +330,10 @@ app: arguments: -u: full: --url - help: URL of remote JSON list (default http://fapp.yunohost.org/app/list/raw) + help: URL of remote JSON list (default https://yunohost.org/official.json) -n: full: --name - help: Name of the list (default fapp) + help: Name of the list (default yunohost) extra: pattern: &pattern_listname - !!str ^[a-z0-9_]+$ @@ -505,6 +517,14 @@ app: full: --sql help: Initial SQL file + ### app_debug() + debug: + action_help: Display all debug informations for an application + api: GET /apps//debug + arguments: + app: + help: App name + ### app_makedefault() makedefault: action_help: Redirect domain root to an app @@ -1009,6 +1029,10 @@ firewall: reload: action_help: Reload all firewall rules api: PUT /firewall + arguments: + --skip-upnp: + help: Do not refresh port forwarding using UPnP + action: store_true ### firewall_allow() allow: @@ -1189,9 +1213,12 @@ tools: ### tools_maindomain() maindomain: action_help: Main domain change tool - api: PUT /domains/main + api: + - GET /domains/main + - PUT /domains/main configuration: authenticate: all + lock: false arguments: -o: full: --old-domain @@ -1284,6 +1311,16 @@ hook: app: help: Scripts related to app will be removed + ### hook_info() + info: + action_help: Get information about a given hook + api: GET /hooks// + arguments: + action: + help: Action name + name: + help: Hook name + ### hook_list() list: action_help: List available hooks for an action @@ -1320,21 +1357,20 @@ hook: help: Ordered list of arguments to pass to the script nargs: "*" - ### hook_check() - check: - action_help: Parse the script file and get arguments - api: GET /hook/check - arguments: - file: - help: File to check - ### hook_exec() exec: action_help: Execute hook from a file with arguments api: GET /hook arguments: - file: - help: Script to execute + path: + help: Path of the script to execute -a: full: --args help: Arguments to pass to the script + --raise-on-error: + help: Raise if the script returns a non-zero exit code + action: store_true + -q: + full: --no-trace + help: Do not print each command that will be executed + action: store_true diff --git a/data/apps/helpers.d/filesystem b/data/apps/helpers.d/filesystem index c3332a794..087a9e944 100644 --- a/data/apps/helpers.d/filesystem +++ b/data/apps/helpers.d/filesystem @@ -27,3 +27,15 @@ ynh_bind_or_cp() { fi $SUDO_CMD cp -r "$SRCDIR" "$DESTDIR" } + +# Create a directory under /tmp +# +# usage: ynh_mkdir_tmp +# | ret: the created directory path +ynh_mkdir_tmp() { + TMPDIR="/tmp/$(ynh_string_random 6)" + while [ -d $TMPDIR ]; do + TMPDIR="/tmp/$(ynh_string_random 6)" + done + mkdir -p "$TMPDIR" && echo "$TMPDIR" +} diff --git a/data/apps/helpers.d/mysql b/data/apps/helpers.d/mysql new file mode 100644 index 000000000..d4b655aa7 --- /dev/null +++ b/data/apps/helpers.d/mysql @@ -0,0 +1,92 @@ +MYSQL_ROOT_PWD_FILE=/etc/yunohost/mysql + +# Open a connection as a user +# +# example: ynh_mysql_connect_as 'user' 'pass' <<< "UPDATE ...;" +# example: ynh_mysql_connect_as 'user' 'pass' < /path/to/file.sql +# +# usage: ynh_mysql_connect_as user pwd [db] +# | arg: user - the user name to connect as +# | arg: pwd - the user password +# | arg: db - the database to connect to +ynh_mysql_connect_as() { + mysql -u "$1" --password="$2" -B "${3:-}" +} + +# Execute a command as root user +# +# usage: ynh_mysql_execute_as_root sql [db] +# | arg: sql - the SQL command to execute +# | arg: db - the database to connect to +ynh_mysql_execute_as_root() { + ynh_mysql_connect_as "root" "$(sudo cat $MYSQL_ROOT_PWD_FILE)" \ + "${2:-}" <<< "$1" +} + +# Execute a command from a file as root user +# +# usage: ynh_mysql_execute_file_as_root sql [db] +# | arg: file - the file containing SQL commands +# | arg: db - the database to connect to +ynh_mysql_execute_file_as_root() { + ynh_mysql_connect_as "root" "$(sudo cat $MYSQL_ROOT_PWD_FILE)" \ + "${2:-}" < "$1" +} + +# Create a database and grant optionnaly privilegies to a user +# +# usage: ynh_mysql_create_db db [user [pwd]] +# | arg: db - the database name to create +# | arg: user - the user to grant privilegies +# | arg: pwd - the password to identify user by +ynh_mysql_create_db() { + db=$1 + + sql="CREATE DATABASE ${db};" + + # grant all privilegies to user + if [[ $# -gt 1 ]]; then + sql+=" GRANT ALL PRIVILEGES ON ${db}.* TO '${2}'@'localhost'" + [[ -n ${3:-} ]] && sql+=" IDENTIFIED BY '${3}'" + sql+=" WITH GRANT OPTION;" + fi + + ynh_mysql_execute_as_root "$sql" +} + +# Drop a database +# +# usage: ynh_mysql_drop_db db +# | arg: db - the database name to drop +ynh_mysql_drop_db() { + ynh_mysql_execute_as_root "DROP DATABASE ${1};" +} + +# Dump a database +# +# example: ynh_mysql_dump_db 'roundcube' > ./dump.sql +# +# usage: ynh_mysql_dump_db db +# | arg: db - the database name to dump +# | ret: the mysqldump output +ynh_mysql_dump_db() { + mysqldump -u "root" -p"$(sudo cat $MYSQL_ROOT_PWD_FILE)" "$1" +} + +# Create a user +# +# usage: ynh_mysql_create_user user pwd [host] +# | arg: user - the user name to create +# | arg: pwd - the password to identify user by +ynh_mysql_create_user() { + ynh_mysql_execute_as_root \ + "CREATE USER '${1}'@'localhost' IDENTIFIED BY '${2}';" +} + +# Drop a user +# +# usage: ynh_mysql_drop_user user +# | arg: user - the user name to drop +ynh_mysql_drop_user() { + ynh_mysql_execute_as_root "DROP USER '${1}'@'localhost';" +} diff --git a/data/apps/helpers.d/package b/data/apps/helpers.d/package new file mode 100644 index 000000000..dd78692f3 --- /dev/null +++ b/data/apps/helpers.d/package @@ -0,0 +1,88 @@ +# Check either a package is installed or not +# +# example: ynh_package_is_installed 'yunohost' && echo "ok" +# +# usage: ynh_package_is_installed name +# | arg: name - the package name to check +ynh_package_is_installed() { + dpkg-query -W -f '${Status}' "$1" 2>/dev/null \ + | grep -c "ok installed" &>/dev/null +} + +# Get the version of an installed package +# +# example: version=$(ynh_package_version 'yunohost') +# +# usage: ynh_package_version name +# | arg: name - the package name to get version +# | ret: the version or an empty string +ynh_package_version() { + if ynh_package_is_installed "$1"; then + dpkg-query -W -f '${Version}' "$1" 2>/dev/null + else + echo '' + fi +} + +# Update package index files +# +# usage: ynh_package_update +ynh_package_update() { + sudo apt-get -y -qq update +} + +# Install package(s) +# +# usage: ynh_package_install name [name [...]] +# | arg: name - the package name to install +ynh_package_install() { + sudo apt-get -y -qq install $@ +} + +# Build and install a package from an equivs control file +# +# example: generate an empty control file with `equivs-control`, adjust its +# content and use helper to build and install the package: +# ynh_package_install_from_equivs /path/to/controlfile +# +# usage: ynh_package_install_from_equivs controlfile +# | arg: controlfile - path of the equivs control file +ynh_package_install_from_equivs() { + ynh_package_is_installed 'equivs' \ + || ynh_package_install equivs + + # retrieve package information + pkgname=$(grep '^Package: ' $1 | cut -d' ' -f 2) + pkgversion=$(grep '^Version: ' $1 | cut -d' ' -f 2) + [[ -z "$pkgname" || -z "$pkgversion" ]] \ + && echo "Invalid control file" && exit 1 + controlfile=$(readlink -f "$1") + + # update packages cache + ynh_package_update + + # build and install the package + TMPDIR=$(ynh_mkdir_tmp) + (cd $TMPDIR \ + && equivs-build "$controlfile" 1>/dev/null \ + && sudo dpkg --force-depends \ + -i "./${pkgname}_${pkgversion}_all.deb" 2>&1 \ + && sudo apt-get -f -y -qq install) \ + && ([[ -n "$TMPDIR" ]] && rm -rf $TMPDIR) +} + +# Remove package(s) +# +# usage: ynh_package_remove name [name [...]] +# | arg: name - the package name to remove +ynh_package_remove() { + sudo apt-get -y -qq remove $@ +} + +# Remove package(s) and their uneeded dependencies +# +# usage: ynh_package_autoremove name [name [...]] +# | arg: name - the package name to remove +ynh_package_autoremove() { + sudo apt-get -y -qq autoremove $@ +} diff --git a/data/apps/helpers.d/password b/data/apps/helpers.d/password deleted file mode 100644 index 0087921c4..000000000 --- a/data/apps/helpers.d/password +++ /dev/null @@ -1,3 +0,0 @@ -ynh_password() { - echo $(dd if=/dev/urandom bs=1 count=200 2> /dev/null | tr -c -d '[A-Za-z0-9]' | sed -n 's/\(.\{24\}\).*/\1/p') -} diff --git a/data/apps/helpers.d/setting b/data/apps/helpers.d/setting new file mode 100644 index 000000000..70a81c84e --- /dev/null +++ b/data/apps/helpers.d/setting @@ -0,0 +1,27 @@ +# Get an application setting +# +# usage: ynh_app_setting_get app key +# | arg: app - the application id +# | arg: key - the setting to get +ynh_app_setting_get() { + sudo yunohost app setting "$1" "$2" --output-as plain +} + +# Set an application setting +# +# usage: ynh_app_setting_set app key value +# | arg: app - the application id +# | arg: key - the setting name to set +# | arg: value - the setting value to set +ynh_app_setting_set() { + sudo yunohost app setting "$1" "$2" -v "$3" +} + +# Delete an application setting +# +# usage: ynh_app_setting_delete app key +# | arg: app - the application id +# | arg: key - the setting to delete +ynh_app_setting_delete() { + sudo yunohost app setting -d "$1" "$2" +} diff --git a/data/apps/helpers.d/string b/data/apps/helpers.d/string new file mode 100644 index 000000000..a2bf0d463 --- /dev/null +++ b/data/apps/helpers.d/string @@ -0,0 +1,11 @@ +# Generate a random string +# +# example: pwd=$(ynh_string_random 8) +# +# usage: ynh_string_random [length] +# | arg: length - the string length to generate (default: 24) +ynh_string_random() { + dd if=/dev/urandom bs=1 count=200 2> /dev/null \ + | tr -c -d '[A-Za-z0-9]' \ + | sed -n 's/\(.\{'"${1:-24}"'\}\).*/\1/p' +} diff --git a/data/apps/helpers.d/user b/data/apps/helpers.d/user index 1fd8d203c..bfd044070 100644 --- a/data/apps/helpers.d/user +++ b/data/apps/helpers.d/user @@ -1,15 +1,29 @@ -# Check if a user exists +# Check if a YunoHost user exists +# +# example: ynh_user_exists 'toto' || exit 1 # # usage: ynh_user_exists username -# | ret: retcode - 0 if user exists, 1 otherwise +# | arg: username - the username to check ynh_user_exists() { - sudo yunohost user list --json | grep -q "\"username\": \"${1}\"" + sudo yunohost user list --output-as json | grep -q "\"username\": \"${1}\"" } -# Retrieve a user information +# Retrieve a YunoHost user information # -# usage: ynh_user_info username key +# example: mail=$(ynh_user_get_info 'toto' 'mail') +# +# usage: ynh_user_get_info username key +# | arg: username - the username to retrieve info from +# | arg: key - the key to retrieve # | ret: string - the key's value ynh_user_get_info() { - sudo yunohost user info "${1}" --plain | ynh_get_plain_key "${2}" + sudo yunohost user info "$1" --output-as plain | ynh_get_plain_key "$2" +} + +# Check if a user exists on the system +# +# usage: ynh_system_user_exists username +# | arg: username - the username to check +ynh_system_user_exists() { + getent passwd "$1" &>/dev/null } diff --git a/data/apps/helpers.d/utils b/data/apps/helpers.d/utils index 87cd47f6b..a6755e3c8 100644 --- a/data/apps/helpers.d/utils +++ b/data/apps/helpers.d/utils @@ -12,7 +12,7 @@ ynh_get_plain_key() { [[ "$line" =~ ^${prefix}[^#] ]] && return echo $line elif [[ "$line" =~ ^${prefix}${key}$ ]]; then - if [[ -n "$1" ]]; then + if [[ -n "${1:-}" ]]; then prefix+="#" key=$1 shift diff --git a/data/bash-completion.d/yunohost b/data/bash-completion.d/yunohost new file mode 100644 index 000000000..106f8fbdf --- /dev/null +++ b/data/bash-completion.d/yunohost @@ -0,0 +1,12 @@ +# +# Bash completion for yunohost +# + +_python_argcomplete() { + local IFS=' ' + COMPREPLY=( $(IFS="$IFS" COMP_LINE="$COMP_LINE" COMP_POINT="$COMP_POINT" _ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" _ARGCOMPLETE=1 "$1" 8>&1 9>&2 1>/dev/null 2>/dev/null) ) + if [[ $? != 0 ]]; then + unset COMPREPLY + fi +} +complete -o nospace -o default -F _python_argcomplete "yunohost" diff --git a/data/hooks/backup/05-conf_ldap b/data/hooks/backup/05-conf_ldap index a0c7b8c09..84ae2fb65 100644 --- a/data/hooks/backup/05-conf_ldap +++ b/data/hooks/backup/05-conf_ldap @@ -1,15 +1,16 @@ -backup_dir="$1/conf/ldap" -sudo mkdir -p $backup_dir +#!/bin/bash + +backup_dir="${1}/conf/ldap" +sudo mkdir -p "$backup_dir" # Fix for first jessie yunohost where slapd.conf is called slapd-yuno.conf # without slapcat doesn't work -if [ ! -f /etc/ldap/slapd.conf ] -then - sudo mv /etc/ldap/slapd-yuno.conf /etc/ldap/slapd.conf -fi +[[ ! -f /etc/ldap/slapd.conf ]] \ + && sudo mv /etc/ldap/slapd-yuno.conf /etc/ldap/slapd.conf -sudo cp -a /etc/ldap/slapd.conf $backup_dir/ +# Back up the configuration +sudo cp -a /etc/ldap/slapd.conf "${backup_dir}/slapd.conf" +sudo slapcat -b cn=config -l "${backup_dir}/cn=config.master.ldif" -sudo slapcat -l $backup_dir/slapcat.ldif.raw -sudo bash -c "egrep -v '^entryCSN:' < $backup_dir/slapcat.ldif.raw > $backup_dir/slapcat.ldif" -sudo rm -f $backup_dir/slapcat.ldif.raw +# Back up the database +sudo slapcat -b dc=yunohost,dc=org -l "${backup_dir}/dc=yunohost-dc=org.ldif" diff --git a/data/hooks/conf_regen/06-slapd b/data/hooks/conf_regen/06-slapd index 63cffac2e..d6a4eb28f 100644 --- a/data/hooks/conf_regen/06-slapd +++ b/data/hooks/conf_regen/06-slapd @@ -24,29 +24,49 @@ cd /usr/share/yunohost/templates/slapd || sudo yunohost service saferemove -s slapd \ /etc/ldap/slapd-yuno.conf +# Retrieve current backend +backend=$(sudo slapcat -n 0 | sed -n 's/^dn: olcDatabase={1}\(.*\),cn=config$/\1/p') + +# Save current database in case of a backend change +BACKEND_CHANGE=0 +BACKUP_DIR="/var/backups/dc=yunohost,dc=org-${backend}-$(date +%s)" +if [[ -n "$backend" && "$backend" != "mdb" && "$force" == "True" ]]; then + BACKEND_CHANGE=1 + sudo mkdir -p "$BACKUP_DIR" + sudo slapcat -b dc=yunohost,dc=org \ + -l "${BACKUP_DIR}/dc=yunohost-dc=org.ldif" +fi + safe_copy sudo.schema /etc/ldap/schema/sudo.schema safe_copy mailserver.schema /etc/ldap/schema/mailserver.schema safe_copy ldap.conf /etc/ldap/ldap.conf safe_copy slapd.default /etc/default/slapd - -# Compatibility: change from HDB to MDB on Jessie -version=$(sed 's/\..*//' /etc/debian_version) -if [[ "$version" == '8' ]]; then - cat slapd.conf \ - | sed "s/hdb$/mdb/g" \ - | sed "s/back_hdb/back_mdb/g" \ - | sed "s/^dbconfig set_/#dbconfig set_/g" \ - | sudo tee slapd.conf -fi - safe_copy slapd.conf /etc/ldap/slapd.conf + +# Fix some permissions sudo chown root:openldap /etc/ldap/slapd.conf -sudo rm -Rf /etc/ldap/slapd.d -sudo mkdir /etc/ldap/slapd.d sudo chown -R openldap:openldap /etc/ldap/schema/ sudo chown -R openldap:openldap /etc/ldap/slapd.d/ -sudo slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ -sudo chown -R openldap:openldap /etc/ldap/slapd.d/ +# Check the slapd config file at first +sudo slaptest -u -f /etc/ldap/slapd.conf + +if [[ $BACKEND_CHANGE -eq 1 ]]; then + # Regenerate LDAP config directory and import database as root + # since the admin user may be unavailable + sudo sh -c "rm -Rf /etc/ldap/slapd.d; +mkdir /etc/ldap/slapd.d; +slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d; +chown -R openldap:openldap /etc/ldap/slapd.d; +slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org \ + -l '${BACKUP_DIR}/dc=yunohost-dc=org.ldif'; +chown -R openldap:openldap /var/lib/ldap" 2>&1 +else + # Regenerate LDAP config directory from slapd.conf + sudo rm -Rf /etc/ldap/slapd.d + sudo mkdir /etc/ldap/slapd.d + sudo slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d/ 2>&1 + sudo chown -R openldap:openldap /etc/ldap/slapd.d/ +fi sudo service slapd force-reload diff --git a/data/hooks/conf_regen/12-metronome b/data/hooks/conf_regen/12-metronome index 70fa64dd3..82610cee8 100644 --- a/data/hooks/conf_regen/12-metronome +++ b/data/hooks/conf_regen/12-metronome @@ -18,30 +18,17 @@ function safe_copy () { cd /usr/share/yunohost/templates/metronome -# Copy additional modules -files="ldap.lib.lua -mod_auth_ldap2.lua -mod_legacyauth.lua -mod_storage_ldap.lua -vcard.lib.lua" - -for file in $files; do - safe_copy modules/$file /usr/lib/metronome/modules/$file -done - # Copy configuration files main_domain=$(cat /etc/yunohost/current_host) cat metronome.cfg.lua.sed \ | sed "s/{{ main_domain }}/$main_domain/g" \ | sudo tee metronome.cfg.lua safe_copy metronome.cfg.lua /etc/metronome/metronome.cfg.lua -safe_copy metronome.init /etc/init.d/metronome -safe_copy metronome.logrotate /etc/logrotate.d/metronome need_restart=False sudo mkdir -p /etc/metronome/conf.d -domain_list=$(sudo yunohost domain list --plain) +domain_list=$(sudo yunohost domain list --output-as plain) # Copy a configuration file for each YunoHost domain for domain in $domain_list; do @@ -51,7 +38,7 @@ for domain in $domain_list; do cat domain.cfg.lua.sed \ | sed "s/{{ domain }}/$domain/g" \ | sudo tee $domain.cfg.lua - if [[ $(safe_copy $domain.cfg.lua /etc/metronome/conf.d/$domain.cfg.lua) == "True" ]]; then + if [[ $(safe_copy $domain.cfg.lua /etc/metronome/conf.d/$domain.cfg.lua | tail -n1) == "True" ]]; then need_restart=True fi done @@ -63,9 +50,8 @@ for file in /etc/metronome/conf.d/*; do | sed 's|.cfg.lua||') sanitzed_domain="$(echo $domain | sed 's/\./%2e/g')" [[ $domain_list =~ $domain ]] \ - || ($(sudo yunohost service saferemove -s metronome $file) == "True" \ + || ([[ $(sudo yunohost service saferemove -s metronome $file | tail -n1) == "True" ]] \ && rm -rf /var/lib/metronome/$sanitzed_domain) - done # Create domain directory diff --git a/data/hooks/conf_regen/15-nginx b/data/hooks/conf_regen/15-nginx index e5a8716b3..7a4795202 100644 --- a/data/hooks/conf_regen/15-nginx +++ b/data/hooks/conf_regen/15-nginx @@ -37,7 +37,7 @@ done if [ -f /etc/yunohost/installed ]; then need_restart=False - domain_list=$(sudo yunohost domain list --plain) + domain_list=$(sudo yunohost domain list --output-as plain) # Copy a configuration file for each YunoHost domain for domain in $domain_list; do @@ -45,7 +45,7 @@ if [ -f /etc/yunohost/installed ]; then cat server.conf.sed \ | sed "s/{{ domain }}/$domain/g" \ | sudo tee $domain.conf - [[ $(safe_copy $domain.conf /etc/nginx/conf.d/$domain.conf) == "True" ]] \ + [[ $(safe_copy $domain.conf /etc/nginx/conf.d/$domain.conf | tail -n1) == "True" ]] \ && need_restart=True [ -f /etc/nginx/conf.d/$domain.d/yunohost_local.conf ] \ diff --git a/data/hooks/conf_regen/19-postfix b/data/hooks/conf_regen/19-postfix index 38061ee63..cf339df14 100644 --- a/data/hooks/conf_regen/19-postfix +++ b/data/hooks/conf_regen/19-postfix @@ -19,7 +19,7 @@ function safe_copy () { cd /usr/share/yunohost/templates/postfix # Copy plain single configuration files -files="header_check +files="header_checks ldap-accounts.cf ldap-aliases.cf ldap-domains.cf diff --git a/data/hooks/conf_regen/22-email-legacy b/data/hooks/conf_regen/22-email-legacy new file mode 100644 index 000000000..04962c4ca --- /dev/null +++ b/data/hooks/conf_regen/22-email-legacy @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Execute this hook only if we force the configuration regeneration +if [[ "$1" == "True" ]]; then + + # Add new email services + sudo yunohost service add rspamd -l /var/log/mail.log \ + || echo "Rspamd is already listed in services" + + sudo yunohost service add rmilter -l /var/log/mail.log \ + || echo "Rmilter is already listed in services" + + sudo yunohost service add memcached \ + || echo "Memcached is already listed in services" + + # Remove previous email services + sudo yunohost service remove spamassassin \ + || echo "Spamassassin is already removed" \ + && sudo systemctl disable spamassassin || true + + sudo rm -f etc/cron.daily/spamassassin + + sudo yunohost service remove amavis \ + || echo "Amavis is already removed" \ + && sudo systemctl disable amavis || true + + sudo yunohost service remove postgrey \ + || echo "Postgrey is already removed" \ + && sudo systemctl disable postgrey || true + + sudo systemctl stop spamassassin || true + sudo systemctl stop amavis || true + sudo systemctl stop postgrey || true +fi diff --git a/data/hooks/conf_regen/22-postgrey b/data/hooks/conf_regen/22-postgrey deleted file mode 100644 index b1f924a0e..000000000 --- a/data/hooks/conf_regen/22-postgrey +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e - -force=$1 - -function safe_copy () { - if [[ "$force" == "True" ]]; then - sudo yunohost service safecopy \ - -s postgrey \ - $1 $2 \ - --force - else - sudo yunohost service safecopy \ - -s postgrey \ - $1 $2 - fi -} - -cd /usr/share/yunohost/templates/postgrey - -if [[ "$(safe_copy postgrey.default /etc/default/postgrey)" == "True" ]]; then - sudo service nslcd restart -fi diff --git a/data/hooks/conf_regen/25-dovecot b/data/hooks/conf_regen/25-dovecot index 974fa3e0b..551400bdf 100644 --- a/data/hooks/conf_regen/25-dovecot +++ b/data/hooks/conf_regen/25-dovecot @@ -39,16 +39,14 @@ safe_copy dovecot-ldap.conf /etc/dovecot/dovecot-ldap.conf # Setup Sieve -sudo rm -rf /etc/dovecot/global_script -sudo mkdir -p -m 0770 /etc/dovecot/global_script -safe_copy sa-learn-pipe.sh /usr/bin/sa-learn-pipe.sh -sudo chmod 755 /usr/bin/sa-learn-pipe.sh +sudo mkdir -p /etc/dovecot/global_script +sudo chmod -R 770 /etc/dovecot/global_script safe_copy dovecot.sieve /etc/dovecot/global_script/dovecot.sieve sudo chmod 660 /etc/dovecot/global_script/dovecot.sieve > /dev/null 2>&1 \ || safe_copy dovecot.sieve /etc/dovecot/global_script/dovecot.sieve sudo sievec /etc/dovecot/global_script/dovecot.sieve sudo chmod 660 /etc/dovecot/global_script/dovecot.svbin - +sudo chown -R vmail:mail /etc/dovecot/global_script sudo service dovecot restart diff --git a/data/hooks/conf_regen/28-rmilter b/data/hooks/conf_regen/28-rmilter new file mode 100644 index 000000000..0b6b93a38 --- /dev/null +++ b/data/hooks/conf_regen/28-rmilter @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +force=$1 + +function safe_copy () { + if [[ "$force" == "True" ]]; then + sudo yunohost service safecopy \ + -s rmilter $1 $2 --force + else + sudo yunohost service safecopy \ + -s rmilter $1 $2 + fi +} + +cd /usr/share/yunohost/templates/rmilter + +# Copy Rmilter configuration +safe_copy rmilter.conf /etc/rmilter.conf + +# Override socket configuration +safe_copy rmilter.socket /etc/systemd/system/rmilter.socket + +# Create DKIM key for each YunoHost domain +sudo mkdir -p /etc/dkim +domain_list=$(sudo yunohost domain list --output-as plain) + +for domain in $domain_list; do + [ -f /etc/dkim/$domain.mail.key ] \ + || (sudo opendkim-genkey --domain=$domain \ + --selector=mail\ + --directory=/etc/dkim \ + && sudo mv /etc/dkim/mail.private /etc/dkim/$domain.mail.key \ + && sudo mv /etc/dkim/mail.txt /etc/dkim/$domain.mail.txt) + + sudo chown _rmilter /etc/dkim/$domain.mail.key + sudo chmod 400 /etc/dkim/$domain.mail.key +done + +# Reload systemd daemon and stop rmilter service to take into account the +# new configuration. It will be started again by the socket as needed. +sudo systemctl daemon-reload +sudo systemctl stop rmilter.service 2>&1 || true diff --git a/data/hooks/conf_regen/28-spamassassin b/data/hooks/conf_regen/28-spamassassin deleted file mode 100644 index e55f10dcd..000000000 --- a/data/hooks/conf_regen/28-spamassassin +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -e - -force=$1 - -function safe_copy () { - if [[ "$force" == "True" ]]; then - sudo yunohost service safecopy \ - -s spamassassin $1 $2 --force - else - sudo yunohost service safecopy \ - -s spamassassin $1 $2 - fi -} - -cd /usr/share/yunohost/templates/spamassassin - -safe_copy spamassassin.default /etc/default/spamassassin -safe_copy local.cf /etc/spamassassin/local.cf - -sudo service spamassassin restart diff --git a/data/hooks/conf_regen/31-amavis b/data/hooks/conf_regen/31-amavis deleted file mode 100644 index f25c70fe4..000000000 --- a/data/hooks/conf_regen/31-amavis +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -e - -force=$1 - -function safe_copy () { - if [[ "$force" == "True" ]]; then - sudo yunohost service safecopy \ - -s amavis $1 $2 --force - else - sudo yunohost service safecopy \ - -s amavis $1 $2 - fi -} - -cd /usr/share/yunohost/templates/amavis - -sudo mkdir -p /etc/amavis/conf.d/ - -# Copy plain single configuration files -files="05-domain_id -05-node_id -15-content_filter_mode -20-debian_defaults" - -for file in $files; do - safe_copy $file /etc/amavis/conf.d/$file -done - -main_domain=$(cat /etc/yunohost/current_host) -cat 50-user.sed \ - | sed "s/{{ main_domain }}/$main_domain/g" \ - | sudo tee 50-user -safe_copy 50-user /etc/amavis/conf.d/50-user - - -sudo service amavis restart diff --git a/data/hooks/conf_regen/31-rspamd b/data/hooks/conf_regen/31-rspamd new file mode 100644 index 000000000..4c1520062 --- /dev/null +++ b/data/hooks/conf_regen/31-rspamd @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +force=$1 + +function safe_copy () { + if [[ "$force" == "True" ]]; then + sudo yunohost service safecopy \ + -s rspamd $1 $2 --force + else + sudo yunohost service safecopy \ + -s rspamd $1 $2 + fi +} + +cd /usr/share/yunohost/templates/rspamd + +# Copy Rspamd configuration +safe_copy metrics.conf /etc/rspamd/metrics.conf + +# Install Rspamd sieve script +safe_copy rspamd.sieve /etc/dovecot/global_script/rspamd.sieve +sudo sievec /etc/dovecot/global_script/rspamd.sieve +sudo chmod 660 /etc/dovecot/global_script/rspamd.svbin +sudo chown -R vmail:mail /etc/dovecot/global_script + +sudo systemctl restart rspamd.socket +sudo systemctl restart dovecot diff --git a/data/hooks/conf_regen/34-mysql b/data/hooks/conf_regen/34-mysql index 021e4537a..8617e5573 100644 --- a/data/hooks/conf_regen/34-mysql +++ b/data/hooks/conf_regen/34-mysql @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -e force=$1 @@ -21,12 +21,12 @@ function randpass () { cd /usr/share/yunohost/templates/mysql -if [[ "$(safe_copy my.cnf /etc/mysql/my.cnf)" == "True" ]]; then +if [[ "$(safe_copy my.cnf /etc/mysql/my.cnf | tail -n1)" == "True" ]]; then sudo service mysql restart fi if [ ! -f /etc/yunohost/mysql ]; then - [[ $(/bin/ps aux | grep mysqld | grep -vc "grep") == "0" ]] \ + [[ $(/bin/ps aux | grep '[m]ysqld') == "0" ]] \ && sudo service mysql start mysql_password=$(randpass 10 0) diff --git a/data/hooks/conf_regen/37-avahi-daemon b/data/hooks/conf_regen/37-avahi-daemon index 31306de53..02bca0386 100644 --- a/data/hooks/conf_regen/37-avahi-daemon +++ b/data/hooks/conf_regen/37-avahi-daemon @@ -15,6 +15,6 @@ function safe_copy () { cd /usr/share/yunohost/templates/avahi-daemon -if [[ "$(safe_copy avahi-daemon.conf /etc/avahi/avahi-daemon.conf)" == "True" ]]; then +if [[ "$(safe_copy avahi-daemon.conf /etc/avahi/avahi-daemon.conf | tail -n1)" == "True" ]]; then sudo service avahi-daemon restart fi diff --git a/data/hooks/conf_regen/40-glances b/data/hooks/conf_regen/40-glances index 9f7c9c5c8..d9bb8aa20 100644 --- a/data/hooks/conf_regen/40-glances +++ b/data/hooks/conf_regen/40-glances @@ -15,6 +15,6 @@ function safe_copy () { cd /usr/share/yunohost/templates/glances -if [[ "$(safe_copy glances.default /etc/default/glances)" == "True" ]]; then +if [[ "$(safe_copy glances.default /etc/default/glances | tail -n1)" == "True" ]]; then sudo service glances restart fi diff --git a/data/hooks/conf_regen/43-dnsmasq b/data/hooks/conf_regen/43-dnsmasq index 821bfa392..dae14b0f6 100644 --- a/data/hooks/conf_regen/43-dnsmasq +++ b/data/hooks/conf_regen/43-dnsmasq @@ -16,7 +16,7 @@ function safe_copy () { cd /usr/share/yunohost/templates/dnsmasq # Get IP address -ip=$(curl ip.yunohost.org || echo '0.0.0.0') +ip=$(curl ip.yunohost.org 2>/dev/null || echo '0.0.0.0') # Get IPv6 IP address ipv6=$(ip route get 2000:: | grep -q "unreachable" && echo '' \ @@ -24,7 +24,7 @@ ipv6=$(ip route get 2000:: | grep -q "unreachable" && echo '' \ sudo mkdir -p /etc/dnsmasq.d -domain_list=$(sudo yunohost domain list --plain) +domain_list=$(sudo yunohost domain list --output-as plain) # Copy a configuration file for each YunoHost domain for domain in $domain_list; do diff --git a/data/hooks/conf_regen/46-nsswitch b/data/hooks/conf_regen/46-nsswitch index 73535eeda..90d05e087 100644 --- a/data/hooks/conf_regen/46-nsswitch +++ b/data/hooks/conf_regen/46-nsswitch @@ -15,6 +15,6 @@ function safe_copy () { cd /usr/share/yunohost/templates/nsswitch -if [[ "$(safe_copy nsswitch.conf /etc/nsswitch.conf)" == "True" ]]; then +if [[ "$(safe_copy nsswitch.conf /etc/nsswitch.conf | tail -n1)" == "True" ]]; then sudo service nscd restart fi diff --git a/data/hooks/conf_regen/49-udisks-glue b/data/hooks/conf_regen/49-udisks-glue deleted file mode 100644 index 85de9182d..000000000 --- a/data/hooks/conf_regen/49-udisks-glue +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -e - -force=$1 - -function safe_copy () { - if [[ "$force" == "True" ]]; then - sudo yunohost service safecopy \ - -s udisks-glue $1 $2 --force - else - sudo yunohost service safecopy \ - -s udisks-glue $1 $2 - fi -} - -cd /usr/share/yunohost/templates/udisks-glue - -if [[ "$(safe_copy udisks-glue.conf /etc/udisks-glue.conf)" == "True" ]]; then - sudo service udisks-glue restart -fi diff --git a/data/hooks/conf_regen/52-fail2ban b/data/hooks/conf_regen/52-fail2ban index 9c609c74a..b9d76afeb 100644 --- a/data/hooks/conf_regen/52-fail2ban +++ b/data/hooks/conf_regen/52-fail2ban @@ -24,6 +24,6 @@ version=$(sed 's/\..*//' /etc/debian_version) && sudo cp jail-jessie.conf jail.conf \ || sudo cp jail-wheezy.conf jail.conf -if [[ $(safe_copy jail.conf /etc/fail2ban/jail.conf) == "True" ]]; then +if [[ $(safe_copy jail.conf /etc/fail2ban/jail.conf | tail -n1) == "True" ]]; then sudo service fail2ban restart fi diff --git a/data/hooks/restore/05-conf_ldap b/data/hooks/restore/05-conf_ldap index c41020352..7470f6eef 100644 --- a/data/hooks/restore/05-conf_ldap +++ b/data/hooks/restore/05-conf_ldap @@ -1,37 +1,60 @@ -backup_dir="$1/conf/ldap" +#!/bin/bash -if [ -z "$2" ]; then +backup_dir="${1}/conf/ldap" + +if [[ $EUID -ne 0 ]]; then # We need to execute this script as root, since the ldap # service will be shut down during the operation (and sudo # won't be available) - sudo bash $(pwd)/$0 $1 sudoed + sudo /bin/bash $(readlink -f $0) $1 else - service slapd stop - # Backup old configuration - mv /var/lib/ldap /var/lib/ldap.old + service slapd stop || true - # Recreate new DB folder - mkdir /var/lib/ldap - chown openldap: /var/lib/ldap - chmod go-rwx /var/lib/ldap + # Create a directory for backup + TMPDIR="/tmp/$(date +%s)" + mkdir -p "$TMPDIR" - # Restore LDAP configuration (just to be sure) - cp -a $backup_dir/slapd.conf /etc/ldap/slapd.conf + die() { + state=$1 + error=$2 - # Regenerate the configuration - rm -rf /etc/ldap/slapd.d/* - slaptest -f /etc/ldap/slapd.conf -F /etc/ldap/slapd.d - chown -R openldap:openldap /etc/ldap/slapd.d - cp -rfp /var/lib/ldap.old/DB_CONFIG /var/lib/ldap + # Restore saved configuration and database + [[ $state -ge 1 ]] \ + && (rm -rf /etc/ldap/slapd.d && + mv "${TMPDIR}/slapd.d" /etc/ldap/slapd.d) + [[ $state -ge 2 ]] \ + && (rm -rf /var/lib/ldap && + mv "${TMPDIR}/ldap" /var/lib/ldap) + chown -R openldap: /etc/ldap/slapd.d /var/lib/ldap - # Import the database - slapadd -l $backup_dir/slapcat.ldif + service slapd start + rm -rf "$TMPDIR" + + # Print an error message and exit + printf "%s" "$error" 1>&2 + exit 1 + } + + # Restore the configuration + mv /etc/ldap/slapd.d "$TMPDIR" + mkdir -p /etc/ldap/slapd.d + cp -a "${backup_dir}/slapd.conf" /etc/ldap/slapd.conf + slapadd -F /etc/ldap/slapd.d -b cn=config \ + -l "${backup_dir}/cn=config.master.ldif" \ + || die 1 "Unable to restore LDAP configuration" + chown -R openldap: /etc/ldap/slapd.d + + # Restore the database + mv /var/lib/ldap "$TMPDIR" + mkdir -p /var/lib/ldap + slapadd -F /etc/ldap/slapd.d -b dc=yunohost,dc=org \ + -l "${backup_dir}/dc=yunohost-dc=org.ldif" \ + || die 2 "Unable to restore LDAP database" + chown -R openldap: /var/lib/ldap - # Change permissions and restart slapd - chown openldap: /var/lib/ldap/* service slapd start - rm -rf /var/lib/ldap.old + rm -rf "$TMPDIR" fi diff --git a/data/templates/amavis/05-domain_id b/data/templates/amavis/05-domain_id deleted file mode 100644 index 01a71e4b3..000000000 --- a/data/templates/amavis/05-domain_id +++ /dev/null @@ -1,19 +0,0 @@ -use strict; - -# $mydomain is used just for convenience in the config files and it is not -# used internally by amavisd-new except in the default X_HEADER_LINE (which -# Debian overrides by default anyway). - -#chomp($mydomain = `head -n 1 /etc/mailname`); - -# amavisd-new needs to know which email domains are to be considered local -# to the administrative domain. Only emails to "local" domains are subject -# to certain functionality, such as the addition of spam tags. -# -# Default local domains to $mydomain and all subdomains. Remember to -# override or redefine this if $mydomain is changed later in the config -# sequence. - -@local_domains_acl = ( ".$mydomain" ); - -1; # ensure a defined return diff --git a/data/templates/amavis/05-node_id b/data/templates/amavis/05-node_id deleted file mode 100644 index ee6665436..000000000 --- a/data/templates/amavis/05-node_id +++ /dev/null @@ -1,13 +0,0 @@ -use strict; - -# $myhostname is used by amavisd-new for node identification, and it is -# important to get it right (e.g. for ESMTP EHLO, loop detection, and so on). - -#chomp($myhostname = `hostname --fqdn`); - -# To manually set $myhostname, edit the following line with the correct Fully -# Qualified Domain Name (FQDN) and remove the # at the beginning of the line. -# -#$myhostname = "mail.example.com"; - -1; # ensure a defined return diff --git a/data/templates/amavis/15-content_filter_mode b/data/templates/amavis/15-content_filter_mode deleted file mode 100644 index 825e9e03c..000000000 --- a/data/templates/amavis/15-content_filter_mode +++ /dev/null @@ -1,23 +0,0 @@ -use strict; - -# You can modify this file to re-enable SPAM checking through spamassassin -# and to re-enable antivirus checking. - -# -# Default antivirus checking mode -# Uncomment the two lines below to enable it back -# - -#@bypass_virus_checks_maps = ( -# \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); - - -# -# Default SPAM checking mode -# Uncomment the two lines below to enable it back -# - -@bypass_spam_checks_maps = ( - \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); - -1; # ensure a defined return diff --git a/data/templates/amavis/20-debian_defaults b/data/templates/amavis/20-debian_defaults deleted file mode 100644 index 83e553d28..000000000 --- a/data/templates/amavis/20-debian_defaults +++ /dev/null @@ -1,216 +0,0 @@ -use strict; - -# ADMINISTRATORS: -# Debian suggests that any changes you need to do that should never -# be "updated" by the Debian package should be made in another file, -# overriding the settings in this file. -# -# The package will *not* overwrite your settings, but by keeping -# them separate, you will make the task of merging changes on these -# configuration files much simpler... - -# see /usr/share/doc/amavisd-new/examples/amavisd.conf-default for -# a list of all variables with their defaults; -# see /usr/share/doc/amavisd-new/examples/amavisd.conf-sample for -# a traditional-style commented file -# [note: the above files were not converted to Debian settings!] -# -# for more details see documentation in /usr/share/doc/amavisd-new -# and at http://www.ijs.si/software/amavisd/amavisd-new-docs.html - -$QUARANTINEDIR = "$MYHOME/virusmails"; -$quarantine_subdir_levels = 1; # enable quarantine dir hashing - -$log_recip_templ = undef; # disable by-recipient level-0 log entries -$DO_SYSLOG = 1; # log via syslogd (preferred) -$syslog_ident = 'amavis'; # syslog ident tag, prepended to all messages -$syslog_facility = 'mail'; -$syslog_priority = 'debug'; # switch to info to drop debug output, etc - -$enable_db = 1; # enable use of BerkeleyDB/libdb (SNMP and nanny) -$enable_global_cache = 1; # enable use of libdb-based cache if $enable_db=1 - -$inet_socket_port = 10024; # default listening socket - -$sa_spam_subject_tag = '***SPAM*** '; -$sa_tag_level_deflt = undef; # add spam info headers if at, or above that level -$sa_tag2_level_deflt = 4.00; # add 'spam detected' headers at that level -$sa_kill_level_deflt = 20.00; # triggers spam evasive actions -$sa_dsn_cutoff_level = 10; # spam level beyond which a DSN is not sent - -$sa_mail_body_size_limit = 200*1024; # don't waste time on SA if mail is larger -$sa_local_tests_only = 0; # only tests which do not require internet access? - -$recipient_delimiter = '+'; -@addr_extension_spam_maps = ('Junk'); - -# Quota limits to avoid bombs (like 42.zip) - -$MAXLEVELS = 14; -$MAXFILES = 1500; -$MIN_EXPANSION_QUOTA = 100*1024; # bytes -$MAX_EXPANSION_QUOTA = 300*1024*1024; # bytes - -# You should: -# Use D_DISCARD to discard data (viruses) -# Use D_BOUNCE to generate local bounces by amavisd-new -# Use D_REJECT to generate local or remote bounces by the calling MTA -# Use D_PASS to deliver the message -# -# Whatever you do, *NEVER* use D_REJECT if you have other MTAs *forwarding* -# mail to your account. Use D_BOUNCE instead, otherwise you are delegating -# the bounce work to your friendly forwarders, which might not like it at all. -# -# On dual-MTA setups, one can often D_REJECT, as this just makes your own -# MTA generate the bounce message. Test it first. -# -# Bouncing viruses is stupid, always discard them after you are sure the AV -# is working correctly. Bouncing real SPAM is also useless, if you cannot -# D_REJECT it (and don't D_REJECT mail coming from your forwarders!). - -$final_virus_destiny = D_DISCARD; # (data not lost, see virus quarantine) -$final_banned_destiny = D_BOUNCE; # D_REJECT when front-end MTA -$final_spam_destiny = D_DISCARD; -$final_bad_header_destiny = D_PASS; # False-positive prone (for spam) - -$enable_dkim_verification = 1; #disabled to prevent warning -$enable_dkim_signing =1; - -$virus_admin = "postmaster\@$mydomain"; # due to D_DISCARD default - -# Set to empty ("") to add no header -$X_HEADER_LINE = "Debian $myproduct_name at $mydomain"; - -# REMAINING IMPORTANT VARIABLES ARE LISTED HERE BECAUSE OF LONGER ASSIGNMENTS - -# -# DO NOT SEND VIRUS NOTIFICATIONS TO OUTSIDE OF YOUR DOMAIN. EVER. -# -# These days, almost all viruses fake the envelope sender and mail headers. -# Therefore, "virus notifications" became nothing but undesired, aggravating -# SPAM. This holds true even inside one's domain. We disable them all by -# default, except for the EICAR test pattern. -# - -@viruses_that_fake_sender_maps = (new_RE( - [qr'\bEICAR\b'i => 0], # av test pattern name - [qr/.*/ => 1], # true for everything else -)); - -@keep_decoded_original_maps = (new_RE( -# qr'^MAIL$', # retain full original message for virus checking (can be slow) - qr'^MAIL-UNDECIPHERABLE$', # recheck full mail if it contains undecipherables - qr'^(ASCII(?! cpio)|text|uuencoded|xxencoded|binhex)'i, -# qr'^Zip archive data', # don't trust Archive::Zip -)); - - -# for $banned_namepath_re, a new-style of banned table, see amavisd.conf-sample - -$banned_filename_re = new_RE( -# qr'^UNDECIPHERABLE$', # is or contains any undecipherable components - - # block certain double extensions anywhere in the base name - qr'\.[^./]*\.(exe|vbs|pif|scr|bat|cmd|com|cpl|dll)\.?$'i, - - qr'\{[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\}?$'i, # Windows Class ID CLSID, strict - - qr'^application/x-msdownload$'i, # block these MIME types - qr'^application/x-msdos-program$'i, - qr'^application/hta$'i, - -# qr'^application/x-msmetafile$'i, # Windows Metafile MIME type -# qr'^\.wmf$', # Windows Metafile file(1) type - -# qr'^message/partial$'i, qr'^message/external-body$'i, # rfc2046 MIME types - -# [ qr'^\.(Z|gz|bz2)$' => 0 ], # allow any in Unix-compressed -# [ qr'^\.(rpm|cpio|tar)$' => 0 ], # allow any in Unix-type archives -# [ qr'^\.(zip|rar|arc|arj|zoo)$'=> 0 ], # allow any within such archives -# [ qr'^application/x-zip-compressed$'i => 0], # allow any within such archives - - qr'.\.(exe|vbs|pif|scr|bat|cmd|com|cpl)$'i, # banned extension - basic -# qr'.\.(ade|adp|app|bas|bat|chm|cmd|com|cpl|crt|emf|exe|fxp|grp|hlp|hta| -# inf|ins|isp|js|jse|lnk|mda|mdb|mde|mdw|mdt|mdz|msc|msi|msp|mst| -# ops|pcd|pif|prg|reg|scr|sct|shb|shs|vb|vbe|vbs| -# wmf|wsc|wsf|wsh)$'ix, # banned ext - long - -# qr'.\.(mim|b64|bhx|hqx|xxe|uu|uue)$'i, # banned extension - WinZip vulnerab. - - qr'^\.(exe-ms)$', # banned file(1) types -# qr'^\.(exe|lha|tnef|cab|dll)$', # banned file(1) types -); -# See http://support.microsoft.com/default.aspx?scid=kb;EN-US;q262631 -# and http://www.cknow.com/vtutor/vtextensions.htm - - -# ENVELOPE SENDER SOFT-WHITELISTING / SOFT-BLACKLISTING - -@score_sender_maps = ({ # a by-recipient hash lookup table, - # results from all matching recipient tables are summed - -# ## per-recipient personal tables (NOTE: positive: black, negative: white) -# 'user1@example.com' => [{'bla-mobile.press@example.com' => 10.0}], -# 'user3@example.com' => [{'.ebay.com' => -3.0}], -# 'user4@example.com' => [{'cleargreen@cleargreen.com' => -7.0, -# '.cleargreen.com' => -5.0}], - - ## site-wide opinions about senders (the '.' matches any recipient) - '.' => [ # the _first_ matching sender determines the score boost - - new_RE( # regexp-type lookup table, just happens to be all soft-blacklist - [qr'^(bulkmail|offers|cheapbenefits|earnmoney|foryou)@'i => 5.0], - [qr'^(greatcasino|investments|lose_weight_today|market\.alert)@'i=> 5.0], - [qr'^(money2you|MyGreenCard|new\.tld\.registry|opt-out|opt-in)@'i=> 5.0], - [qr'^(optin|saveonlsmoking2002k|specialoffer|specialoffers)@'i => 5.0], - [qr'^(stockalert|stopsnoring|wantsome|workathome|yesitsfree)@'i => 5.0], - [qr'^(your_friend|greatoffers)@'i => 5.0], - [qr'^(inkjetplanet|marketopt|MakeMoney)\d*@'i => 5.0], - ), - -# read_hash("/var/amavis/sender_scores_sitewide"), - -# This are some examples for whitelists, since envelope senders can be forged -# they are not enabled by default. - { # a hash-type lookup table (associative array) - #'nobody@cert.org' => -3.0, - #'cert-advisory@us-cert.gov' => -3.0, - #'owner-alert@iss.net' => -3.0, - #'slashdot@slashdot.org' => -3.0, - #'securityfocus.com' => -3.0, - #'ntbugtraq@listserv.ntbugtraq.com' => -3.0, - #'security-alerts@linuxsecurity.com' => -3.0, - #'mailman-announce-admin@python.org' => -3.0, - #'amavis-user-admin@lists.sourceforge.net'=> -3.0, - #'amavis-user-bounces@lists.sourceforge.net' => -3.0, - #'spamassassin.apache.org' => -3.0, - #'notification-return@lists.sophos.com' => -3.0, - #'owner-postfix-users@postfix.org' => -3.0, - #'owner-postfix-announce@postfix.org' => -3.0, - #'owner-sendmail-announce@lists.sendmail.org' => -3.0, - #'sendmail-announce-request@lists.sendmail.org' => -3.0, - #'donotreply@sendmail.org' => -3.0, - #'ca+envelope@sendmail.org' => -3.0, - #'noreply@freshmeat.net' => -3.0, - #'owner-technews@postel.acm.org' => -3.0, - #'ietf-123-owner@loki.ietf.org' => -3.0, - #'cvs-commits-list-admin@gnome.org' => -3.0, - #'rt-users-admin@lists.fsck.com' => -3.0, - #'clp-request@comp.nus.edu.sg' => -3.0, - #'surveys-errors@lists.nua.ie' => -3.0, - #'emailnews@genomeweb.com' => -5.0, - #'yahoo-dev-null@yahoo-inc.com' => -3.0, - #'returns.groups.yahoo.com' => -3.0, - #'clusternews@linuxnetworx.com' => -3.0, - #lc('lvs-users-admin@LinuxVirtualServer.org') => -3.0, - #lc('owner-textbreakingnews@CNNIMAIL12.CNN.COM') => -5.0, - - # soft-blacklisting (positive score) - #'sender@example.net' => 3.0, - #'.example.net' => 1.0, - - }, - ], # end of site-wide tables -}); - -1; # ensure a defined return diff --git a/data/templates/amavis/50-user.sed b/data/templates/amavis/50-user.sed deleted file mode 100644 index b0e7ce148..000000000 --- a/data/templates/amavis/50-user.sed +++ /dev/null @@ -1,30 +0,0 @@ -use strict; - -# -# Place your configuration directives here. They will override those in -# earlier files. -# -# See /usr/share/doc/amavisd-new/ for documentation and examples of -# the directives you can use in this file -# - -$myhostname = "{{ main_domain }}"; - -$mydomain = "{{ main_domain }}"; - -# Enable LDAP support -$enable_ldap = 1; - -# Default LDAP settings -$default_ldap = { - hostname => "127.0.0.1", - tls => 0, - version => 3, - base => "dc=yunohost,dc=org", - scope => "sub", - query_filter => "(&(objectClass=inetOrgPerson)(mail=%m))", -}; - - -#------------ Do not modify anything below this line ------------- -1; # ensure a defined return diff --git a/data/templates/dovecot/dovecot.conf.sed b/data/templates/dovecot/dovecot.conf.sed index 6a5070078..44ce55147 100644 --- a/data/templates/dovecot/dovecot.conf.sed +++ b/data/templates/dovecot/dovecot.conf.sed @@ -57,12 +57,12 @@ plugin { antispam_debug_target = syslog antispam_verbose_debug = 0 antispam_backend = pipe + antispam_spam = Junk;SPAM antispam_trash = Trash - antispam_spam = SPAM;Junk - antispam_allow_append_to_spam = no - antispam_pipe_program = /usr/bin/sa-learn-pipe.sh - antispam_pipe_program_spam_arg = --spam - antispam_pipe_program_notspam_arg = --ham + antispam_pipe_program = /usr/bin/rspamc + antispam_pipe_program_args = -h;localhost:11334;-P;q1 + antispam_pipe_program_spam_arg = learn_spam + antispam_pipe_program_notspam_arg = learn_ham } plugin { diff --git a/data/templates/dovecot/sa-learn-pipe.sh b/data/templates/dovecot/sa-learn-pipe.sh deleted file mode 100644 index 67437e559..000000000 --- a/data/templates/dovecot/sa-learn-pipe.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -echo /usr/bin/sa-learn $* /tmp/sendmail-msg-$$.txt ; -echo "$$-start ($*)" >> /tmp/sa-learn-pipe.log ; -#echo $* > /tmp/sendmail-parms.txt ; -cat<&0 >> /tmp/sendmail-msg-$$.txt ; -/usr/bin/sa-learn $* /tmp/sendmail-msg-$$.txt ; -rm -f /tmp/sendmail-msg-$$.txt ; -echo "$$-end" >> /tmp/sa-learn-pipe.log ; -exit 0; diff --git a/data/templates/metronome/metronome.init b/data/templates/metronome/metronome.init deleted file mode 100644 index 5f6f2ed46..000000000 --- a/data/templates/metronome/metronome.init +++ /dev/null @@ -1,119 +0,0 @@ -#! /bin/sh - -### BEGIN INIT INFO -# Provides: metronome -# Required-Start: $network $local_fs $remote_fs $syslog -# Required-Stop: $remote_fs $syslog -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Metronome XMPP Server -### END INIT INFO - -set -e - -# /etc/init.d/metronome: start and stop Metronome XMPP server - -NAME=metronome -USER=metronome -DAEMON=/usr/bin/metronome -PIDPATH=/var/run/metronome -PIDFILE="$PIDPATH"/metronome.pid - -NICE= -MAXFDS= -CPUSCHED= -IOSCHED= - -test -x "$DAEMON" || exit 0 - -. /lib/lsb/init-functions - -if [ -f /etc/default/metronome ] ; then - . /etc/default/metronome -fi - -start_opts() { - test -z "$NICE" || echo -n " --nicelevel $NICE" - test -z "$CPUSCHED" || echo -n " --procsched $CPUSCHED" - test -z "$IOSCHED" || echo -n " --iosched $IOSCHED" -} - -start_metronome () { - mkdir -p `dirname $PIDFILE` - chown metronome:adm `dirname $PIDFILE` - if start-stop-daemon --start --quiet --pidfile "$PIDFILE" \ - --chuid "$USER" --oknodo --user "$USER" --name lua5.1 \ - $(start_opts) --startas "$DAEMON"; - then - return 0 - else - return 1 - fi -} - -stop_metronome () { - if start-stop-daemon --stop --quiet --retry 30 \ - --oknodo --pidfile "$PIDFILE" --user "$USER" --name lua5.1; - then - return 0 - else - return 1 - fi -} - -signal_metronome () { - if start-stop-daemon --stop --quiet --pidfile "$PIDFILE" \ - --user "$USER" --name lua5.1 --oknodo --signal $1; - then - return 0 - else - return 1 - fi -} - -case "$1" in - start) - log_daemon_msg "Starting Metronome XMPP Server" "metronome" - if start_metronome; then - log_end_msg 0; - else - log_end_msg 1; - fi - ;; - stop) - log_daemon_msg "Stopping Metronome XMPP Server" "metronome" - if stop_metronome; then - log_end_msg 0; - else - log_end_msg 1; - fi - ;; - force-reload|restart) - log_daemon_msg "Restarting Metronome XMPP Server" "metronome" - - stop_metronome - - if start_metronome; then - log_end_msg 0; - else - log_end_msg 1; - fi - ;; - reload) - log_daemon_msg "Reloading Metronome XMPP Server" "metronome" - - if signal_metronome 1; then - log_end_msg 0; - else - log_end_msg 1; - fi - ;; - status) - status_of_proc -p $PIDFILE $DAEMON $NAME - ;; - *) - log_action_msg "Usage: /etc/init.d/metronome {start|stop|restart|reload|status}" - exit 1 -esac - -exit 0 diff --git a/data/templates/metronome/metronome.logrotate b/data/templates/metronome/metronome.logrotate deleted file mode 100644 index ccdc2febe..000000000 --- a/data/templates/metronome/metronome.logrotate +++ /dev/null @@ -1,11 +0,0 @@ -/var/log/metronome/metronome.log /var/log/metronome/metronome.err { - daily - rotate 14 - compress - create 640 metronome adm - postrotate - /etc/init.d/metronome reload > /dev/null - endscript - sharedscripts - missingok -} diff --git a/data/templates/metronome/modules/mod_auth_ldap2.lua b/data/templates/metronome/modules/mod_auth_ldap2.lua deleted file mode 100644 index 8c50a99fd..000000000 --- a/data/templates/metronome/modules/mod_auth_ldap2.lua +++ /dev/null @@ -1,81 +0,0 @@ --- vim:sts=4 sw=4 - --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- --- http://code.google.com/p/prosody-modules/source/browse/mod_auth_ldap/mod_auth_ldap.lua --- adapted to use common LDAP store - -local ldap = module:require 'ldap'; -local new_sasl = require 'util.sasl'.new; -local jsplit = require 'util.jid'.split; - -if not ldap then - return; -end - -local provider = {} - -function provider.test_password(username, password) - return ldap.bind(username, password); -end - -function provider.user_exists(username) - local params = ldap.getparams() - - local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username); - if params.user.usernamefield == 'mail' then - filter = ldap.filter.combine_and(params.user.filter, 'mail=' .. username .. '@*'); - end - - return ldap.singlematch { - base = params.user.basedn, - filter = filter, - }; -end - -function provider.get_password(username) - return nil, "Passwords unavailable for LDAP."; -end - -function provider.set_password(username, password) - return nil, "Passwords unavailable for LDAP."; -end - -function provider.create_user(username, password) - return nil, "Account creation/modification not available with LDAP."; -end - -function provider.get_sasl_handler() - local testpass_authentication_profile = { - plain_test = function(sasl, username, password, realm) - return provider.test_password(username, password), true; - end, - mechanisms = { PLAIN = true }, - }; - return new_sasl(module.host, testpass_authentication_profile); -end - -function provider.is_admin(jid) - local admin_config = ldap.getparams().admin; - - if not admin_config then - return; - end - - local ld = ldap:getconnection(); - local username = jsplit(jid); - local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); - - return ldap.singlematch { - base = admin_config.basedn, - filter = filter, - }; -end - -module:provides("auth", provider); diff --git a/data/templates/metronome/modules/mod_storage_ldap.lua b/data/templates/metronome/modules/mod_storage_ldap.lua deleted file mode 100644 index 17850a217..000000000 --- a/data/templates/metronome/modules/mod_storage_ldap.lua +++ /dev/null @@ -1,180 +0,0 @@ --- vim:sts=4 sw=4 - --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- Copyright (C) 2012 Rob Hoelz --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - ----------------------------------------- --- Constants and such -- ----------------------------------------- - -local setmetatable = setmetatable; -local ldap = module:require 'ldap'; -local vcardlib = module:require 'vcard'; -local st = require 'util.stanza'; -local gettime = require 'socket'.gettime; - -if not ldap then - return; -end - -local CACHE_EXPIRY = 300; -local params = module:get_option('ldap'); - ----------------------------------------- --- Utility Functions -- ----------------------------------------- - -local function ldap_record_to_vcard(record) - return vcardlib.create { - record = record, - format = params.vcard_format, - } -end - -local get_alias_for_user; - -do - local user_cache; - local last_fetch_time; - - local function populate_user_cache() - local ld = ldap.getconnection(); - - local usernamefield = params.user.usernamefield; - local namefield = params.user.namefield; - - user_cache = {}; - - for _, attrs in ld:search { base = params.user.basedn, scope = 'onelevel', filter = params.user.filter } do - user_cache[attrs[usernamefield]] = attrs[namefield]; - end - last_fetch_time = gettime(); - end - - function get_alias_for_user(user) - if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then - user_cache = nil; - end - if not user_cache then - populate_user_cache(); - end - return user_cache[user]; - end -end - ----------------------------------------- --- General Setup -- ----------------------------------------- - -local ldap_store = {}; -ldap_store.__index = ldap_store; - -local adapters = { - roster = {}, - vcard = {}, -} - -for k, v in pairs(adapters) do - setmetatable(v, ldap_store); - v.__index = v; - v.name = k; -end - -function ldap_store:get(username) - return nil, "get method unimplemented on store '" .. tostring(self.name) .. "'" -end - -function ldap_store:set(username, data) - return nil, "LDAP storage is currently read-only"; -end - ----------------------------------------- --- Roster Storage Implementation -- ----------------------------------------- - -function adapters.roster:get(username) - local ld = ldap.getconnection(); - local contacts = {}; - - local memberfield = params.groups.memberfield; - local namefield = params.groups.namefield; - local filter = memberfield .. '=' .. tostring(username); - - local groups = {}; - for _, config in ipairs(params.groups) do - groups[ config[namefield] ] = config.name; - end - - -- XXX this kind of relies on the way we do groups at INOC - for _, attrs in ld:search { base = params.groups.basedn, scope = 'onelevel', filter = filter } do - if groups[ attrs[namefield] ] then - local members = attrs[memberfield]; - - for _, user in ipairs(members) do - if user ~= username then - local jid = user .. '@' .. module.host; - local record = contacts[jid]; - - if not record then - record = { - subscription = 'both', - groups = {}, - name = get_alias_for_user(user), - }; - contacts[jid] = record; - end - - record.groups[ groups[ attrs[namefield] ] ] = true; - end - end - end - end - - return contacts; -end - ----------------------------------------- --- vCard Storage Implementation -- ----------------------------------------- - -function adapters.vcard:get(username) - if not params.vcard_format then - return nil, ''; - end - - local ld = ldap.getconnection(); - local filter = params.user.usernamefield .. '=' .. tostring(username); - - local match = ldap.singlematch { - base = params.user.basedn, - filter = filter, - }; - if match then - match.jid = username .. '@' .. module.host - return st.preserialize(ldap_record_to_vcard(match)); - else - return nil, 'not found'; - end -end - ----------------------------------------- --- Driver Definition -- ----------------------------------------- - -local driver = {}; - -function driver:open(store, typ) - local adapter = adapters[store]; - - if adapter and not typ then - return adapter; - end - return nil, "unsupported-store"; -end -module:provides("storage", driver); diff --git a/data/templates/mysql/my.cnf b/data/templates/mysql/my.cnf index 2d4e1df2a..cf9e6ddd7 100644 --- a/data/templates/mysql/my.cnf +++ b/data/templates/mysql/my.cnf @@ -36,6 +36,9 @@ read_rnd_buffer_size = 256K net_buffer_length = 2K thread_stack = 128K +# to avoid corruption on powerfailure +default-storage-engine=innodb + # Don't listen on a TCP/IP port at all. This can be a security enhancement, # if all processes that need to connect to mysqld run on the same host. # All interaction with mysqld must be made via Unix sockets or named pipes. diff --git a/data/templates/nginx/yunohost_admin.conf.inc b/data/templates/nginx/yunohost_admin.conf.inc index 736102944..b0ab4cef6 100644 --- a/data/templates/nginx/yunohost_admin.conf.inc +++ b/data/templates/nginx/yunohost_admin.conf.inc @@ -2,4 +2,10 @@ location /yunohost/admin { alias /usr/share/yunohost/admin/; default_type text/html; index index.html; + + # Short cache on handlebars templates + location ~* \.(?:ms)$ { + expires 5m; + add_header Cache-Control "public"; + } } diff --git a/data/templates/nginx/yunohost_panel.conf.inc b/data/templates/nginx/yunohost_panel.conf.inc index 4d5b441d1..0ca8b02aa 100644 --- a/data/templates/nginx/yunohost_panel.conf.inc +++ b/data/templates/nginx/yunohost_panel.conf.inc @@ -1,2 +1,2 @@ -sub_filter ''; +sub_filter ''; sub_filter_once on; diff --git a/data/templates/nslcd/nslcd.conf b/data/templates/nslcd/nslcd.conf index c927b5f39..091ecb7cc 100644 --- a/data/templates/nslcd/nslcd.conf +++ b/data/templates/nslcd/nslcd.conf @@ -15,24 +15,11 @@ base dc=yunohost,dc=org # The LDAP protocol version to use. #ldap_version 3 -# The DN to bind with for normal lookups. -#binddn cn=annonymous,dc=example,dc=net -#bindpw secret - -# The DN used for password modifications by root. -#rootpwmoddn cn=admin,dc=example,dc=com - -# SSL options -#ssl off -#tls_reqcert never -tls_cacertfile /etc/ssl/certs/ca-certificates.crt - # The search scope. #scope sub +# Build a full list of non-LDAP users on startup. +nss_initgroups_ignoreusers ALLLOCAL - - - - - +# The minimum numeric user id to lookup. +nss_min_uid 1000 diff --git a/data/templates/postfix/header_check b/data/templates/postfix/header_checks similarity index 100% rename from data/templates/postfix/header_check rename to data/templates/postfix/header_checks diff --git a/data/templates/postfix/main.cf.sed b/data/templates/postfix/main.cf.sed index fd81ae64f..394e23e0b 100644 --- a/data/templates/postfix/main.cf.sed +++ b/data/templates/postfix/main.cf.sed @@ -86,9 +86,6 @@ smtpd_sasl_security_options = noanonymous smtpd_sasl_local_domain = -# Use AMaVis -content_filter = amavis:[127.0.0.1]:10024 - # Wait until the RCPT TO command before evaluating restrictions smtpd_delay_reject = yes @@ -128,13 +125,8 @@ smtpd_recipient_restrictions = reject_non_fqdn_recipient, reject_unknown_recipient_domain, reject_unauth_destination, - check_policy_service unix:private/policy-spf - check_policy_service inet:127.0.0.1:10023 permit -# Use SPF -policy-spf_time_limit = 3600s - # SRS sender_canonical_maps = regexp:/etc/postfix/sender_canonical sender_canonical_classes = envelope_sender @@ -143,3 +135,11 @@ sender_canonical_classes = envelope_sender smtp_header_checks = regexp:/etc/postfix/header_checks smtp_reply_filter = pcre:/etc/postfix/smtp_reply_filter + +# Rmilter +milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} +milter_protocol = 6 +smtpd_milters = inet:localhost:11000 + +# Skip email without checking if milter has died +milter_default_action = accept diff --git a/data/templates/postfix/master.cf b/data/templates/postfix/master.cf index cf7fd6267..ed6d87bd3 100644 --- a/data/templates/postfix/master.cf +++ b/data/templates/postfix/master.cf @@ -116,32 +116,3 @@ dovecot unix - n n - - pipe # (yes) (yes) (yes) (never) (100) # ========================================================================== # Added using postfix-add-filter script: -amavis unix - - - - 2 smtp - -o smtp_data_done_timeout=1200 - -o smtp_send_xforward_command=yes - -o smtp_tls_note_starttls_offer=no - -policy-spf unix - n n - - spawn - user=nobody argv=/usr/bin/perl /usr/sbin/postfix-policyd-spf-perl - -127.0.0.1:10025 inet n - - - - smtpd - -o content_filter= - -o smtpd_delay_reject=no - -o smtpd_client_restrictions=permit_mynetworks,reject - -o smtpd_helo_restrictions= - -o smtpd_sender_restrictions= - -o smtpd_recipient_restrictions=permit_mynetworks,reject - -o smtpd_data_restrictions=reject_unauth_pipelining - -o smtpd_end_of_data_restrictions= - -o smtpd_restriction_classes= - -o mynetworks=127.0.0.0/8 - -o smtpd_error_sleep_time=0 - -o smtpd_soft_error_limit=1001 - -o smtpd_hard_error_limit=1000 - -o smtpd_client_connection_count_limit=0 - -o smtpd_client_connection_rate_limit=0 - -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_milters - -o local_header_rewrite_clients= - -o smtpd_milters= - -o local_recipient_maps= - -o relay_recipient_maps= diff --git a/data/templates/postgrey/postgrey.default b/data/templates/postgrey/postgrey.default deleted file mode 100644 index 1af70c149..000000000 --- a/data/templates/postgrey/postgrey.default +++ /dev/null @@ -1,12 +0,0 @@ -# postgrey startup options, created for Debian - -# you may want to set -# --delay=N how long to greylist, seconds (default: 300) -# --max-age=N delete old entries after N days (default: 35) -# see also the postgrey(8) manpage - -POSTGREY_OPTS="--inet=10023 --delay=30" - -# the --greylist-text commandline argument can not be easily passed through -# POSTGREY_OPTS when it contains spaces. So, insert your text here: -#POSTGREY_TEXT="Your customized rejection message here" diff --git a/data/templates/rmilter/rmilter.conf b/data/templates/rmilter/rmilter.conf new file mode 100644 index 000000000..d585b9217 --- /dev/null +++ b/data/templates/rmilter/rmilter.conf @@ -0,0 +1,18 @@ +# systemd-specific settings for rmilter + +.include /etc/rmilter.conf.common + +# pidfile - path to pid file +pidfile = /run/rmilter/rmilter.pid; + +# rmilter is socket-activated under systemd +bind_socket = fd:3; + +# DKIM signing +dkim { + domain { + key = /etc/dkim; + domain = "*"; + selector = "mail"; + }; +}; diff --git a/data/templates/rmilter/rmilter.socket b/data/templates/rmilter/rmilter.socket new file mode 100644 index 000000000..dc3ae7a2a --- /dev/null +++ b/data/templates/rmilter/rmilter.socket @@ -0,0 +1,5 @@ +.include /lib/systemd/system/rmilter.socket + +[Socket] +ListenStream= +ListenStream=127.0.0.1:11000 diff --git a/data/templates/rspamd/metrics.conf b/data/templates/rspamd/metrics.conf new file mode 100644 index 000000000..1236b2a3e --- /dev/null +++ b/data/templates/rspamd/metrics.conf @@ -0,0 +1,1163 @@ +# Metrics settings + +metric { + name = "default"; + # If this param is set to non-zero + # then a metric would accept all symbols + # unknown_weight = 1.0 + + actions { + reject = 21; + add_header = 8; + greylist = 4; + }; + + group { + name = "header"; + symbol { + weight = 2.0; + description = "Subject is missing inside message"; + name = "MISSING_SUBJECT"; + } + symbol { + weight = 2.100000; + description = "Message pretends to be send from Outlook but has 'strange' tags "; + name = "FORGED_OUTLOOK_TAGS"; + } + symbol { + weight = 0.30; + description = "Sender is forged (different From: header and smtp MAIL FROM: addresses)"; + name = "FORGED_SENDER"; + } + symbol { + weight = 3.500000; + description = "Recipients seems to be autogenerated (works if recipients count is more than 5)"; + name = "SUSPICIOUS_RECIPS"; + } + symbol { + weight = 6.0; + description = "Fake reply (has RE in subject, but has not References header)"; + name = "FAKE_REPLY_C"; + } + symbol { + weight = 1.0; + description = "Messages that have only HTML part"; + name = "MIME_HTML_ONLY"; + } + symbol { + weight = 2.0; + description = "Forged yahoo msgid"; + name = "FORGED_MSGID_YAHOO"; + } + symbol { + weight = 2.0; + description = "Forged The Bat! MUA headers"; + name = "FORGED_MUA_THEBAT_BOUN"; + } + symbol { + weight = 5.0; + description = "Charset is missing in a message"; + name = "R_MISSING_CHARSET"; + } + symbol { + weight = 2.0; + description = "Two received headers with ip addresses"; + name = "RCVD_DOUBLE_IP_SPAM"; + } + symbol { + weight = 5.0; + description = "Forged outlook HTML signature"; + name = "FORGED_OUTLOOK_HTML"; + } + symbol { + weight = 5.0; + description = "Recipients are absent or undisclosed"; + name = "R_UNDISC_RCPT"; + } + symbol { + weight = 2.0; + description = "Fake helo for verizon provider"; + name = "FM_FAKE_HELO_VERIZON"; + } + symbol { + weight = 2.0; + description = "Quoted reply-to from yahoo (seems to be forged)"; + name = "REPTO_QUOTE_YAHOO"; + } + symbol { + weight = 5.0; + description = "Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)"; + name = "MISSING_MIMEOLE"; + } + symbol { + weight = 2.0; + description = "To header is missing"; + name = "MISSING_TO"; + } + symbol { + weight = 1.500000; + description = "From that contains encoded characters while base 64 is not needed as all symbols are 7bit"; + name = "FROM_EXCESS_BASE64"; + } + symbol { + weight = 1.200000; + description = "From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; + name = "FROM_EXCESS_QP"; + } + symbol { + weight = 1.500000; + description = "To that contains encoded characters while base 64 is not needed as all symbols are 7bit"; + name = "TO_EXCESS_BASE64"; + } + symbol { + weight = 1.200000; + description = "To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; + name = "TO_EXCESS_QP"; + } + symbol { + weight = 1.500000; + description = "Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit"; + name = "REPLYTO_EXCESS_BASE64"; + } + symbol { + weight = 1.200000; + description = "Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; + name = "REPLYTO_EXCESS_QP"; + } + symbol { + weight = 1.500000; + description = "Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit"; + name = "CC_EXCESS_BASE64"; + } + symbol { + weight = 1.200000; + description = "Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit"; + name = "CC_EXCESS_QP"; + } + symbol { + weight = 5.0; + description = "Mixed characters in a message"; + name = "R_MIXED_CHARSET"; + } + symbol { + weight = 3.500000; + description = "Recipients list seems to be sorted"; + name = "SORTED_RECIPS"; + } + symbol { + weight = 3.0; + description = "Spambots signatures in received headers"; + name = "R_RCVD_SPAMBOTS"; + } + symbol { + weight = 2.0; + description = "To header seems to be autogenerated"; + name = "R_TO_SEEMS_AUTO"; + } + symbol { + weight = 1.0; + description = "Subject needs encoding"; + name = "SUBJECT_NEEDS_ENCODING"; + } + symbol { + weight = 3.840000; + description = "Spam string at the end of message to make statistics faults 0"; + name = "TRACKER_ID"; + } + symbol { + weight = 1.0; + description = "No space in from header"; + name = "R_NO_SPACE_IN_FROM"; + } + symbol { + weight = 8.0; + description = "Subject seems to be spam"; + name = "R_SAJDING"; + } + symbol { + weight = 3.0; + description = "Detects bad content-transfer-encoding for text parts"; + name = "R_BAD_CTE_7BIT"; + } + symbol { + weight = 10.0; + description = "Flash redirect on imageshack.us"; + name = "R_FLASH_REDIR_IMGSHACK"; + } + symbol { + weight = 5.0; + description = "Message id is incorrect"; + name = "INVALID_MSGID"; + } + symbol { + weight = 3.0; + description = "Message id is missing "; + name = "MISSING_MID"; + } + symbol { + weight = 1.0; + description = "Recipients are not the same as RCPT TO: mail command"; + name = "FORGED_RECIPIENTS"; + } + symbol { + weight = 0.0; + description = "Recipients are not the same as RCPT TO: mail command, but a message from a maillist"; + name = "FORGED_RECIPIENTS_MAILLIST"; + } + symbol { + weight = 0.0; + description = "Sender is not the same as MAIL FROM: envelope, but a message is from a maillist"; + name = "FORGED_SENDER_MAILLIST"; + } + symbol { + weight = 2.0; + description = "Forged Exchange messages "; + name = "RATWARE_MS_HASH"; + } + symbol { + weight = 1.0; + description = "Reply-type in content-type"; + name = "STOX_REPLY_TYPE"; + } + symbol { + weight = 0.1; + description = "One received header in a message "; + name = "ONCE_RECEIVED"; + } + symbol { + weight = 2.0; + description = "One received header with 'bad' patterns inside"; + name = "ONCE_RECEIVED_STRICT"; + } + symbol { + weight = 2.0; + description = "Only Content-Type header without other MIME headers"; + name = "MIME_HEADER_CTYPE_ONLY"; + } + symbol { + weight = -1.0; + description = "Message seems to be from maillist"; + name = "MAILLIST"; + } + symbol { + weight = 1.0; + description = "Header From begins with tab"; + name = "HEADER_FROM_DELIMITER_TAB"; + } + symbol { + weight = 1.0; + description = "Header To begins with tab"; + name = "HEADER_TO_DELIMITER_TAB"; + } + symbol { + weight = 1.0; + description = "Header Cc begins with tab"; + name = "HEADER_CC_DELIMITER_TAB"; + } + symbol { + weight = 1.0; + description = "Header Reply-To begins with tab"; + name = "HEADER_REPLYTO_DELIMITER_TAB"; + } + symbol { + weight = 1.0; + description = "Header Date begins with tab"; + name = "HEADER_DATE_DELIMITER_TAB"; + } + symbol { + weight = 1.0; + description = "Header From has no delimiter between header name and header value"; + name = "HEADER_FROM_EMPTY_DELIMITER"; + } + symbol { + weight = 1.0; + description = "Header To has no delimiter between header name and header value"; + name = "HEADER_TO_EMPTY_DELIMITER"; + } + symbol { + weight = 1.0; + description = "Header Cc has no delimiter between header name and header value"; + name = "HEADER_CC_EMPTY_DELIMITER"; + } + symbol { + weight = 1.0; + description = "Header Reply-To has no delimiter between header name and header value"; + name = "HEADER_REPLYTO_EMPTY_DELIMITER"; + } + symbol { + weight = 1.0; + description = "Header Date has no delimiter between header name and header value"; + name = "HEADER_DATE_EMPTY_DELIMITER"; + } + symbol { + weight = 4.0; + description = "Header Received has raw illegal character"; + name = "RCVD_ILLEGAL_CHARS"; + } + symbol { + weight = 4.0; + description = "Fake helo mail.ru in header Received from non mail.ru sender address"; + name = "FAKE_RECEIVED_mail_ru"; + } + symbol { + weight = 4.0; + description = "Fake smtp.yandex.ru Received"; + name = "FAKE_RECEIVED_smtp_yandex_ru"; + } + symbol { + weight = 3.600000; + description = "Forged generic Received"; + name = "FORGED_GENERIC_RECEIVED"; + } + symbol { + weight = 3.600000; + description = "Forged generic Received"; + name = "FORGED_GENERIC_RECEIVED2"; + } + symbol { + weight = 3.600000; + description = "Forged generic Received"; + name = "FORGED_GENERIC_RECEIVED3"; + } + symbol { + weight = 3.600000; + description = "Forged generic Received"; + name = "FORGED_GENERIC_RECEIVED4"; + } + symbol { + weight = 4.600000; + description = "Forged generic Received"; + name = "FORGED_GENERIC_RECEIVED5"; + } + symbol { + weight = 3.0; + description = "Invalid Postfix Received"; + name = "INVALID_POSTFIX_RECEIVED"; + } + symbol { + weight = 5.0; + description = "Invalid Exim Received"; + name = "INVALID_EXIM_RECEIVED"; + } + symbol { + weight = 3.0; + description = "Invalid Exim Received"; + name = "INVALID_EXIM_RECEIVED2"; + } + } + + group { + name = "mua"; + symbol { + weight = 4.0; + description = "Message pretends to be send from The Bat! but has forged Message-ID"; + name = "FORGED_MUA_THEBAT_MSGID"; + } + symbol { + weight = 3.0; + description = "Message pretends to be send from The Bat! but has forged Message-ID"; + name = "FORGED_MUA_THEBAT_MSGID_UNKNOWN"; + } + symbol { + weight = 3.0; + description = "Message pretends to be send from KMail but has forged Message-ID"; + name = "FORGED_MUA_KMAIL_MSGID"; + } + symbol { + weight = 2.500000; + description = "Message pretends to be send from KMail but has forged Message-ID"; + name = "FORGED_MUA_KMAIL_MSGID_UNKNOWN"; + } + symbol { + weight = 4.0; + description = "Message pretends to be send from Opera Mail but has forged Message-ID"; + name = "FORGED_MUA_OPERA_MSGID"; + } + symbol { + weight = 4.0; + description = "Message pretends to be send from suspicious Opera Mail/10.x (Windows) but has forged Message-ID, apparently from KMail"; + name = "SUSPICIOUS_OPERA_10W_MSGID"; + } + symbol { + weight = 4.0; + description = "Message pretends to be send from Mozilla Mail but has forged Message-ID"; + name = "FORGED_MUA_MOZILLA_MAIL_MSGID"; + } + symbol { + weight = 2.500000; + description = "Message pretends to be send from Mozilla Mail but has forged Message-ID"; + name = "FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN"; + } + symbol { + weight = 4.0; + description = "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID"; + name = "FORGED_MUA_THUNDERBIRD_MSGID"; + } + symbol { + weight = 2.500000; + description = "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID"; + name = "FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN"; + } + symbol { + weight = 4.0; + description = "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID"; + name = "FORGED_MUA_SEAMONKEY_MSGID"; + } + symbol { + weight = 2.500000; + description = "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID"; + name = "FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN"; + } + symbol { + weight = 3.0; + description = "Forged outlook MUA"; + name = "FORGED_MUA_OUTLOOK"; + } + } + symbol { + weight = 0.0; + description = "Avoid false positives for FORGED_MUA_* in maillist"; + name = "FORGED_MUA_MAILLIST"; + } + + group { + name = "body"; + symbol { + weight = 9.0; + description = "White color on white background in HTML messages"; + name = "R_WHITE_ON_WHITE"; + } + symbol { + weight = 3.0; + description = "Short html part with a link to an image"; + name = "HTML_SHORT_LINK_IMG_1"; + } + symbol { + weight = 1.0; + description = "Short html part with a link to an image"; + name = "HTML_SHORT_LINK_IMG_2"; + } + symbol { + weight = 0.5; + description = "Short html part with a link to an image"; + name = "HTML_SHORT_LINK_IMG_3"; + } + symbol { + weight = 5.0; + description = "Suspicious boundary in header Content-Type"; + name = "SUSPICIOUS_BOUNDARY"; + } + symbol { + weight = 4.0; + description = "Suspicious boundary in header Content-Type"; + name = "SUSPICIOUS_BOUNDARY2"; + } + symbol { + weight = 3.0; + description = "Suspicious boundary in header Content-Type"; + name = "SUSPICIOUS_BOUNDARY3"; + } + symbol { + weight = 4.0; + description = "Suspicious boundary in header Content-Type"; + name = "SUSPICIOUS_BOUNDARY4"; + } + symbol { + weight = 3.0; + description = "Text and HTML parts differ"; + name = "R_PARTS_DIFFER"; + } + + symbol { + weight = 2.0; + description = "Message contains empty parts and image"; + name = "R_EMPTY_IMAGE"; + } + symbol { + weight = 2.0; + description = "Drugs patterns inside message"; + name = "DRUGS_MANYKINDS"; + } + symbol { + weight = 2.0; + description = ""; + name = "DRUGS_ANXIETY"; + } + symbol { + weight = 2.0; + description = ""; + name = "DRUGS_MUSCLE"; + } + symbol { + weight = 2.0; + description = ""; + name = "DRUGS_ANXIETY_EREC"; + } + symbol { + weight = 2.0; + description = ""; + name = "DRUGS_DIET"; + } + symbol { + weight = 2.0; + description = ""; + name = "DRUGS_ERECTILE"; + } + symbol { + weight = 3.300000; + description = "2 'advance fee' patterns in a message"; + name = "ADVANCE_FEE_2"; + } + symbol { + weight = 2.120000; + description = "3 'advance fee' patterns in a message"; + name = "ADVANCE_FEE_3"; + } + symbol { + weight = 8.0; + description = "Lotto signatures"; + name = "R_LOTTO"; + } + } + + group { + name = "rbl"; + symbol { + name = "DNSWL_BLOCKED"; + weight = 0.0; + description = "Resolver blocked due to excessive queries"; + } + symbol { + name = "RCVD_IN_DNSWL"; + weight = 0.0; + description = "Unrecognised result from dnswl.org"; + } + symbol { + name = "RCVD_IN_DNSWL_NONE"; + weight = 0.0; + description = "Sender listed at http://www.dnswl.org, low none"; + } + symbol { + name = "RCVD_IN_DNSWL_LOW"; + weight = 0.0; + description = "Sender listed at http://www.dnswl.org, low trust"; + } + symbol { + name = "RCVD_IN_DNSWL_MED"; + weight = 0.0; + description = "Sender listed at http://www.dnswl.org, medium trust"; + } + symbol { + name = "RCVD_IN_DNSWL_HI"; + weight = 0.0; + description = "Sender listed at http://www.dnswl.org, high trust"; + } + + symbol { + name = "RBL_SPAMHAUS"; + weight = 0.0; + description = "Unrecognised result from Spamhaus zen"; + } + symbol { + name = "RBL_SPAMHAUS_SBL"; + weight = 2.0; + description = "From address is listed in zen sbl"; + } + symbol { + name = "RBL_SPAMHAUS_CSS"; + weight = 2.0; + description = "From address is listed in zen css"; + } + symbol { + name = "RBL_SPAMHAUS_XBL"; + weight = 4.0; + description = "From address is listed in zen xbl"; + } + symbol { + name = "RBL_SPAMHAUS_PBL"; + weight = 2.0; + description = "From address is listed in zen pbl"; + } + symbol { + name = "RECEIVED_SPAMHAUS_XBL"; + weight = 3.0; + description = "Received address is listed in zen pbl"; + one_shot = true; + } + + symbol { + name = "RWL_SPAMHAUS_WL"; + weight = 0.0; + description = "Unrecognised result from Spamhaus whitelist"; + } + symbol { + name = "RWL_SPAMHAUS_WL_IND"; + weight = 0.0; + description = "Sender listed at Spamhaus whitelist"; + } + symbol { + name = "RWL_SPAMHAUS_WL_TRANS"; + weight = 0.0; + description = "Sender listed at Spamhaus whitelist"; + } + symbol { + name = "RWL_SPAMHAUS_WL_IND_EXP"; + weight = 0.0; + description = "Sender listed at Spamhaus whitelist"; + } + symbol { + name = "RWL_SPAMHAUS_WL_TRANS_EXP"; + weight = 0.0; + description = "Sender listed at Spamhaus whitelist"; + } + + symbol { + weight = 2.0; + description = "From address is listed in senderscore.com BL"; + name = "RBL_SENDERSCORE"; + } + symbol { + weight = 1.0; + description = "From address is listed in ABUSE.CH BL"; + name = "RBL_ABUSECH"; + } + symbol { + weight = 1.0; + description = "From address is listed in UCEPROTECT LEVEL1 BL"; + name = "RBL_UCEPROTECT_LEVEL1"; + } + symbol { + name = "RBL_MAILSPIKE"; + weight = 0.0; + description = "Unrecognised result from Mailspike blacklist"; + } + symbol { + name = "RWL_MAILSPIKE"; + weight = 0.0; + description = "Unrecognised result from Mailspike whitelist"; + } + symbol { + name = "RBL_MAILSPIKE_ZOMBIE"; + weight = 2.0; + description = "From address is listed in RBL"; + } + symbol { + name = "RBL_MAILSPIKE_WORST"; + weight = 2.0; + description = "From address is listed in RBL"; + } + symbol { + name = "RBL_MAILSPIKE_VERYBAD"; + weight = 1.5; + description = "From address is listed in RBL"; + } + symbol { + name = "RBL_MAILSPIKE_BAD"; + weight = 1.0; + description = "From address is listed in RBL"; + } + symbol { + name = "RWL_MAILSPIKE_POSSIBLE"; + weight = 0.0; + description = "From address is listed in RWL"; + } + symbol { + name = "RWL_MAILSPIKE_GOOD"; + weight = 0.0; + description = "From address is listed in RWL"; + } + symbol { + name = "RWL_MAILSPIKE_VERYGOOD"; + weight = 0.0; + description = "From address is listed in RWL"; + } + symbol { + name = "RWL_MAILSPIKE_EXCELLENT"; + weight = 0.0; + description = "From address is listed in RWL"; + } + + symbol { + weight = 0.0; + name = "RBL_SORBS"; + description = "Unrecognised result from SORBS RBL"; + } + symbol { + weight = 2.5; + name = "RBL_SORBS_HTTP"; + description = "List of Open HTTP Proxy Servers."; + } + symbol { + weight = 2.5; + name = "RBL_SORBS_SOCKS"; + description = "List of Open SOCKS Proxy Servers."; + } + symbol { + weight = 1.0; + name = "RBL_SORBS_MISC"; + description = "List of open Proxy Servers not listed in the SOCKS or HTTP lists."; + } + symbol { + weight = 3.0; + name = "RBL_SORBS_SMTP"; + description = "List of Open SMTP relay servers."; + } + symbol { + weight = 1.5; + name = "RBL_SORBS_RECENT"; + description = "List of hosts that have been noted as sending spam/UCE/UBE to the admins of SORBS within the last 28 days (includes new.spam.dnsbl.sorbs.net)."; + } + symbol { + weight = 0.4; + name = "RBL_SORBS_WEB"; + description = "List of web (WWW) servers which have spammer abusable vulnerabilities (e.g. FormMail scripts)"; + } + symbol { + weight = 2.0; + name = "RBL_SORBS_DUL"; + description = "Dynamic IP Address ranges (NOT a Dial Up list!)"; + } + symbol { + weight = 1.0; + name = "RBL_SORBS_BLOCK"; + description = "List of hosts demanding that they never be tested by SORBS."; + } + symbol { + weight = 1.0; + name = "RBL_SORBS_ZOMBIE"; + description = "List of networks hijacked from their original owners, some of which have already used for spamming."; + } + + symbol { + weight = 1.0; + name = "RBL_SEM"; + description = "Address is listed in Spameatingmonkey RBL"; + } + + symbol { + weight = 1.0; + name = "RBL_SEM_IPV6"; + description = "Address is listed in Spameatingmonkey RBL (ipv6)"; + } + } + + group { + name = "bayes"; + + symbol { + weight = 3.0; + description = "Message probably spam, probability: "; + name = "BAYES_SPAM"; + } + symbol { + weight = -3.0; + description = "Message probably ham, probability: "; + name = "BAYES_HAM"; + } + } + + group { + name = "fuzzy"; + symbol { + weight = 5.0; + description = "Generic fuzzy hash match"; + name = "FUZZY_UNKNOWN"; + } + symbol { + weight = 10.0; + description = "Denied fuzzy hash"; + name = "FUZZY_DENIED"; + } + symbol { + weight = 5.0; + description = "Probable fuzzy hash"; + name = "FUZZY_PROB"; + } + symbol { + weight = -2.1; + description = "Whitelisted fuzzy hash"; + name = "FUZZY_WHITE"; + } + } + + group { + name = "spf"; + symbol { + weight = 1.0; + description = "SPF verification failed"; + name = "R_SPF_FAIL"; + } + symbol { + weight = 0.0; + description = "SPF verification soft-failed"; + name = "R_SPF_SOFTFAIL"; + } + symbol { + weight = 0.0; + description = "SPF policy is neutral"; + name = "R_SPF_NEUTRAL"; + } + symbol { + weight = -1.1; + description = "SPF verification alowed"; + name = "R_SPF_ALLOW"; + } + } + + group { + name = "dkim"; + symbol { + weight = 1.0; + description = "DKIM verification failed"; + name = "R_DKIM_REJECT"; + } + symbol { + weight = 0.0; + description = "DKIM verification soft-failed"; + name = "R_DKIM_TEMPFAIL"; + } + symbol { + weight = -1.1; + description = "DKIM verification succeed"; + name = "R_DKIM_ALLOW"; + one_shot = true; + } + } + + group { + name = "surbl"; + symbol { + weight = 5.500000; + description = "SURBL: Phishing sites"; + name = "PH_SURBL_MULTI"; + } + symbol { + weight = 5.500000; + description = "SURBL: Malware sites"; + name = "MW_SURBL_MULTI"; + } + symbol { + weight = 5.500000; + description = "SURBL: AbuseButler web sites"; + name = "AB_SURBL_MULTI"; + } + symbol { + weight = 5.500000; + description = "SURBL: SpamCop web sites"; + name = "SC_SURBL_MULTI"; + } + symbol { + weight = 5.500000; + description = "SURBL: jwSpamSpy + Prolocation sites"; + name = "JP_SURBL_MULTI"; + } + symbol { + weight = 5.500000; + description = "SURBL: sa-blacklist web sites "; + name = "WS_SURBL_MULTI"; + } + symbol { + weight = 4.500000; + description = "rambler.ru uribl"; + name = "RAMBLER_URIBL"; + } + + symbol { + weight = 0.0; + name = "SEM_URIBL_UNKNOWN"; + description = "Spameatingmonkey uribl unknown"; + } + symbol { + weight = 3.5; + name = "SEM_URIBL"; + description = "Spameatingmonkey uribl"; + } + + symbol { + weight = 0.0; + name = "SEM_URIBL_FRESH15_UNKNOWN"; + description = "Spameatingmonkey uribl unknown"; + } + symbol { + weight = 3.0; + name = "SEM_URIBL_FRESH15"; + description = "Spameatingmonkey uribl. Domains registered in the last 15 days (.AERO,.BIZ,.COM,.INFO,.NAME,.NET,.PRO,.SK,.TEL,.US)"; + } + + symbol { + weight = 0.000000; + description = "DBL uribl unknown symbol (error)"; + name = "DBL"; + } + symbol { + weight = 6.500000; + description = "DBL uribl spam"; + name = "DBL_SPAM"; + } + symbol { + weight = 6.500000; + description = "DBL uribl phishing"; + name = "DBL_PHISH"; + } + symbol { + weight = 6.500000; + description = "DBL uribl malware"; + name = "DBL_MALWARE"; + } + symbol { + weight = 5.500000; + description = "DBL uribl botnet C&C domain"; + name = "DBL_BOTNET"; + } + symbol { + weight = 6.500000; + description = "DBL uribl abused legit spam"; + name = "DBL_ABUSE"; + } + symbol { + weight = 1.500000; + description = "DBL uribl abused spammed redirector domain"; + name = "DBL_ABUSE_REDIR"; + } + symbol { + weight = 7.500000; + description = "DBL uribl abused legit phish"; + name = "DBL_ABUSE_PHISH"; + } + symbol { + weight = 7.500000; + description = "DBL uribl abused legit malware"; + name = "DBL_ABUSE_MALWARE"; + } + symbol { + weight = 5.500000; + description = "DBL uribl abused legit botnet C&C"; + name = "DBL_ABUSE_BOTNET"; + } + symbol { + weight = 0.00000; + description = "DBL uribl IP queries prohibited!"; + name = "DBL_PROHIBIT"; + } + symbol { + weight = 7.5; + description = "uribl.com black url"; + name = "URIBL_BLACK"; + } + symbol { + weight = 3.5; + description = "uribl.com red url"; + name = "URIBL_RED"; + } + symbol { + weight = 1.5; + description = "uribl.com grey url"; + name = "URIBL_GREY"; + } + symbol { + weight = 9.500000; + description = "rambler.ru emailbl"; + name = "RAMBLER_EMAILBL"; + } + } + + group { + name = "phishing"; + + symbol { + weight = 5.0; + description = "Phished mail"; + name = "PHISHING"; + } + } + + group { + name = "date"; + + symbol { + weight = 4.0; + description = "Message date is in future"; + name = "DATE_IN_FUTURE"; + } + symbol { + weight = 1.0; + description = "Message date is in past"; + name = "DATE_IN_PAST"; + } + symbol { + weight = 1.0; + description = "Message date is missing"; + name = "MISSING_DATE"; + } + } + + group { + name = "hfilter"; + + symbol { + weight = 3.00; + name = "HFILTER_HELO_BAREIP"; + description = "Helo host is bare ip"; + } + symbol { + weight = 4.50; + name = "HFILTER_HELO_BADIP"; + description = "Helo host is very bad ip"; + } + symbol { + weight = 2.00; + name = "HFILTER_HELO_UNKNOWN"; + description = "Helo host empty or unknown"; + } + symbol { + weight = 0.5; + name = "HFILTER_HELO_1"; + description = "Helo host checks (very low)"; + } + symbol { + weight = 1.00; + name = "HFILTER_HELO_2"; + description = "Helo host checks (low)"; + } + symbol { + weight = 2.00; + name = "HFILTER_HELO_3"; + description = "Helo host checks (medium)"; + } + symbol { + weight = 2.50; + name = "HFILTER_HELO_4"; + description = "Helo host checks (hard)"; + } + symbol { + weight = 3.00; + name = "HFILTER_HELO_5"; + description = "Helo host checks (very hard)"; + } + symbol { + weight = 0.5; + name = "HFILTER_HOSTNAME_1"; + description = "Hostname checks (very low)"; + } + symbol { + weight = 1.00; + name = "HFILTER_HOSTNAME_2"; + description = "Hostname checks (low)"; + } + symbol { + weight = 2.00; + name = "HFILTER_HOSTNAME_3"; + description = "Hostname checks (medium)"; + } + symbol { + weight = 2.50; + name = "HFILTER_HOSTNAME_4"; + description = "Hostname checks (hard)"; + } + symbol { + weight = 3.00; + name = "HFILTER_HOSTNAME_5"; + description = "Hostname checks (very hard)"; + } + symbol { + weight = 0.20; + name = "HFILTER_HELO_NORESOLVE_MX"; + description = "MX found in Helo and no resolve"; + } + symbol { + weight = 0.3; + name = "HFILTER_HELO_NORES_A_OR_MX"; + description = "Helo no resolve to A or MX"; + } + symbol { + weight = 1.00; + name = "HFILTER_HELO_IP_A"; + description = "Helo A IP != hostname IP"; + } + symbol { + weight = 2.00; + name = "HFILTER_HELO_NOT_FQDN"; + description = "Helo not FQDN"; + } + symbol { + weight = 0.5; + name = "HFILTER_FROMHOST_NORESOLVE_MX"; + description = "MX found in FROM host and no resolve"; + } + symbol { + weight = 1.50; + name = "HFILTER_FROMHOST_NORES_A_OR_MX"; + description = "FROM host no resolve to A or MX"; + } + symbol { + weight = 3.00; + name = "HFILTER_FROMHOST_NOT_FQDN"; + description = "FROM host not FQDN"; + } + symbol { + weight = 0.00; + name = "HFILTER_FROM_BOUNCE"; + description = "Bounce message"; + } + symbol { + weight = 0.50; + name = "HFILTER_MID_NORESOLVE_MX"; + description = "MX found in Message-id host and no resolve"; + } + symbol { + weight = 0.50; + name = "HFILTER_MID_NORES_A_OR_MX"; + description = "Message-id host no resolve to A or MX"; + } + symbol { + weight = 0.50; + name = "HFILTER_MID_NOT_FQDN"; + description = "Message-id host not FQDN"; + } + symbol { + weight = 4.00; + name = "HFILTER_HOSTNAME_UNKNOWN"; + description = "Unknown hostname (no PTR or no resolve PTR to hostname)"; + } + symbol { + weight = 1.50; + name = "HFILTER_RCPT_BOUNCEMOREONE"; + description = "Message from bounce and over 1 recepient"; + } + symbol { + weight = 3.50; + name = "HFILTER_URL_ONLY"; + description = "URL only in body"; + } + symbol { + weight = 2.20; + name = "HFILTER_URL_ONELINE"; + description = "One line URL and text in body"; + } + } + + group { + name = "dmarc"; + + symbol { + weight = -1.0; + name = "DMARC_POLICY_ALLOW"; + description = "DMARC permit policy"; + } + symbol { + weight = 2.0; + name = "DMARC_POLICY_REJECT"; + description = "DMARC reject policy"; + } + symbol { + weight = 1.5; + name = "DMARC_POLICY_QUARANTINE"; + description = "DMARC quarantine policy"; + } + symbol { + weight = 0.1; + name = "DMARC_POLICY_SOFTFAIL"; + description = "DMARC failed"; + } + } +} diff --git a/data/templates/rspamd/rspamd.sieve b/data/templates/rspamd/rspamd.sieve new file mode 100644 index 000000000..38943eefa --- /dev/null +++ b/data/templates/rspamd/rspamd.sieve @@ -0,0 +1,4 @@ +require ["fileinto"]; +if header :is "X-Spam" "yes" { + fileinto "Junk"; +} diff --git a/data/templates/slapd/slapd.conf b/data/templates/slapd/slapd.conf index f47e6761b..9a8800d9d 100644 --- a/data/templates/slapd/slapd.conf +++ b/data/templates/slapd/slapd.conf @@ -22,14 +22,15 @@ pidfile /var/run/slapd/slapd.pid # List of arguments that were passed to the server argsfile /var/run/slapd/slapd.args -password-hash {SSHA} - # Read slapd.conf(5) for possible values -loglevel 256 +loglevel none + +# Hashes to be used in generation of user passwords +password-hash {SSHA} # Where the dynamically loaded modules are stored modulepath /usr/lib/ldap -moduleload back_hdb +moduleload back_mdb moduleload memberof # The maximum number of entries that is returned for a search operation @@ -40,52 +41,31 @@ sizelimit 500 tool-threads 1 ####################################################################### -# Specific Backend Directives for hdb: +# Specific Backend Directives for mdb: # Backend specific directives apply to this backend until another # 'backend' directive occurs -backend hdb +backend mdb ####################################################################### -# Specific Backend Directives for 'other': -# Backend specific directives apply to this backend until another -# 'backend' directive occurs -#backend - -####################################################################### -# Specific Directives for database #1, of type hdb: +# Specific Directives for database #1, of type mdb: # Database specific directives apply to this databasse until another # 'database' directive occurs -database hdb +database mdb # The base of your directory in database #1 suffix "dc=yunohost,dc=org" +# rootdn directive for specifying a superuser on the database. This is needed +# for syncrepl. +# rootdn "cn=admin,dc=yunohost,dc=org" + +# Where the database file are physically stored for database #1 directory "/var/lib/ldap" -# The dbconfig settings are used to generate a DB_CONFIG file the first -# time slapd starts. They do NOT override existing an existing DB_CONFIG -# file. You should therefore change these settings in DB_CONFIG directly -# or remove DB_CONFIG and restart slapd for changes to take effect. - -# For the Debian package we use 2MB as default but be sure to update this -# value if you have plenty of RAM -dbconfig set_cachesize 0 2097152 0 - -# Sven Hartge reported that he had to set this value incredibly high -# to get slapd running at all. See http://bugs.debian.org/303057 for more -# information. - -# Number of objects that can be locked at the same time. -dbconfig set_lk_max_objects 1500 -# Number of locks (both requested and granted) -dbconfig set_lk_max_locks 1500 -# Number of lockers -dbconfig set_lk_max_lockers 1500 - # Indexing options for database #1 -index objectClass eq -index uid eq,sub -index entryCSN,entryUUID eq +index objectClass eq +index uid eq,sub +index entryCSN,entryUUID eq # Save the time that the entry gets modified, for database #1 lastmod on @@ -94,26 +74,25 @@ lastmod on # failure and to speed slapd shutdown. checkpoint 512 30 -# Where to store the replica logs for database #1 -# replogfile /var/lib/ldap/replog - # The userPassword by default can be changed # by the entry owning it if they are authenticated. # Others should not be able to see it, except the # admin entry below # These access lines apply to database #1 only -access to attrs=userPassword +access to attrs=userPassword,shadowLastChange by dn="cn=admin,dc=yunohost,dc=org" write - by anonymous auth + by anonymous auth by self write by * none +# Personnal information can be changed by the entry +# owning it if they are authenticated. +# Others should be able to see it. access to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn by dn="cn=admin,dc=yunohost,dc=org" write by self write by * read - # Ensure read access to the base for things like # supportedSASLMechanisms. Without this you may # have problems with SASL not knowing what @@ -129,14 +108,5 @@ access to dn.base="" by * read # can read everything. access to * by dn="cn=admin,dc=yunohost,dc=org" write - by group/groupOfNames/Member="cn=admin,ou=groups,dc=yunohost,dc=org" write - by * read - -####################################################################### -# Specific Directives for database #2, of type 'other' (can be hdb too): -# Database specific directives apply to this databasse until another -# 'database' directive occurs -#database - -# The base of your directory for database #2 -#suffix "dc=debian,dc=org" + by group/groupOfNames/Member="cn=admin,ou=groups,dc=yunohost,dc=org" write + by * read diff --git a/data/templates/spamassassin/local.cf b/data/templates/spamassassin/local.cf deleted file mode 100644 index bc37b3a60..000000000 --- a/data/templates/spamassassin/local.cf +++ /dev/null @@ -1,94 +0,0 @@ -# This is the right place to customize your installation of SpamAssassin. -report_safe 0 -lock_method flock - -# Bayes-related operations -use_bayes 1 -use_bayes_rules 1 -bayes_auto_learn 1 -bayes_auto_expire 1 -bayes_path /var/lib/amavis/.spamassassin/bayes -bayes_file_mode 0777 - -# External network tests -dns_available yes -skip_rbl_checks 0 -use_razor2 1 -use_pyzor 1 - -# Use URIBL (http://www.uribl.com/about.shtml) -urirhssub URIBL_BLACK multi.uribl.com. A 2 -body URIBL_BLACK eval:check_uridnsbl('URIBL_BLACK') -describe URIBL_BLACK Contains an URL listed in the URIBL blacklist -tflags URIBL_BLACK net -score URIBL_BLACK 3.0 - -urirhssub URIBL_GREY multi.uribl.com. A 4 -body URIBL_GREY eval:check_uridnsbl('URIBL_GREY') -describe URIBL_GREY Contains an URL listed in the URIBL greylist -tflags URIBL_GREY net -score URIBL_GREY 0.25 - -# Use SURBL (http://www.surbl.org/) -urirhssub URIBL_JP_SURBL multi.surbl.org. A 64 -body URIBL_JP_SURBL eval:check_uridnsbl('URIBL_JP_SURBL') -describe URIBL_JP_SURBL Has URI in JP at http://www.surbl.org/lists.html -tflags URIBL_JP_SURBL net -score URIBL_JP_SURBL 3.0 - - -score SPF_FAIL 10.000 -score SPF_HELO_FAIL 10.000 -score RAZOR2_CHECK 2.500 -score RAZOR2_CF_RANGE_51_100 3.500 -# -# See 'perldoc Mail::SpamAssassin::Conf' for details of what can be -# tweaked. -# -# Only a small subset of options are listed below -# -########################################################################### - -# Add *****SPAM***** to the Subject header of spam e-mails -# -# rewrite_header Subject *****SPAM***** - - -# Save spam messages as a message/rfc822 MIME attachment instead of -# modifying the original message (0: off, 2: use text/plain instead) -# -# report_safe 1 - - -# Set which networks or hosts are considered 'trusted' by your mail -# server (i.e. not spammers) -# -# trusted_networks 212.17.35. - - -# Set file-locking method (flock is not safe over NFS, but is faster) -# -# lock_method flock - - -# Set the threshold at which a message is considered spam (default: 5.0) -# -# required_score 5.0 - - -# Use Bayesian classifier (default: 1) -# -# use_bayes 1 - - -# Bayesian classifier auto-learning (default: 1) -# -# bayes_auto_learn 1 - - -# Set headers which may provide inappropriate cues to the Bayesian -# classifier -# -# bayes_ignore_header X-Bogosity -# bayes_ignore_header X-Spam-Flag -# bayes_ignore_header X-Spam-Status diff --git a/data/templates/spamassassin/spamassassin.default b/data/templates/spamassassin/spamassassin.default deleted file mode 100644 index da1b33110..000000000 --- a/data/templates/spamassassin/spamassassin.default +++ /dev/null @@ -1,31 +0,0 @@ -# /etc/default/spamassassin -# Duncan Findlay - -# WARNING: please read README.spamd before using. -# There may be security risks. - -# Change to one to enable spamd -ENABLED=0 - -# Options -# See man spamd for possible options. The -d option is automatically added. - -# SpamAssassin uses a preforking model, so be careful! You need to -# make sure --max-children is not set to anything higher than 5, -# unless you know what you're doing. - -OPTIONS="--create-prefs --max-children 5 --helper-home-dir" - -# Pid file -# Where should spamd write its PID to file? If you use the -u or -# --username option above, this needs to be writable by that user. -# Otherwise, the init script will not be able to shut spamd down. -PIDFILE="/var/run/spamd.pid" - -# Set nice level of spamd -#NICE="--nicelevel 15" - -# Cronjob -# Set to anything but 0 to enable the cron job to automatically update -# spamassassin's rules on a nightly basis -CRON=1 diff --git a/data/templates/udisks-glue/udisks-glue.conf b/data/templates/udisks-glue/udisks-glue.conf deleted file mode 100644 index f97de948f..000000000 --- a/data/templates/udisks-glue/udisks-glue.conf +++ /dev/null @@ -1,9 +0,0 @@ -filter disks { - optical = false - partition_table = false - usage = filesystem -} -match disks { - automount=true - automount_options= { sync, noatime, "dmask=0", "fmask=0" } -} diff --git a/data/templates/yunohost/services.yml b/data/templates/yunohost/services.yml index c015c8cdd..f8dc324d3 100644 --- a/data/templates/yunohost/services.yml +++ b/data/templates/yunohost/services.yml @@ -13,6 +13,10 @@ dovecot: postfix: status: service log: [/var/log/mail.log,/var/log/mail.err] +rmilter: + status: systemctl status rmilter.socket +rspamd: + status: systemctl status rspamd.socket mysql: status: service log: [/var/log/mysql.log,/var/log/mysql.err] @@ -32,11 +36,10 @@ php5-fpm: log: /var/log/php5-fpm.log yunohost-api: status: service - log: /var/log/yunohost.log -postgrey: + log: /var/log/yunohost/yunohost-api.log +yunohost-firewall: status: service - log: /var/log/mail.log -amavis: +postgrey: status: service log: /var/log/mail.log nslcd: @@ -44,8 +47,5 @@ nslcd: log: /var/log/syslog nsswitch: status: service -spamassassin: - status: service - log: /var/log/mail.log -udisks-glue: +udisks2: status: service diff --git a/debian/changelog b/debian/changelog index c8d5a231a..f0b594853 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,175 @@ +yunohost (2.3.7) testing; urgency=low + + [ Laurent Peuch ] + * [enh] new command to generate DNS configuration for a given domain name + + [ Jérôme Lebleu ] + * [fix] Save LDAP database when switching to MDB (bugfix #169) + * [fix] Review LDAP backup and restore hooks + * [enh] Replace msignals.display by logging in backup category + * [enh] Add a ynh_app_setting_delete helper + * [enh] Update rmilter hook and dependencies for 1.7 release + * [enh] Set minimum uid and ignore local users in nslcd.conf + * [enh] Use a common function to retrieve app settings + * [enh] Check the slapd config file at first in conf_regen + * [fix] Validate arguments and app settings in app_map (bugfix #168) + * [fix] Replace udisks-glue by udisks2 and only suggest it + * [fix] Correct condition syntax in metronome conf_regen hook + * [fix] Allow false and 0 as non-empty values for an app argument + * [fix] Some improvements and fixes to actions related to app access + * [fix] Remove old services and add rmilter/rspamd + * [fix] Correct log file of yunohost-api in services.yml + * [i18n] Use named variables in app category translations + + -- Jérôme Lebleu Sun, 07 Feb 2016 18:56:13 +0100 + +yunohost (2.3.6) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Pass app id to scripts and remove hook_check action + * [enh] Rely only on app_id argument for multi-instances apps + * [enh] Add support for app argument 'type' defined in the manifest + * [enh] Integrate 'optional' key of arguments in app manifest + * [enh] Implement 'boolean' argument type support in app manifest + * [enh] Add ping util as recommended package + * [enh] Add a helper to check if a user exists on the system + * [enh] Provide bash helpers for packages manipulation (wip #97) + * [enh] Add ynh_package_update helper and call it in install_from_equivs + * [fix] Do not block while set main domain + * [fix] Add GRANT OPTION in ynh_mysql_create_db helper + * [fix] Validate app argument choice for input value too + * [fix] Log rotation is already handled by WatchedFileHandler (fixbug #137) + * [fix] Use rmilter as a socket-activated service + * [fix] Parse app arguments before creating app folder and settings + * [fix] Use INFO logging level if app setting is not found + * [fix] Split service_configuration_conflict translation key (fixbug #136) + * [fix] Set default value of boolean argument type to false if unset + * [fix] Remove useless SPF setting in Postfix configuration (fixbug #150) + * [fix] Add procmail to packages dependencies + * [i18n] Review translations and keys related to app arguments + + [ Sebastien Badia ] + * hooks: Use a more elegant grep command for mysql process check + + -- Jérôme Lebleu Sun, 17 Jan 2016 02:57:53 +0100 + +yunohost (2.3.5) testing; urgency=low + + [ opi ] + * [enh] Get app label for installed app in app list + * [enh] Short cache on handlebars templates + + [ Jérôme Lebleu ] + * [enh] Allow to pass the admin password as argument in the cli + * [enh] Add main domain GET route + * [enh] Provide bash helpers for MySQL databases and app settings (wip #97) + * [enh] Rename ynh_password bash helper to ynh_string_random + * [fix] Check app min_version with yunohost package (fixbug #113) + * [fix] Use --output-as instead of deprecated options + * [fix] Prevent error if unset variable is treated in utils helper + * [doc] Improve usage and add examples for user helpers + * [i18n] Update translations from Transifex belatedly + + -- Jérôme Lebleu Thu, 24 Dec 2015 10:55:36 +0100 + +yunohost (2.3.4) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Make use of call_async_output in hook_exec to get output in real time + * [fix] Display a more detailed message when yunohost-firewall is stopped + * [fix] Prevent insserv warning when using systemd at package postinst + * [fix] Log real exception string error in hook_callback + * [fix] Add yunohost-firewall.service but do not enable it + + [ julienmalik ] + * [fix] Log for rmilter instead of rspamd + * [fix] Do not exit at first service which can't be stopped + + -- Jérôme Lebleu Tue, 17 Nov 2015 11:10:42 +0100 + +yunohost (2.3.3) testing; urgency=low + + * [fix] Do not modify handlers with root_handlers in bin/yunohost + + -- Jérôme Lebleu Sun, 15 Nov 2015 15:00:04 +0100 + +yunohost (2.3.2) testing; urgency=low + + [ Jérôme Lebleu ] + * [fix] Do not rely on dh_installinit and restart service after upgrade + * [fix] Add tty in root handlers if debug is set in bin/yunohost + + [ kload ] + * [fix] Do not remove the global_script directory + * [fix] Unexpected warnings comming from stderr + * [enh] Warn the user about the waiting at the configuration generation + * [fix] Delayed upgrade of the package 'yunohost' + + -- Jérôme Lebleu Sun, 15 Nov 2015 14:03:39 +0100 + +yunohost (2.3.1) testing; urgency=low + + [ Jérôme Lebleu ] + * [enh] Add logrotate configuration + * [enh] Allow to set default options for yunohost-api service + * [enh] Add bash completion for bin/yunohost + * [enh] Make use of new logging facilities in firewall, hook and service + * [enh] Refactor bin/yunohost and bin/yunohost-api to follow moulinette + changes and provide help for global arguments + * [enh] Split stdout/stderr wrapping in hook_exec and add a no_trace option + * [fix] Create home directory during login (fixbug #80) + * [fix] Keep compat with deprecated --plain and --json in the cli + * [fix] Do not restrict warning to tty in service_saferemove + * [fix] Enable yunohost-api systemd service manually + + [ kload ] + * [fix] Restart Dovecot at the end of Rspamd configuration script + * [fix] Translate regenconf messages in English and French + + -- Jérôme Lebleu Sun, 15 Nov 2015 00:23:27 +0100 + +yunohost (2.3.0) testing; urgency=low + + [ breaking changes ] + * Merge all packages into one + * Wheezy compatibility drop + + [ features ] + * Implement a regenconf command + * Implement local backup/restore functions + * Allow to filter which app to backup/restore + * Replace the email stack by Rspamd/Rmilter + * Create shallow clone to increase app installation time + * Add helper bash functions for apps developers + * Update app_info to show app installation status + * Implement an app_debug function + * IPv6 compatibility enhancement + + [ bugfixes ] + * Display YunoHost packages versions (fix #11) + * Allow empty app arguments in app_install + * Invalidate passwd at user creation/deletion (fix #70) + * Fix skipped_urls for each domain and #68 + * Correct logger object in backup_list (fix #75) + * 2nd installation of apps with a hooks directory + * Add netcat-openbsd dependency + * Ensure that arguments are passed to the hook as string + * Use SSL/TLS to fetch app list + * IPv6 record in DynDNS + * Use sudo to execute hook script + * Debian postinst script : only respond to configure + * Handle SSL generation better + * Ensure that the service yunohost-api is always running + * Sieve permission denied + * Do not enable yunohost-firewall service at install + * Open port 1900 when enabling UPnP (fixes #30) + + [ other ] + * Add AGPL license + * French translation using Weblate + + -- kload Tue, 03 Nov 2015 11:55:19 +0000 + moulinette-yunohost (2.3.1) testing; urgency=low [ Julien Malik ] diff --git a/debian/conf/pam/mkhomedir b/debian/conf/pam/mkhomedir new file mode 100644 index 000000000..eedc8b745 --- /dev/null +++ b/debian/conf/pam/mkhomedir @@ -0,0 +1,6 @@ +Name: Create home directory during login +Default: yes +Priority: 900 +Session-Type: Additional +Session: + required pam_mkhomedir.so umask=0022 skel=/etc/skel diff --git a/debian/control b/debian/control index e9d036216..692878f1e 100644 --- a/debian/control +++ b/debian/control @@ -2,14 +2,15 @@ Source: yunohost Section: utils Priority: extra Maintainer: YunoHost Contributors -Build-Depends: debhelper (>=9), dh-systemd +Build-Depends: debhelper (>=9), dh-systemd, dh-python, python-all (>= 2.7) Standards-Version: 3.9.6 +X-Python-Version: >= 2.7 Homepage: https://yunohost.org/ Package: yunohost Architecture: all -Depends: ${misc:Depends}, ${shlibs:Depends}, - moulinette (>= 2.2.1), +Depends: ${python:Depends}, ${misc:Depends}, + moulinette (>= 2.3.4), python-psutil, python-requests, glances, @@ -27,18 +28,20 @@ Depends: ${misc:Depends}, ${shlibs:Depends}, curl, mariadb-server | mysql-server, php5-mysql | php5-mysqlnd, slapd, ldap-utils, sudo-ldap, libnss-ldapd, - postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, postgrey, + postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved, - amavisd-new, razor, pyzor, dovecot-antispam, spamassassin, fail2ban, + dovecot-antispam, fail2ban, nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl, dnsmasq, openssl, avahi-daemon, - ssowat, metronome + ssowat, metronome, + rspamd, rmilter (>=1.7.0), redis-server, opendkim-tools Recommends: yunohost-admin, bash-completion, rsyslog, ntp, openssh-server, + inetutils-ping | iputils-ping, php5-gd, php5-curl, php-gettext, php5-mcrypt, - udisks-glue, unattended-upgrades, + unattended-upgrades, libdbd-ldap-perl, libnet-dns-perl -Suggests: htop, vim, rsync, acpi-support-base +Suggests: htop, vim, rsync, acpi-support-base, udisks2 Conflicts: iptables-persistent, moulinette-yunohost, yunohost-config, yunohost-config-others, yunohost-config-postfix, @@ -50,9 +53,11 @@ Replaces: moulinette-yunohost, yunohost-config, yunohost-config-dovecot, yunohost-config-slapd, yunohost-config-nginx, yunohost-config-amavis, yunohost-config-mysql, yunohost-predepends -Description: YunoHost installation package - YunoHost aims to make self-hosting accessible to everyone. +Description: manageable and configured self-hosting server + YunoHost aims to make self-hosting accessible to everyone. It configures + an email, Web and IM server alongside a LDAP base. It also provides + facilities to manage users, domains, apps and so. . This package contains YunoHost scripts and binaries to be used by the - moulinette. It allows to manage the server with a command-line tool and - an API. + moulinette. It allows one to manage the server with a command-line tool + and an API. diff --git a/debian/install b/debian/install index 772027be8..57a6830e1 100644 --- a/debian/install +++ b/debian/install @@ -1,8 +1,11 @@ bin/* /usr/bin/ +data/bash-completion.d/yunohost /etc/bash_completion.d/ data/actionsmap/* /usr/share/moulinette/actionsmap/ data/hooks/* /usr/share/yunohost/hooks/ data/other/* /usr/share/yunohost/yunohost-config/moulinette/ data/templates/* /usr/share/yunohost/templates/ data/apps/* /usr/share/yunohost/apps/ -lib/yunohost/*.py /usr/lib/moulinette/yunohost/ +debian/conf/pam/* /usr/share/pam-configs/ +lib/metronome/modules/* /usr/lib/metronome/modules/ locales/* /usr/lib/moulinette/yunohost/locales/ +src/yunohost/*.py /usr/lib/moulinette/yunohost/ diff --git a/debian/logrotate b/debian/logrotate new file mode 100644 index 000000000..4d1cc22df --- /dev/null +++ b/debian/logrotate @@ -0,0 +1,8 @@ +/var/log/yunohost/*.log { + weekly + rotate 4 + delaycompress + compress + notifempty + missingok +} diff --git a/debian/postinst b/debian/postinst index 2a13c8e83..17ccd5b82 100644 --- a/debian/postinst +++ b/debian/postinst @@ -4,16 +4,25 @@ set -e do_configure() { rm -rf /var/cache/moulinette/* - service yunohost-api restart if [ ! -f /etc/yunohost/installed ]; then - bash /usr/share/yunohost/hooks/conf_regen/01-yunohost - bash /usr/share/yunohost/hooks/conf_regen/02-ssl - bash /usr/share/yunohost/hooks/conf_regen/06-slapd - bash /usr/share/yunohost/hooks/conf_regen/15-nginx + bash /usr/share/yunohost/hooks/conf_regen/01-yunohost True + bash /usr/share/yunohost/hooks/conf_regen/02-ssl True + bash /usr/share/yunohost/hooks/conf_regen/06-slapd True + bash /usr/share/yunohost/hooks/conf_regen/15-nginx True else + echo "Regenerating configuration, this might take a while..." yunohost service regenconf + + # restart yunohost-firewall if it's running + service yunohost-firewall status > /dev/null \ + && service yunohost-firewall restart \ + || echo "Service yunohost-firewall is not running, you should " \ + "consider to start it." fi + + # update PAM configs + pam-auth-update --package } # summary of how this script can be called: @@ -38,3 +47,13 @@ case "$1" in exit 1 ;; esac + +# Enable and start yunohost-api service for non-systemd system +if [ -x /etc/init.d/yunohost-api ] && [ ! -d /run/systemd/system ]; then + update-rc.d yunohost-api defaults >/dev/null + invoke-rc.d yunohost-api start || exit $? +fi + +#DEBHELPER# + +exit 0 diff --git a/debian/prerm b/debian/prerm new file mode 100644 index 000000000..01aee685b --- /dev/null +++ b/debian/prerm @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +if [ -x "/etc/init.d/yunohost-firewall" ]; then + invoke-rc.d yunohost-firewall stop || exit $? +fi + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules index 5de55b6d6..51d60b695 100755 --- a/debian/rules +++ b/debian/rules @@ -5,8 +5,14 @@ #export DH_VERBOSE=1 %: - dh ${@} --with=systemd + dh ${@} --with=python2,systemd override_dh_installinit: - dh_installinit --name=yunohost-api - dh_installinit --name=yunohost-firewall + dh_installinit --noscripts + +override_dh_systemd_enable: + dh_systemd_enable --name=yunohost-api + dh_systemd_enable --name=yunohost-firewall --no-enable + +override_dh_systemd_start: + dh_systemd_start --restart-after-upgrade yunohost-api.service diff --git a/debian/yunohost-api.default b/debian/yunohost-api.default new file mode 100644 index 000000000..b6a9e5a99 --- /dev/null +++ b/debian/yunohost-api.default @@ -0,0 +1,4 @@ +# Override yunohost-api options. +# Example to log debug: DAEMON_OPTS="--debug" +# +#DAEMON_OPTS="" diff --git a/debian/yunohost-api.init b/debian/yunohost-api.init index 0a27554d2..3cda507e6 100755 --- a/debian/yunohost-api.init +++ b/debian/yunohost-api.init @@ -21,6 +21,11 @@ PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME LOGFILE=/var/log/$NAME.log +# Include yunohost-api defaults if available +if [ -r /etc/default/yunohost-api ]; then + . /etc/default/yunohost-api +fi + # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 @@ -45,7 +50,7 @@ do_start() || return 1 start-stop-daemon --start --background --make-pidfile --quiet --no-close \ --pidfile $PIDFILE --exec $DAEMON -- \ - $DAEMON_ARGS >>$LOGFILE 2>&1 \ + $DAEMON_OPTS >>$LOGFILE 2>&1 \ || return 2 } diff --git a/debian/yunohost-api.service b/debian/yunohost-api.service index c7a913b61..4e71eadac 100644 --- a/debian/yunohost-api.service +++ b/debian/yunohost-api.service @@ -4,7 +4,9 @@ After=network.target [Service] Type=simple -ExecStart=/usr/bin/yunohost-api +Environment=DAEMON_OPTS= +EnvironmentFile=-/etc/default/yunohost-api +ExecStart=/usr/bin/yunohost-api $DAEMON_OPTS ExecReload=/bin/kill -HUP $MAINPID Restart=always RestartSec=1 diff --git a/debian/yunohost-firewall.service b/debian/yunohost-firewall.service new file mode 100644 index 000000000..f61ca1f64 --- /dev/null +++ b/debian/yunohost-firewall.service @@ -0,0 +1,11 @@ +[Unit] +Description=YunoHost Firewall +Requires=network.target +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/yunohost firewall reload +ExecReload=/usr/bin/yunohost firewall reload +ExecStop=/usr/bin/yunohost firewall stop +RemainAfterExit=yes diff --git a/data/templates/metronome/modules/ldap.lib.lua b/lib/metronome/modules/ldap.lib.lua similarity index 100% rename from data/templates/metronome/modules/ldap.lib.lua rename to lib/metronome/modules/ldap.lib.lua diff --git a/lib/metronome/modules/mod_auth_ldap2.lua b/lib/metronome/modules/mod_auth_ldap2.lua new file mode 100644 index 000000000..bb62ca546 --- /dev/null +++ b/lib/metronome/modules/mod_auth_ldap2.lua @@ -0,0 +1,89 @@ +-- vim:sts=4 sw=4 + +-- Metronome IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2012 Rob Hoelz +-- Copyright (C) 2015 YUNOHOST.ORG +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- +-- https://github.com/YunoHost/yunohost-config-metronome/blob/unstable/lib/modules/mod_auth_ldap2.lua +-- adapted to use common LDAP store on Metronome + +local ldap = module:require 'ldap'; +local new_sasl = require 'util.sasl'.new; +local jsplit = require 'util.jid'.split; + +local log = module._log + +if not ldap then + return; +end + +function new_default_provider(host) + local provider = { name = "ldap2" }; + log("debug", "initializing ldap2 authentication provider for host '%s'", host); + + function provider.test_password(username, password) + return ldap.bind(username, password); + end + + function provider.user_exists(username) + local params = ldap.getparams() + + local filter = ldap.filter.combine_and(params.user.filter, params.user.usernamefield .. '=' .. username); + if params.user.usernamefield == 'mail' then + filter = ldap.filter.combine_and(params.user.filter, 'mail=' .. username .. '@*'); + end + + return ldap.singlematch { + base = params.user.basedn, + filter = filter, + }; + end + + function provider.get_password(username) + return nil, "Passwords unavailable for LDAP."; + end + + function provider.set_password(username, password) + return nil, "Passwords unavailable for LDAP."; + end + + function provider.create_user(username, password) + return nil, "Account creation/modification not available with LDAP."; + end + + function provider.get_sasl_handler() + local testpass_authentication_profile = { + plain_test = function(sasl, username, password, realm) + return provider.test_password(username, password), true; + end, + order = { "plain_test" }, + }; + return new_sasl(module.host, testpass_authentication_profile); + end + + function provider.is_admin(jid) + local admin_config = ldap.getparams().admin; + + if not admin_config then + return; + end + + local ld = ldap:getconnection(); + local username = jsplit(jid); + local filter = ldap.filter.combine_and(admin_config.filter, admin_config.namefield .. '=' .. username); + + return ldap.singlematch { + base = admin_config.basedn, + filter = filter, + }; + end + + return provider; +end + +module:add_item("auth-provider", new_default_provider(module.host)); diff --git a/data/templates/metronome/modules/mod_legacyauth.lua b/lib/metronome/modules/mod_legacyauth.lua similarity index 100% rename from data/templates/metronome/modules/mod_legacyauth.lua rename to lib/metronome/modules/mod_legacyauth.lua diff --git a/lib/metronome/modules/mod_storage_ldap.lua b/lib/metronome/modules/mod_storage_ldap.lua new file mode 100644 index 000000000..83fb4d003 --- /dev/null +++ b/lib/metronome/modules/mod_storage_ldap.lua @@ -0,0 +1,243 @@ +-- vim:sts=4 sw=4 + +-- Metronome IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- Copyright (C) 2012 Rob Hoelz +-- Copyright (C) 2015 YUNOHOST.ORG +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. + +---------------------------------------- +-- Constants and such -- +---------------------------------------- + +local setmetatable = setmetatable; + +local get_config = require "core.configmanager".get; +local ldap = module:require 'ldap'; +local vcardlib = module:require 'vcard'; +local st = require 'util.stanza'; +local gettime = require 'socket'.gettime; + +local log = module._log + +if not ldap then + return; +end + +local CACHE_EXPIRY = 300; + +---------------------------------------- +-- Utility Functions -- +---------------------------------------- + +local function ldap_record_to_vcard(record, format) + return vcardlib.create { + record = record, + format = format, + } +end + +local get_alias_for_user; + +do + local user_cache; + local last_fetch_time; + + local function populate_user_cache() + local user_c = get_config(module.host, 'ldap').user; + if not user_c then return; end + + local ld = ldap.getconnection(); + + local usernamefield = user_c.usernamefield; + local namefield = user_c.namefield; + + user_cache = {}; + + for _, attrs in ld:search { base = user_c.basedn, scope = 'onelevel', filter = user_c.filter } do + user_cache[attrs[usernamefield]] = attrs[namefield]; + end + last_fetch_time = gettime(); + end + + function get_alias_for_user(user) + if last_fetch_time and last_fetch_time + CACHE_EXPIRY < gettime() then + user_cache = nil; + end + if not user_cache then + populate_user_cache(); + end + return user_cache[user]; + end +end + +---------------------------------------- +-- Base LDAP store class -- +---------------------------------------- + +local function ldap_store(config) + local self = {}; + local config = config; + + function self:get(username) + return nil, "Data getting is not available for this storage backend"; + end + + function self:set(username, data) + return nil, "Data setting is not available for this storage backend"; + end + + return self; +end + +local adapters = {}; + +---------------------------------------- +-- Roster Storage Implementation -- +---------------------------------------- + +adapters.roster = function (config) + -- Validate configuration requirements + if not config.groups then return nil; end + + local self = ldap_store(config) + + function self:get(username) + local ld = ldap.getconnection(); + local contacts = {}; + + local memberfield = config.groups.memberfield; + local namefield = config.groups.namefield; + local filter = memberfield .. '=' .. tostring(username); + + local groups = {}; + for _, config in ipairs(config.groups) do + groups[ config[namefield] ] = config.name; + end + + log("debug", "Found %d group(s) for user %s", select('#', groups), username) + + -- XXX this kind of relies on the way we do groups at INOC + for _, attrs in ld:search { base = config.groups.basedn, scope = 'onelevel', filter = filter } do + if groups[ attrs[namefield] ] then + local members = attrs[memberfield]; + + for _, user in ipairs(members) do + if user ~= username then + local jid = user .. '@' .. module.host; + local record = contacts[jid]; + + if not record then + record = { + subscription = 'both', + groups = {}, + name = get_alias_for_user(user), + }; + contacts[jid] = record; + end + + record.groups[ groups[ attrs[namefield] ] ] = true; + end + end + end + end + + return contacts; + end + + function self:set(username, data) + log("warn", "Setting data in Roster LDAP storage is not supported yet") + return nil, "not supported"; + end + + return self; +end + +---------------------------------------- +-- vCard Storage Implementation -- +---------------------------------------- + +adapters.vcard = function (config) + -- Validate configuration requirements + if not config.vcard_format or not config.user then return nil; end + + local self = ldap_store(config) + + function self:get(username) + local ld = ldap.getconnection(); + local filter = config.user.usernamefield .. '=' .. tostring(username); + + log("debug", "Retrieving vCard for user '%s'", username); + + local match = ldap.singlematch { + base = config.user.basedn, + filter = filter, + }; + if match then + match.jid = username .. '@' .. module.host + return st.preserialize(ldap_record_to_vcard(match, config.vcard_format)); + else + return nil, "username not found"; + end + end + + function self:set(username, data) + log("warn", "Setting data in vCard LDAP storage is not supported yet") + return nil, "not supported"; + end + + return self; +end + +---------------------------------------- +-- Driver Definition -- +---------------------------------------- + +cache = {}; + +local driver = { name = "ldap" }; + +function driver:open(store) + log("debug", "Opening ldap storage backend for host '%s' and store '%s'", module.host, store); + + if not cache[module.host] then + log("debug", "Caching adapters for the host '%s'", module.host); + + local ad_config = get_config(module.host, "ldap"); + local ad_cache = {}; + for k, v in pairs(adapters) do + ad_cache[k] = v(ad_config); + end + + cache[module.host] = ad_cache; + end + + local adapter = cache[module.host][store]; + + if not adapter then + log("info", "Unavailable adapter for store '%s'", store); + return nil, "unsupported-store"; + end + return adapter; +end + +function driver:stores(username, type, pattern) + return nil, "not implemented"; +end + +function driver:store_exists(username, datastore, type) + return nil, "not implemented"; +end + +function driver:purge(username) + return nil, "not implemented"; +end + +function driver:users() + return nil, "not implemented"; +end + +module:add_item("data-driver", driver); diff --git a/data/templates/metronome/modules/vcard.lib.lua b/lib/metronome/modules/vcard.lib.lua similarity index 100% rename from data/templates/metronome/modules/vcard.lib.lua rename to lib/metronome/modules/vcard.lib.lua diff --git a/locales/de.json b/locales/de.json index 2e5c17edb..b3862b088 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,75 +1,75 @@ { - "yunohost_not_installed": "YunoHost ist nicht (oder nicht recht) installiert. Bitte 'yunohost tools postinstall' ablaufen.", - "upgrade_complete": "Upgrade erfolgreich beendet", - "installation_complete": "erfolgreich installiert", - "installation_failed": "Fehler beim Installation", - "app_unknown": "unbekannte App", - "app_no_upgrade": "Keine App zu updaten", - "app_not_installed": "{:s} ist nicht intalliert", - "custom_app_url_required": "Bitte eine URL geben, um deine nüzterspezifische App {:s} zu updaten", - "app_recent_version_required": "{:s} braucht eine jüngstere Fassung von \"moulinette\"", - "app_upgraded": "{:s} erfolgreich updaten", - "app_id_invalid": "Falsche App-ID", "app_already_installed": "{:s} ist schon installiert", - "app_removed": "{:s} erfolgreich gelöscht", + "app_extraction_failed": "Installationsdateien nicht extrahierbar", + "app_id_invalid": "Falsche App-ID", + "app_install_files_invalid": "Ungültige Installationsdateien", "app_location_already_used": "Eine App ist auf diesem Ort schon installiert", "app_location_install_failed": "Diese App ist auf diesem Ort nicht Installbar", - "app_extraction_failed": "Installationsdateien nicht extrahierbar", - "app_install_files_invalid": "Ungültige Installationsdateien", + "app_no_upgrade": "Keine App zu updaten", + "app_not_installed": "{:s} ist nicht intalliert", + "app_recent_version_required": "{:s} braucht eine jüngstere Fassung von \"moulinette\"", + "app_removed": "{:s} erfolgreich gelöscht", "app_sources_fetch_failed": "Quelledateien nicht abrufbar", - "ssowat_conf_updated": "SSOwat beständige Einstellung erfolgreich upgedatet", - "ssowat_conf_generated": "SSOwat-einstellung erfolgreich erzeugt", - "mysql_db_creation_failed": "Fehler beim MySQL-datenbankerzeugung", - "mysql_db_init_failed": "Fehler beim MySQL-datenbankinitialisierung", - "mysql_db_initialized": "MySQL-datenbank erfolgreich initialisiert", - "extracting": "Extrahierend...", - "downloading": "Herunterladend...", - "done": "Erledigt.", - "domain_unknown": "Unbekannte Domain", - "domain_dyndns_invalid": "Domain mit DynDNS nicht nützbar", - "domain_dyndns_already_subscribed": "Du hast dich schon für einen DynDNS-domain angemeldet", - "domain_dyndns_root_unknown": "Unbekannte DynDNS-hauptdomain", + "app_unknown": "unbekannte App", + "app_upgraded": "{:s} erfolgreich updaten", + "custom_app_url_required": "Bitte eine URL geben, um deine nüzterspezifische App {:s} zu updaten", "domain_cert_gen_failed": "Zertifizierung nicht erzeugbar", - "domain_exists": "diese Domain existiert schon", - "domain_creation_failed": "Domain nicht erzeugbar", "domain_created": "Domain erfolgreich erzeugt", - "domain_uninstall_app_first": "Mindestens eine App ist schon auf diese Domain installiert. Bitte erst die Apps deinstallieren, und nur dann die Domain löschen.", - "domain_deletion_failed": "Domain nicht löschbar", + "domain_creation_failed": "Domain nicht erzeugbar", "domain_deleted": "Domain erfolgreich gelöscht", - "no_internet_connection": "Server not connected to the Internet", - "dyndns_key_generating": "DNS key is being generated, it may take a while...", - "dyndns_unavailable": "DynDNS-subdomain nicht verfügbar", - "dyndns_registration_failed": "DynDNS-domain {:s} nicht registrierbar", - "dyndns_registered": "DynDNS-domain erfolgreich registriert", - "dyndns_ip_update_failed": "IP-adress auf DynDNS nicht updatbar", - "dyndns_ip_updated": "IP-adress auf DynDNS erfolgreich upgedatet", + "domain_deletion_failed": "Domain nicht löschbar", + "domain_dyndns_already_subscribed": "Du hast dich schon für einen DynDNS-domain angemeldet", + "domain_dyndns_invalid": "Domain mit DynDNS nicht nützbar", + "domain_dyndns_root_unknown": "Unbekannte DynDNS-hauptdomain", + "domain_exists": "diese Domain existiert schon", + "domain_uninstall_app_first": "Mindestens eine App ist schon auf diese Domain installiert. Bitte erst die Apps deinstallieren, und nur dann die Domain löschen.", + "domain_unknown": "Unbekannte Domain", + "done": "Erledigt.", + "downloading": "Herunterladend...", "dyndns_cron_installed": "DynDNS cron job erfolgreich installiert", "dyndns_cron_remove_failed": "DynDNS cron job nicht löschbar", "dyndns_cron_removed": "DynDNS cron job erfolgreich gelöscht", - "iptables_unavailable": "Du kannst nicht hier die IP-Tabelle bearbeiten. Entweder bist du in einen Container oder deinen Systemkern erhält es nicht.", + "dyndns_ip_update_failed": "IP-adress auf DynDNS nicht updatbar", + "dyndns_ip_updated": "IP-adress auf DynDNS erfolgreich upgedatet", + "dyndns_key_generating": "DNS key is being generated, it may take a while...", + "dyndns_registered": "DynDNS-domain erfolgreich registriert", + "dyndns_registration_failed": "DynDNS-domain {:s} nicht registrierbar", + "dyndns_unavailable": "DynDNS-subdomain nicht verfügbar", + "extracting": "Extrahierend...", "firewall_reloaded": "Firewall erfolgreich neu geladen", - "hook_choice_invalid": "ungültige Wahl '{:s}'", "hook_argument_missing": "Fehlend Argument '{:s}'", - "mountpoint_unknown": "unbekannten Einhängepunkt", - "unit_unknown": "unbekannte Einheit '{:s}'", - "monitor_period_invalid": "Falschen Zeitraum", - "monitor_stats_no_update": "Keine Monitoringstatistik zu updaten", - "monitor_stats_file_not_found": "Statistikdatei nicht gefunden", - "monitor_stats_period_unavailable": "Keine verfügbare Statistik für diese Zeitraum", - "monitor_enabled": "Servermonitoring erfolgreich aktiviert", + "hook_choice_invalid": "ungültige Wahl '{:s}'", + "installation_complete": "erfolgreich installiert", + "installation_failed": "Fehler beim Installation", + "iptables_unavailable": "Du kannst nicht hier die IP-Tabelle bearbeiten. Entweder bist du in einen Container oder deinen Systemkern erhält es nicht.", "monitor_disabled": "Servermonitoring erfolgreich deaktiviert", - "monitor_not_enabled": "Servermonitoring ist nicht aktiviert", + "monitor_enabled": "Servermonitoring erfolgreich aktiviert", "monitor_glances_con_failed": "Verbindung mit Glances-server nicht möglich", - "service_unknown": "Unbekannte Dienst '{:s}'", - "service_start_failed": "Kann nicht '{:s}' -dienst starten", + "monitor_not_enabled": "Servermonitoring ist nicht aktiviert", + "monitor_period_invalid": "Falschen Zeitraum", + "monitor_stats_file_not_found": "Statistikdatei nicht gefunden", + "monitor_stats_no_update": "Keine Monitoringstatistik zu updaten", + "monitor_stats_period_unavailable": "Keine verfügbare Statistik für diese Zeitraum", + "mountpoint_unknown": "unbekannten Einhängepunkt", + "mysql_db_creation_failed": "Fehler beim MySQL-datenbankerzeugung", + "mysql_db_init_failed": "Fehler beim MySQL-datenbankinitialisierung", + "mysql_db_initialized": "MySQL-datenbank erfolgreich initialisiert", + "no_internet_connection": "Server not connected to the Internet", "service_already_started": "'{:s}' -dienst ist schon im Betrieb", - "service_started": "'{:s}' -dienst erfolgreich gestartet", - "service_stop_failed": "Kann nicht '{:s}' -dienst stoppen", "service_already_stopped": "'{:s}' -dienst ist schon abgestoppt", - "service_stopped": "'{:s}' -dienst erfolgreich abgestoppt", - "service_enable_failed": "Kann nicht '{:s}' -dienst aktivieren", - "service_enabled": "'{:s}' -dienst erfolgreich aktiviert", "service_disable_failed": "Kann nicht '{:s}' -dienst deaktivieren", "service_disabled": "'{:s}' -dienst erfolgreich deaktiviert", - "service_status_failed": "Kann nicht '{:s}' -dienststatus feststellen" + "service_enable_failed": "Kann nicht '{:s}' -dienst aktivieren", + "service_enabled": "'{:s}' -dienst erfolgreich aktiviert", + "service_start_failed": "Kann nicht '{:s}' -dienst starten", + "service_started": "'{:s}' -dienst erfolgreich gestartet", + "service_status_failed": "Kann nicht '{:s}' -dienststatus feststellen", + "service_stop_failed": "Kann nicht '{:s}' -dienst stoppen", + "service_stopped": "'{:s}' -dienst erfolgreich abgestoppt", + "service_unknown": "Unbekannte Dienst '{:s}'", + "ssowat_conf_generated": "SSOwat-einstellung erfolgreich erzeugt", + "ssowat_conf_updated": "SSOwat beständige Einstellung erfolgreich upgedatet", + "unit_unknown": "unbekannte Einheit '{:s}'", + "upgrade_complete": "Upgrade erfolgreich beendet", + "yunohost_not_installed": "YunoHost ist nicht (oder nicht recht) installiert. Bitte 'yunohost tools postinstall' ablaufen." } diff --git a/locales/en.json b/locales/en.json index ba4e6b072..46d2cf118 100644 --- a/locales/en.json +++ b/locales/en.json @@ -16,19 +16,23 @@ "appslist_removed" : "Apps list successfully removed", "app_unknown" : "Unknown app", "app_no_upgrade" : "No app to upgrade", - "app_not_installed" : "{:s} is not installed", + "app_not_installed" : "{app:s} is not installed", + "app_not_correctly_installed" : "{app:s} seems to be not correctly installed", "custom_app_url_required" : "You must provide an URL to upgrade your custom app {:s}", - "app_recent_version_required" : "{:s} requires a more recent version of the moulinette", - "app_upgraded" : "{:s} successfully upgraded", + "app_recent_version_required" : "{app:s} requires a more recent version of YunoHost", + "app_upgraded" : "{app:s} successfully upgraded", "app_upgrade_failed" : "Unable to upgrade all apps", "app_id_invalid" : "Invalid app id", "app_already_installed" : "{:s} is already installed", - "app_removed" : "{:s} successfully removed", + "app_removed" : "{app:s} successfully removed", "app_location_already_used" : "An app is already installed on this location", "app_location_install_failed" : "Unable to install the app on this location", "app_extraction_failed" : "Unable to extract installation files", "app_install_files_invalid" : "Invalid installation files", "app_manifest_invalid" : "Invalid app manifest", + "app_argument_choice_invalid" : "Invalid choice for argument '{name:s}', it must be one of {choices:s}", + "app_argument_invalid" : "Invalid value for argument '{name:s}': {error:s}", + "app_argument_required" : "Argument '{name:s}' is required", "app_sources_fetch_failed" : "Unable to fetch sources files", "ssowat_conf_updated" : "SSOwat persistent configuration successfully updated", "ssowat_conf_generated" : "SSOwat configuration successfully generated", @@ -37,7 +41,8 @@ "mysql_db_initialized" : "MySQL database successfully initialized", "extracting" : "Extracting...", "downloading" : "Downloading...", - "executing_script": "Executing script...", + "executing_script": "Executing script '{script:s}'...", + "executing_command": "Executing command '{command:s}'...", "done" : "Done.", "path_removal_failed" : "Unable to remove path {:s}", @@ -84,8 +89,8 @@ "hook_list_by_invalid" : "Invalid property to list hook by", "hook_name_unknown" : "Unknown hook name '{:s}'", - "hook_choice_invalid" : "Invalid choice '{:s}'", - "hook_argument_missing" : "Missing argument '{:s}'", + "hook_exec_failed" : "Script execution failed", + "hook_exec_not_terminated" : "Script execution hasn’t terminated", "mountpoint_unknown" : "Unknown mountpoint", "unit_unknown" : "Unknown unit '{:s}'", @@ -116,6 +121,11 @@ "service_status_failed" : "Unable to determine status of service '{:s}'", "service_no_log" : "No log to display for service '{:s}'", "service_cmd_exec_failed" : "Unable to execute command '{:s}'", + "services_configured": "Configuration successfully generated", + "service_configuration_conflict": "The file {file:s} has been changed since its last generation. Please apply the modifications manually or use the option --force (it will erase all the modifications previously done to the file).", + "no_such_conf_file": "Unable to copy the file {file:s}: the file does not exist", + "service_add_configuration": "Adding the configuration file {file:s}", + "show_diff": "Here are the differences:\n{diff:s}", "network_check_smtp_ok" : "Outbound mail (SMTP port 25) is not blocked", "network_check_smtp_ko" : "Outbound mail (SMTP port 25) seems to be blocked by your network", @@ -143,31 +153,36 @@ "backup_output_directory_required" : "You must provide an output directory for the backup", "backup_output_directory_forbidden" : "Forbidden output directory", "backup_output_directory_not_empty" : "Output directory is not empty", + "backup_hook_unknown" : "Backup hook '{hook:s}' unknown", "backup_running_hooks" : "Running backup hooks...", - "backup_running_app_script" : "Running backup script of app '{:s}'...", + "backup_running_app_script" : "Running backup script of app '{app:s}'...", "backup_creating_archive" : "Creating the backup archive...", "backup_extracting_archive" : "Extracting the backup archive...", "backup_archive_open_failed" : "Unable to open the backup archive", - "backup_archive_name_unknown" : "Unknown local backup archive named '{:s}'", + "backup_archive_name_unknown" : "Unknown local backup archive named '{name:s}'", "backup_archive_name_exists" : "Backup archive name already exists", + "backup_archive_hook_not_exec" : "Hook '{hook:s}' not executed in this backup", + "backup_archive_app_not_found" : "App '{app:s}' not found in the backup archive", "backup_app_failed" : "Unable to back up the app '{app:s}'", "backup_nothings_done" : "There is nothing to save", - "backup_cleaning_failed" : "Unable to clean backup directory", + "backup_cleaning_failed" : "Unable to clean backup temporary directory", "backup_complete" : "Backup complete", "backup_invalid_archive" : "Invalid backup archive", - "backup_hook_unavailable" : "The hook '{:s}' is not in this backup", + "restore_action_required" : "You must specify something to restore", "restore_confirm_yunohost_installed" : "Do you really want to restore an already installed system? [{answers:s}]", + "restore_hook_unavailable" : "Restauration hook '{hook:s}' not available on your system", "restore_app_failed" : "Unable to restore the app '{app:s}'", "restore_running_hooks" : "Running restoration hooks...", + "restore_running_app_script" : "Running restore script of app '{app:s}'...", "restore_failed" : "Unable to restore the system", + "restore_nothings_done" : "Nothing has been restored", + "restore_cleaning_failed" : "Unable to clean restoration temporary directory", "restore_complete" : "Restore complete", "restore_already_installed_app": "An app is already installed with the id '{app:s}'", - "unbackup_app" : "App '{:s}' will not be saved", - "unrestorable_app" : "App '{:s}' will not be restored", - "restore_app_copy_failed" : "Unable to copy the restore script of app '{app:s}'", + "unbackup_app" : "App '{app:s}' will not be saved", "no_restore_script": "No restore script found for the app '{app:s}'", - "unrestore_app" : "App '{:s}' will not be restored", - "backup_delete_error" : "Unable to delete '{:s}'", + "unrestore_app" : "App '{app:s}' will not be restored", + "backup_delete_error" : "Unable to delete '{path:s}'", "backup_deleted" : "Backup successfully deleted", "field_invalid" : "Invalid field '{:s}'", diff --git a/locales/es.json b/locales/es.json index 096447989..7eaf09dd9 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,176 +1,176 @@ { - "yunohost_not_installed": "YunoHost no está instalado o la instilación ha cumplido con errores. Por favor, ejecute 'yunohost tools postinstall'.", - "upgrade_complete": "La actualización se ha completado", - "installation_complete": "La instalación se ha completado", - "installation_failed": "La Instalación se ha fracasado", - "unexpected_error": "Un error ha ocurrido", "action_invalid": "Acción inválida '{:s}'", - "license_undefined": "indefinido", - "no_appslist_found": "No se encontró ninguna lista de Apps", - "custom_appslist_name_required": "Debe proporcionar un nombre para la lista de aplicaciones personalizadas ", - "appslist_retrieve_error": "No se pudo recuperar la lista de aplicaciones a distancia ", - "appslist_fetched": "Lista de aplicaciones se trajo con éxito", - "appslist_unknown": "Lista de aplicaciones desconocidas", - "appslist_removed": "Lista de aplicaciones se eliminó con éxito", - "app_unknown": "App desconocida", - "app_no_upgrade": "Ninguna app a actualizar", - "app_not_installed": "{:s} no está instalado.", - "custom_app_url_required": " Debe proporcionar una URL para actualizar su aplicación personalizada {:s} ", - "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", - "app_upgraded": "{:s} actualizado con éxito", - "app_upgrade_failed": "No se pudo actualizar todas las aplicaciones ", - "app_id_invalid": "id de la aplicación inválida ", + "admin_password": "Contraseña administrativa", + "admin_password_change_failed": "No se pudo cambiar la contraseña", + "admin_password_changed": "Contraseña administrativa se cambió con éxito", "app_already_installed": "{:s} ya está instalado ", - "app_removed": "{:s} era eliminado con éxito ", + "app_extraction_failed": "No se pudo extraer los archivos de instalación ", + "app_id_invalid": "id de la aplicación inválida ", + "app_install_files_invalid": "Archivos de instalación inválidos ", "app_location_already_used": "Una aplicación ya está instalado en este lugar", "app_location_install_failed": "No se pudo instalar la aplicación en esta lugar", - "app_extraction_failed": "No se pudo extraer los archivos de instalación ", - "app_install_files_invalid": "Archivos de instalación inválidos ", "app_manifest_invalid": "Manifesto de la aplicación es inválido", + "app_no_upgrade": "Ninguna app a actualizar", + "app_not_installed": "{:s} no está instalado.", + "app_recent_version_required": "{:s} requiere una versión más reciente de moulinette ", + "app_removed": "{:s} era eliminado con éxito ", "app_sources_fetch_failed": "No se pudo descargar los archivos de códigos fuentes", - "ssowat_conf_updated": "Configuración persistente SSOwat actualizada con éxito", - "ssowat_conf_generated": "Configuración SSOwat generado con éxito ", - "mysql_db_creation_failed": "No se pudo crear el base de datos MySQL", - "mysql_db_init_failed": "No se pudo inicializar el base de datos MySQL.", - "mysql_db_initialized": "Base de datos MySQL inicializado con éxito", - "extracting": "Extrayendo...", - "downloading": "Descargando...", - "executing_script": "Ejecutando script...", - "done": "Completo.", - "path_removal_failed": "No se pudo quitar la ruta {:s}", - "domain_unknown": "Dominio desconocido", - "domain_dyndns_invalid": "El dominio no es valido para usar con DynDNS", - "domain_dyndns_already_subscribed": "Ya te has suscrito a un dominio DynDNS.", - "domain_dyndns_root_unknown": "Dominio raíz DynDNS desconocido ", - "domain_cert_gen_failed": "No se pudo crear certificado", - "domain_exists": "El dominio ya existe", + "app_unknown": "App desconocida", + "app_upgrade_failed": "No se pudo actualizar todas las aplicaciones ", + "app_upgraded": "{:s} actualizado con éxito", + "appslist_fetched": "Lista de aplicaciones se trajo con éxito", + "appslist_removed": "Lista de aplicaciones se eliminó con éxito", + "appslist_retrieve_error": "No se pudo recuperar la lista de aplicaciones a distancia ", + "appslist_unknown": "Lista de aplicaciones desconocidas", + "ask_current_admin_password": "Contraseña administrativa presente", + "ask_email": "Correo electrónico", + "ask_firstname": "Nombre", + "ask_lastname": "Apellido", + "ask_list_to_remove": "Lista a quitar", + "ask_main_domain": "Dominio principal", + "ask_new_admin_password": "Contraseña administrativa nueva", + "ask_password": "Contraseña", + "backup_archive_name_exists": "Un archivo ya existe con el nombre del archivo de backup", + "backup_archive_name_unknown": "El nombre archivo local de backup está desconocido", + "backup_archive_open_failed": "No se pudo abrir el archivo backup", + "backup_complete": "El backup se ha completado", + "backup_creating_archive": "Creando el archivo backup...", + "backup_extracting_archive": "Extrayendo el archivo backup...", + "backup_invalid_archive": "Archivo de backup es inválido", + "backup_output_directory_forbidden": "Carpeta de salida prohibida", + "backup_output_directory_not_empty": "La carpeta de salida no está vacía", + "backup_output_directory_required": "Debe proporcionar un directorio de salida para el backup", + "backup_running_hooks": "Ejecutando los hooks de backup...", + "custom_app_url_required": " Debe proporcionar una URL para actualizar su aplicación personalizada {:s} ", + "custom_appslist_name_required": "Debe proporcionar un nombre para la lista de aplicaciones personalizadas ", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, por favor, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cert_gen_failed": "No se pudo crear certificado", + "domain_created": "Dominio creado con éxito.", + "domain_creation_failed": "No se pudo crear el dominio", + "domain_deleted": "Dominio borrado con éxito.", + "domain_deletion_failed": "No se pudo borrar el dominio.", + "domain_dyndns_already_subscribed": "Ya te has suscrito a un dominio DynDNS.", + "domain_dyndns_invalid": "El dominio no es valido para usar con DynDNS", + "domain_dyndns_root_unknown": "Dominio raíz DynDNS desconocido ", + "domain_exists": "El dominio ya existe", + "domain_uninstall_app_first": "Uno o más apps están instalados en este dominio. Por favor, desinstalarlos antes de quitar este dominio.", + "domain_unknown": "Dominio desconocido", "domain_zone_exists": "El archivo de zonas DNS ya existe.", "domain_zone_not_found": "Archivo de zonas DNS por el dominio [:s] no estaba encontrado", - "domain_creation_failed": "No se pudo crear el dominio", - "domain_created": "Dominio creado con éxito.", - "domain_uninstall_app_first": "Uno o más apps están instalados en este dominio. Por favor, desinstalarlos antes de quitar este dominio.", - "domain_deletion_failed": "No se pudo borrar el dominio.", - "domain_deleted": "Dominio borrado con éxito.", - "no_internet_connection": "El servidor no está conectado al Internet.", - "dyndns_key_generating": "Generación del llave de DNS está en curso. Este podría durar unos momentos...", - "dyndns_unavailable": "Subdominio DynDNS no disponible", - "dyndns_registration_failed": "No se pudo registrar el dominio DynDNS: {:s}", - "dyndns_registered": "El dominio DynDNS era registrado con éxito.", - "dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en DynDNS", - "dyndns_ip_updated": "La dirección IP era actualizado en DynDNS con éxito", + "done": "Completo.", + "downloading": "Descargando...", "dyndns_cron_installed": "El trabajo cron de DynDNS se ha instalado con éxito", "dyndns_cron_remove_failed": "No se pudo quitar el trabajo cron DynDNS", "dyndns_cron_removed": "Trabajo cron DynDNS se quitó con éxito", - "port_available": "El puerto {} está disponible", - "port_unavailable": "El puerto {} no está disponible", - "port_already_opened": "El puerto {} ya está abierto por {:s} connecciones", - "port_already_closed": "El puerto {} ya está cerrado por {:s} connecciones.", - "iptables_unavailable": "No puedes modificar los iptables aquí. Eres en un contenedor o su kernel no soporte este opción.", - "ip6tables_unavailable": "No puedes modificar los ip6tables aquí. Eres en un contenedor o su kernel no soporte este opción.", - "upnp_dev_not_found": "No se encontró ninguno dispositivo UPnP ", - "upnp_port_open_failed": "No se pudo abrir puertos por UPnP", - "upnp_enabled": "UPnP activado con éxito", - "upnp_disabled": "UPnP deshabilitado con éxito", - "firewall_rules_cmd_failed": "Algunos reglas del cortafuegos han fracasado. Para más información, vea al archivo historial.", + "dyndns_ip_update_failed": "No se pudo actualizar la dirección IP en DynDNS", + "dyndns_ip_updated": "La dirección IP era actualizado en DynDNS con éxito", + "dyndns_key_generating": "Generación del llave de DNS está en curso. Este podría durar unos momentos...", + "dyndns_registered": "El dominio DynDNS era registrado con éxito.", + "dyndns_registration_failed": "No se pudo registrar el dominio DynDNS: {:s}", + "dyndns_unavailable": "Subdominio DynDNS no disponible", + "executing_script": "Ejecutando script...", + "extracting": "Extrayendo...", + "field_invalid": "Campo inválido '{:s}'", "firewall_reload_failed": "No se pudo recargar el cortafuegos", "firewall_reloaded": "Cortafuegos recargado con éxito", + "firewall_rules_cmd_failed": "Algunos reglas del cortafuegos han fracasado. Para más información, vea al archivo historial.", + "hook_argument_missing": "Falta un parámetro '{:s}'", + "hook_choice_invalid": "Selección inválida '{:s}'", "hook_list_by_invalid": "La propiedad de este hook es inválida", "hook_name_unknown": "Hook desconocido '{:s}'", - "hook_choice_invalid": "Selección inválida '{:s}'", - "hook_argument_missing": "Falta un parámetro '{:s}'", - "mountpoint_unknown": "Punto de montaje desconocido", - "unit_unknown": "Unidad '{:s}' desconocido", - "monitor_period_invalid": "Período de tiempo inválido", - "monitor_stats_no_update": "No hay ninguna estadísticos de la supervisión del sistema a realizar", - "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticos", - "monitor_stats_period_unavailable": "No hay estadísticos del período del tiempo", - "monitor_enabled": "Supervisión del sistema activado con éxito", + "installation_complete": "La instalación se ha completado", + "installation_failed": "La Instalación se ha fracasado", + "ip6tables_unavailable": "No puedes modificar los ip6tables aquí. Eres en un contenedor o su kernel no soporte este opción.", + "iptables_unavailable": "No puedes modificar los iptables aquí. Eres en un contenedor o su kernel no soporte este opción.", + "ldap_initialized": "LDAP se inició con éxito", + "license_undefined": "indefinido", + "mail_alias_remove_failed": "No se pudo quitar el alias de correos '{:s}'", + "mail_domain_unknown": "El dominio de correos '{:s}' es desconocido", + "mail_forward_remove_failed": "No se pudo quitar la reenvía de correos '{:s}'", + "maindomain_change_failed": "No se pudo cambiar el dominio principal", + "maindomain_changed": "Dominio principal se cambió con éxito", "monitor_disabled": "Supervisión del sistema era desactivado con éxito", - "monitor_not_enabled": "Supervisión del sistema no está activado", + "monitor_enabled": "Supervisión del sistema activado con éxito", "monitor_glances_con_failed": "No se pudo conectar al servidor de Glances", - "service_unknown": "Servicio desconocido '{:s}'", + "monitor_not_enabled": "Supervisión del sistema no está activado", + "monitor_period_invalid": "Período de tiempo inválido", + "monitor_stats_file_not_found": "No se pudo encontrar el archivo de estadísticos", + "monitor_stats_no_update": "No hay ninguna estadísticos de la supervisión del sistema a realizar", + "monitor_stats_period_unavailable": "No hay estadísticos del período del tiempo", + "mountpoint_unknown": "Punto de montaje desconocido", + "mysql_db_creation_failed": "No se pudo crear el base de datos MySQL", + "mysql_db_init_failed": "No se pudo inicializar el base de datos MySQL.", + "mysql_db_initialized": "Base de datos MySQL inicializado con éxito", + "new_domain_required": "Debe proporcionar el dominio principal nuevo", + "no_appslist_found": "No se encontró ninguna lista de Apps", + "no_internet_connection": "El servidor no está conectado al Internet.", + "packages_no_upgrade": "No hay actualización por ningun paquete", + "packages_upgrade_critical_later": "Los paquetes críticos ({:s}) se actualizarán más tarde", + "packages_upgrade_failed": "No se pudo actualizar todo de los paquetes", + "path_removal_failed": "No se pudo quitar la ruta {:s}", + "pattern_backup_archive_name": "Debe que ser un nombre de archivo válido con los caracteres alfanumericos, o los -_.", + "pattern_domain": "El nombre de dominio debe ser válido (e.g. mi-dominio.org)", + "pattern_email": "Debe ser una direccion de email válido (e.g. alguien@dominio.org)", + "pattern_firstname": "Debe ser un nombre válido", + "pattern_lastname": "Debe ser un apellido válido", + "pattern_listname": "Los caracteres deben ser alfanuméricos o el guion bajo.", + "pattern_password": "Debe ser a menos de 3 caracteres", + "pattern_port": "El numéro del puerto debe ser válido (i.e. 0-65535)", + "pattern_port_or_range": "El numéro del puerto debe ser válido (i.e. 0-65535) o un intervalo de puertos (e.g. 100:200)", + "pattern_username": "Debe contener solamente caracteres alfanuméricos o la guion bajo", + "port_already_closed": "El puerto {} ya está cerrado por {:s} connecciones.", + "port_already_opened": "El puerto {} ya está abierto por {:s} connecciones", + "port_available": "El puerto {} está disponible", + "port_unavailable": "El puerto {} no está disponible", + "restore_complete": "Restauración se ha completado", + "restore_confirm_yunohost_installed": "Estás seguro que quieres restaurar a un sistema que ya está instalado? [{answers:s}]", + "restore_failed": "No se pudo restaurar el sistema", + "restore_running_hooks": "Ejecutando hooks de restauración...", "service_add_failed": "No se pudo añadir el servicio '{:s}'", "service_added": "Servicio añadido con éxito", + "service_already_started": "El servicio '{:s}' ya se ha empezado", + "service_already_stopped": "El servicio '{:s}' ya está parado ", + "service_cmd_exec_failed": "No se pudo ejecutar comando '{:s}'", + "service_disable_failed": "No se pudo desactivar el servicio '{:s}'", + "service_disabled": "Servicio '{:s}' desactivado con éxito", + "service_enable_failed": "No se pudo activar el servicio '{:s}'", + "service_enabled": "Servicio '{:s}' activado con éxito", + "service_no_log": "No hay archivo historial del servicio '{:s}' a exhibir", "service_remove_failed": "No se pudo quitar el servicio '{:s}'", "service_removed": "Servicio quitado con éxito", "service_start_failed": "No se pudo empezar el servicio '{:s}'", - "service_already_started": "El servicio '{:s}' ya se ha empezado", "service_started": "El servicio '{:s}' se empezó con éxito", - "service_stop_failed": "No se pudo parar el servicio '{:s}'", - "service_already_stopped": "El servicio '{:s}' ya está parado ", - "service_stopped": "Servicio '{:s}' parado con éxito", - "service_enable_failed": "No se pudo activar el servicio '{:s}'", - "service_enabled": "Servicio '{:s}' activado con éxito", - "service_disable_failed": "No se pudo desactivar el servicio '{:s}'", - "service_disabled": "Servicio '{:s}' desactivado con éxito", "service_status_failed": "No se pudo discernir el estado del servicio '{:s}'", - "service_no_log": "No hay archivo historial del servicio '{:s}' a exhibir", - "service_cmd_exec_failed": "No se pudo ejecutar comando '{:s}'", - "ldap_initialized": "LDAP se inició con éxito", - "admin_password_change_failed": "No se pudo cambiar la contraseña", - "admin_password_changed": "Contraseña administrativa se cambió con éxito", - "new_domain_required": "Debe proporcionar el dominio principal nuevo", - "maindomain_change_failed": "No se pudo cambiar el dominio principal", - "maindomain_changed": "Dominio principal se cambió con éxito", - "yunohost_installing": "Instalando YunoHost...", + "service_stop_failed": "No se pudo parar el servicio '{:s}'", + "service_stopped": "Servicio '{:s}' parado con éxito", + "service_unknown": "Servicio desconocido '{:s}'", + "ssowat_conf_generated": "Configuración SSOwat generado con éxito ", + "ssowat_conf_updated": "Configuración persistente SSOwat actualizada con éxito", + "system_upgraded": "Actualización del sistema se ha completado con éxito.", + "system_username_exists": "Nombre de usuario ya existe en los usuarios del sistema", + "unbackup_app": "La App '{:s}' no será guardada", + "unexpected_error": "Un error ha ocurrido", + "unit_unknown": "Unidad '{:s}' desconocido", + "unrestore_app": "La App '{:s}' no será restaurada", + "update_cache_failed": "No se pudo actualizar el cache APT", + "updating_apt_cache": "Actualizando la lista de paquetes disponibles...", + "upgrade_complete": "La actualización se ha completado", + "upgrading_packages": "Actualizando paquetes...", + "upnp_dev_not_found": "No se encontró ninguno dispositivo UPnP ", + "upnp_disabled": "UPnP deshabilitado con éxito", + "upnp_enabled": "UPnP activado con éxito", + "upnp_port_open_failed": "No se pudo abrir puertos por UPnP", + "user_created": "Usuario creado con éxito", + "user_creation_failed": "No se pudo crear un usuario nuevo", + "user_deleted": "Usuario creado con éxito", + "user_deletion_failed": "No se pudo quitar el usuario", + "user_info_failed": "No se pudo traer la información del usuario. ", + "user_unknown": "usuario desconocido", + "user_update_failed": "No se pudo actualizar el usuario", + "user_updated": "Usuario actualizado con éxito", "yunohost_already_installed": "YunoHost ya está instalado", "yunohost_ca_creation_failed": "No se pudo crear un autoridad de certificación nuevo", "yunohost_configured": "YunoHost se configuró con éxito.", - "updating_apt_cache": "Actualizando la lista de paquetes disponibles...", - "update_cache_failed": "No se pudo actualizar el cache APT", - "packages_no_upgrade": "No hay actualización por ningun paquete", - "packages_upgrade_critical_later": "Los paquetes críticos ({:s}) se actualizarán más tarde", - "upgrading_packages": "Actualizando paquetes...", - "packages_upgrade_failed": "No se pudo actualizar todo de los paquetes", - "system_upgraded": "Actualización del sistema se ha completado con éxito.", - "backup_output_directory_required": "Debe proporcionar un directorio de salida para el backup", - "backup_output_directory_forbidden": "Carpeta de salida prohibida", - "backup_output_directory_not_empty": "La carpeta de salida no está vacía", - "backup_running_hooks": "Ejecutando los hooks de backup...", - "backup_creating_archive": "Creando el archivo backup...", - "backup_extracting_archive": "Extrayendo el archivo backup...", - "backup_archive_open_failed": "No se pudo abrir el archivo backup", - "backup_archive_name_unknown": "El nombre archivo local de backup está desconocido", - "backup_archive_name_exists": "Un archivo ya existe con el nombre del archivo de backup", - "backup_complete": "El backup se ha completado", - "backup_invalid_archive": "Archivo de backup es inválido", - "restore_confirm_yunohost_installed": "Estás seguro que quieres restaurar a un sistema que ya está instalado? [{answers:s}]", - "restore_running_hooks": "Ejecutando hooks de restauración...", - "restore_failed": "No se pudo restaurar el sistema", - "restore_complete": "Restauración se ha completado", - "unbackup_app": "La App '{:s}' no será guardada", - "unrestore_app": "La App '{:s}' no será restaurada", - "field_invalid": "Campo inválido '{:s}'", - "mail_domain_unknown": "El dominio de correos '{:s}' es desconocido", - "mail_alias_remove_failed": "No se pudo quitar el alias de correos '{:s}'", - "mail_forward_remove_failed": "No se pudo quitar la reenvía de correos '{:s}'", - "user_unknown": "usuario desconocido", - "system_username_exists": "Nombre de usuario ya existe en los usuarios del sistema", - "user_creation_failed": "No se pudo crear un usuario nuevo", - "user_created": "Usuario creado con éxito", - "user_deletion_failed": "No se pudo quitar el usuario", - "user_deleted": "Usuario creado con éxito", - "user_update_failed": "No se pudo actualizar el usuario", - "user_updated": "Usuario actualizado con éxito", - "user_info_failed": "No se pudo traer la información del usuario. ", - "admin_password": "Contraseña administrativa", - "ask_firstname": "Nombre", - "ask_lastname": "Apellido", - "ask_email": "Correo electrónico", - "ask_password": "Contraseña", - "ask_current_admin_password": "Contraseña administrativa presente", - "ask_new_admin_password": "Contraseña administrativa nueva", - "ask_main_domain": "Dominio principal", - "ask_list_to_remove": "Lista a quitar", - "pattern_username": "Debe contener solamente caracteres alfanuméricos o la guion bajo", - "pattern_firstname": "Debe ser un nombre válido", - "pattern_lastname": "Debe ser un apellido válido", - "pattern_email": "Debe ser una direccion de email válido (e.g. alguien@dominio.org)", - "pattern_password": "Debe ser a menos de 3 caracteres", - "pattern_domain": "El nombre de dominio debe ser válido (e.g. mi-dominio.org)", - "pattern_listname": "Los caracteres deben ser alfanuméricos o el guion bajo.", - "pattern_port": "El numéro del puerto debe ser válido (i.e. 0-65535)", - "pattern_port_or_range": "El numéro del puerto debe ser válido (i.e. 0-65535) o un intervalo de puertos (e.g. 100:200)", - "pattern_backup_archive_name": "Debe que ser un nombre de archivo válido con los caracteres alfanumericos, o los -_." + "yunohost_installing": "Instalando YunoHost...", + "yunohost_not_installed": "YunoHost no está instalado o la instilación ha cumplido con errores. Por favor, ejecute 'yunohost tools postinstall'." } diff --git a/locales/fr.json b/locales/fr.json index d46c4c3e4..562964456 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,178 +1,185 @@ { - "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'.", - "upgrade_complete": "Mise à jour terminée", - "installation_complete": "Installation terminée", - "installation_failed": "Échec de l'installation", - "unexpected_error": "Une erreur inattendue est survenue", "action_invalid": "Action '{:s}' incorrecte", - "license_undefined": "indéfinie", - "no_appslist_found": "Aucune liste d'applications trouvée", - "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée", - "appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante", - "appslist_fetched": "Liste d'applications récupérée avec succès", - "appslist_unknown": "Liste d'applications inconnue", - "appslist_removed": "Liste d'applications supprimée avec succès", - "app_unknown": "Application inconnue", - "app_no_upgrade": "Aucune application à mettre à jour", - "app_not_installed": "{:s} n'est pas installé", - "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {:s}", - "app_recent_version_required": "{:s} nécessite une version plus récente de la moulinette", - "app_upgraded": "{:s} mis à jour avec succès", - "app_upgrade_failed": "Impossible de mettre à jour toutes les applications", - "app_id_invalid": "Id d'application incorrect", + "admin_password": "Mot de passe d'administration", + "admin_password_change_failed": "Impossible de modifier le mot de passe d'administration", + "admin_password_changed": "Mot de passe d'administration modifié avec succès", "app_already_installed": "{:s} est déjà installé", - "app_removed": "{:s} supprimé avec succès", + "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", + "app_id_invalid": "Id d'application incorrect", + "app_install_files_invalid": "Fichiers d'installation incorrects", "app_location_already_used": "Une application est déjà installée à cet emplacement", "app_location_install_failed": "Impossible d'installer l'application à cet emplacement", - "app_extraction_failed": "Impossible d'extraire les fichiers d'installation", - "app_install_files_invalid": "Fichiers d'installation incorrects", "app_manifest_invalid": "Manifeste d'application incorrect", + "app_no_upgrade": "Aucune application à mettre à jour", + "app_not_installed": "{:s} n'est pas installé", + "app_recent_version_required": "{:s} nécessite une version plus récente de la moulinette", + "app_removed": "{:s} supprimé avec succès", "app_sources_fetch_failed": "Impossible de récupérer les fichiers sources", - "ssowat_conf_updated": "Configuration persistante de SSOwat mise à jour avec succès", - "ssowat_conf_generated": "Configuration de SSOwat générée avec succès", - "mysql_db_creation_failed": "Impossible de créer la base de donnée MySQL", - "mysql_db_init_failed": "Impossible d'initialiser la base de donnée MySQL", - "mysql_db_initialized": "Base de donnée MySQL initialisée avec succès", - "extracting": "Extraction...", - "downloading": "Téléchargement...", - "executing_script": "Exécution du script...", - "done": "Terminé.", - "path_removal_failed": "Impossible de supprimer le chemin {:s}", - "domain_unknown": "Domaine inconnu", - "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", - "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", - "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", - "domain_cert_gen_failed": "Impossible de générer le certificat", - "domain_exists": "Le domaine existe déjà", + "app_unknown": "Application inconnue", + "app_upgrade_failed": "Impossible de mettre à jour toutes les applications", + "app_upgraded": "{:s} mis à jour avec succès", + "appslist_fetched": "Liste d'applications récupérée avec succès", + "appslist_removed": "Liste d'applications supprimée avec succès", + "appslist_retrieve_error": "Impossible de récupérer la liste d'applications distante", + "appslist_unknown": "Liste d'applications inconnue", + "ask_current_admin_password": "Mot de passe d'administration actuel", + "ask_email": "Adresse mail", + "ask_firstname": "Prénom", + "ask_lastname": "Nom", + "ask_list_to_remove": "Liste à supprimer", + "ask_main_domain": "Domaine principal", + "ask_new_admin_password": "Nouveau mot de passe d'administration", + "ask_password": "Mot de passe", + "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà", + "backup_archive_name_unknown": "Nom d'archive de sauvegarde locale inconnu", + "backup_archive_open_failed": "Impossible d'ouvrir l'archive de sauvegarde", + "backup_complete": "Sauvegarde terminée", + "backup_creating_archive": "Création de l'archive de sauvegarde...", + "backup_extracting_archive": "Extraction de l'archive de sauvegarde...", + "backup_hook_unknown": "Script de sauvegarde '{hook:s}' inconnu", + "backup_invalid_archive": "Archive de sauvegarde incorrecte", + "backup_output_directory_forbidden": "Dossier de sortie interdit", + "backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide", + "backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde", + "backup_running_app_script": "Lancement du script de sauvegarde de l'application '{:s}'...", + "backup_running_hooks": "Exécution des scripts de sauvegarde...", + "custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {:s}", + "custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cert_gen_failed": "Impossible de générer le certificat", + "domain_created": "Domaine créé avec succès", + "domain_creation_failed": "Impossible de créer le domaine", + "domain_deleted": "Domaine supprimé avec succès", + "domain_deletion_failed": "Impossible de supprimer le domaine", + "domain_dyndns_already_subscribed": "Vous avez déjà souscris à un domaine DynDNS", + "domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS", + "domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu", + "domain_exists": "Le domaine existe déjà", + "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine.", + "domain_unknown": "Domaine inconnu", "domain_zone_exists": "Le fichier de zone DNS existe déjà", "domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}", - "domain_creation_failed": "Impossible de créer le domaine", - "domain_created": "Domaine créé avec succès", - "domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine.", - "domain_deletion_failed": "Impossible de supprimer le domaine", - "domain_deleted": "Domaine supprimé avec succès", - "no_internet_connection": "Le serveur n'est pas connecté à Internet", - "no_ipv6_connectivity": "IPv6 n'est pas disponible", - "dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre du temps...", - "dyndns_unavailable": "Sous-domaine DynDNS indisponible", - "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {:s}", - "dyndns_registered": "Domaine DynDNS enregistré avec succès", - "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS", - "dyndns_ip_updated": "Adresse IP mise à jour avec succès sur le domaine DynDNS", + "done": "Terminé.", + "downloading": "Téléchargement...", "dyndns_cron_installed": "Tâche cron pour DynDNS installée avec succès", "dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour DynDNS", "dyndns_cron_removed": "Tâche cron pour DynDNS enlevée avec succès", - "port_available": "Le port {} est disponible", - "port_unavailable": "Le port {} n'est pas disponible", - "port_already_opened": "Le port {} est déjà ouvert pour les connexions {:s}", - "port_already_closed": "Le port {} est déjà fermé pour les connexions {:s}", - "iptables_unavailable": "Vous ne pouvez pas faire joujou avec iptables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", - "ip6tables_unavailable": "Vous ne pouvez pas faire joujou avec ip6tables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", - "upnp_dev_not_found": "Aucun périphérique compatible UPnP trouvé", - "upnp_port_open_failed": "Impossible d'ouvrir les ports avec UPnP", - "upnp_enabled": "UPnP activé avec succès", - "upnp_disabled": "UPnP désactivé avec succès", - "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", + "dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS", + "dyndns_ip_updated": "Adresse IP mise à jour avec succès sur le domaine DynDNS", + "dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre du temps...", + "dyndns_registered": "Domaine DynDNS enregistré avec succès", + "dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {:s}", + "dyndns_unavailable": "Sous-domaine DynDNS indisponible", + "executing_script": "Exécution du script...", + "extracting": "Extraction...", + "field_invalid": "Champ incorrect : {:s}", "firewall_reload_failed": "Impossible de recharger le pare-feu", "firewall_reloaded": "Pare-feu rechargé avec succès", + "firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.", + "format_datetime_short": "%d/%m/%Y %H:%M", + "hook_argument_missing": "Argument manquant : '{:s}'", + "hook_choice_invalid": "Choix incorrect : '{:s}'", "hook_list_by_invalid": "Propriété pour lister les scripts incorrecte", "hook_name_unknown": "Nom de script '{:s}' inconnu", - "hook_choice_invalid": "Choix incorrect : '{:s}'", - "hook_argument_missing": "Argument manquant : '{:s}'", - "mountpoint_unknown": "Point de montage inconnu", - "unit_unknown": "Unité '{:s}' inconnue", - "monitor_period_invalid": "Période de temps incorrect", - "monitor_stats_no_update": "Aucune donnée de l'état du serveur à mettre à jour", - "monitor_stats_file_not_found": "Fichier de données de l'état du serveur introuvable", - "monitor_stats_period_unavailable": "Aucune donnée de l'état du serveur disponible pour la période", - "monitor_enabled": "Suivi de l'état du serveur activé avec succès", + "installation_complete": "Installation terminée", + "installation_failed": "Échec de l'installation", + "ip6tables_unavailable": "Vous ne pouvez pas faire joujou avec ip6tables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", + "iptables_unavailable": "Vous ne pouvez pas faire joujou avec iptables ici. Vous êtes sûrement dans un conteneur, autrement votre noyau ne le supporte pas.", + "ldap_initialized": "Répertoire LDAP initialisé avec succès", + "license_undefined": "indéfinie", + "mail_alias_remove_failed": "Impossible de supprimer l'adresse mail supplémentaire '{:s}'", + "mail_domain_unknown": "Domaine '{:s}' de l'adresse mail inconnu", + "mail_forward_remove_failed": "Impossible de supprimer l'adresse mail de transfert '{:s}'", + "maindomain_change_failed": "Impossible de modifier le domaine principal", + "maindomain_changed": "Domaine principal modifié avec succès", "monitor_disabled": "Suivi de l'état du serveur désactivé avec succès", - "monitor_not_enabled": "Le suivi de l'état du serveur n'est pas activé", + "monitor_enabled": "Suivi de l'état du serveur activé avec succès", "monitor_glances_con_failed": "Impossible de se connecter au serveur Glances", - "service_unknown": "Service '{:s}' inconnu", + "monitor_not_enabled": "Le suivi de l'état du serveur n'est pas activé", + "monitor_period_invalid": "Période de temps incorrect", + "monitor_stats_file_not_found": "Fichier de données de l'état du serveur introuvable", + "monitor_stats_no_update": "Aucune donnée de l'état du serveur à mettre à jour", + "monitor_stats_period_unavailable": "Aucune donnée de l'état du serveur disponible pour la période", + "mountpoint_unknown": "Point de montage inconnu", + "mysql_db_creation_failed": "Impossible de créer la base de donnée MySQL", + "mysql_db_init_failed": "Impossible d'initialiser la base de donnée MySQL", + "mysql_db_initialized": "Base de donnée MySQL initialisée avec succès", + "new_domain_required": "Vous devez spécifier le nouveau domaine principal", + "no_appslist_found": "Aucune liste d'applications trouvée", + "no_internet_connection": "Le serveur n'est pas connecté à Internet", + "no_ipv6_connectivity": "IPv6 n'est pas disponible", + "no_such_conf_file": "Le fichier {file:s} n’existe pas, il ne peut pas être copié", + "packages_no_upgrade": "Il n'y a aucun paquet à mettre à jour", + "packages_upgrade_critical_later": "Les paquets critiques ({:s}) seront mis à jour plus tard", + "packages_upgrade_failed": "Impossible de mettre à jour tous les paquets", + "path_removal_failed": "Impossible de supprimer le chemin {:s}", + "pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement", + "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)", + "pattern_email": "Doit être une adresse mail valide (ex. : someone@domain.org)", + "pattern_firstname": "Doit être un prénom valide", + "pattern_lastname": "Doit être un nom valide", + "pattern_listname": "Doit être composé uniquement de caractères alphanumérique et de tiret bas", + "pattern_password": "Doit être composé d'au moins 3 caractères", + "pattern_port": "Doit être un numéro de port valide (0-65535)", + "pattern_port_or_range": "Doit être un numéro de port valide (0-65535) ou une gamme de ports (ex : 100:200)", + "pattern_username": "Doit être composé uniquement de caractères alphanumérique minuscule et de tiret bas", + "port_already_closed": "Le port {} est déjà fermé pour les connexions {:s}", + "port_already_opened": "Le port {} est déjà ouvert pour les connexions {:s}", + "port_available": "Le port {} est disponible", + "port_unavailable": "Le port {} n'est pas disponible", + "restore_complete": "Restauration terminée", + "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]", + "restore_failed": "Impossible de restaurer le système", + "restore_running_app_script": "Lancement du script de restauration pour l'application '{app:s}'...", + "restore_running_hooks": "Exécution des scripts de restauration...", + "service_add_configuration": "Ajout du fichier de configuration {file:s}", "service_add_failed": "Impossible d'ajouter le service '{:s}'", "service_added": "Service ajouté avec succès", + "service_already_started": "Le service '{:s}' est déjà démarré", + "service_already_stopped": "Le service '{:s}' est déjà arrêté", + "service_cmd_exec_failed": "Impossible d'exécuter la commande '{:s}'", + "service_configuration_conflict": "Le fichier {file:s} a été modifié depuis sa dernière génération. Veuillez y appliquer les modifications manuellement ou utiliser l’option --force (ce qui écrasera toutes les modifications effectuées sur le fichier). Voici les différences:\n{diff:s}", + "service_disable_failed": "Impossible de désactiver le service '{:s}'", + "service_disabled": "Service '{:s}' désactivé avec succès", + "service_enable_failed": "Impossible d'activer le service '{:s}'", + "service_enabled": "Service '{:s}' activé avec succès", + "service_no_log": "Aucun journal a afficher pour le service '{:s}'", "service_remove_failed": "Impossible d'enlever le service '{:s}'", "service_removed": "Service enlevé avec succès", "service_start_failed": "Impossible de démarrer le service '{:s}'", - "service_already_started": "Le service '{:s}' est déjà démarré", "service_started": "Service '{:s}' démarré avec succès", - "service_stop_failed": "Impossible d'arrêter le service '{:s}'", - "service_already_stopped": "Le service '{:s}' est déjà arrêté", - "service_stopped": "Service '{:s}' arrêté avec succès", - "service_enable_failed": "Impossible d'activer le service '{:s}'", - "service_enabled": "Service '{:s}' activé avec succès", - "service_disable_failed": "Impossible de désactiver le service '{:s}'", - "service_disabled": "Service '{:s}' désactivé avec succès", "service_status_failed": "Impossible de déterminer le statut du service '{:s}'", - "service_no_log": "Aucun journal a afficher pour le service '{:s}'", - "service_cmd_exec_failed": "Impossible d'exécuter la commande '{:s}'", - "ldap_initialized": "Répertoire LDAP initialisé avec succès", - "admin_password_change_failed": "Impossible de modifier le mot de passe d'administration", - "admin_password_changed": "Mot de passe d'administration modifié avec succès", - "new_domain_required": "Vous devez spécifier le nouveau domaine principal", - "maindomain_change_failed": "Impossible de modifier le domaine principal", - "maindomain_changed": "Domaine principal modifié avec succès", - "yunohost_installing": "Installation de YunoHost...", + "service_stop_failed": "Impossible d'arrêter le service '{:s}'", + "service_stopped": "Service '{:s}' arrêté avec succès", + "service_unknown": "Service '{:s}' inconnu", + "services_configured": "La configuration a été générée avec succès", + "ssowat_conf_generated": "Configuration de SSOwat générée avec succès", + "ssowat_conf_updated": "Configuration persistante de SSOwat mise à jour avec succès", + "system_upgraded": "Système mis à jour avec succès", + "system_username_exists": "Le nom d'utilisateur existe déjà dans les utilisateurs système", + "unbackup_app": "L'application '{:s}' ne sera pas sauvegardée", + "unexpected_error": "Une erreur inattendue est survenue", + "unit_unknown": "Unité '{:s}' inconnue", + "unrestore_app": "L'application '{app:s}' ne sera pas restaurée", + "update_cache_failed": "Impossible de mettre à jour le cache de l'APT", + "updating_apt_cache": "Mise à jour de la liste des paquets disponibles...", + "upgrade_complete": "Mise à jour terminée", + "upgrading_packages": "Mise à jour des paquets...", + "upnp_dev_not_found": "Aucun périphérique compatible UPnP trouvé", + "upnp_disabled": "UPnP désactivé avec succès", + "upnp_enabled": "UPnP activé avec succès", + "upnp_port_open_failed": "Impossible d'ouvrir les ports avec UPnP", + "user_created": "Utilisateur créé avec succès", + "user_creation_failed": "Impossible de créer l'utilisateur", + "user_deleted": "Utilisateur supprimé avec succès", + "user_deletion_failed": "Impossible de supprimer l'utilisateur", + "user_info_failed": "Impossible de récupérer les informations de l'utilisateur", + "user_unknown": "Utilisateur inconnu", + "user_update_failed": "Impossible de modifier l'utilisateur", + "user_updated": "Utilisateur modifié avec succès", "yunohost_already_installed": "YunoHost est déjà installé", "yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification", "yunohost_configured": "YunoHost configuré avec succès", - "updating_apt_cache": "Mise à jour de la liste des paquets disponibles...", - "update_cache_failed": "Impossible de mettre à jour le cache de l'APT", - "packages_no_upgrade": "Il n'y a aucun paquet à mettre à jour", - "packages_upgrade_critical_later": "Les paquets critiques ({:s}) seront mis à jour plus tard", - "upgrading_packages": "Mise à jour des paquets...", - "packages_upgrade_failed": "Impossible de mettre à jour tous les paquets", - "system_upgraded": "Système mis à jour avec succès", - "backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde", - "backup_output_directory_forbidden": "Dossier de sortie interdit", - "backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide", - "backup_running_hooks": "Exécution des scripts de sauvegarde...", - "backup_creating_archive": "Création de l'archive de sauvegarde...", - "backup_extracting_archive": "Extraction de l'archive de sauvegarde...", - "backup_archive_open_failed": "Impossible d'ouvrir l'archive de sauvegarde", - "backup_archive_name_unknown": "Nom d'archive de sauvegarde locale inconnu", - "backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà", - "backup_complete": "Sauvegarde terminée", - "backup_invalid_archive": "Archive de sauvegarde incorrecte", - "restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]", - "restore_running_hooks": "Exécution des scripts de restauration...", - "restore_failed": "Impossible de restaurer le système", - "restore_complete": "Restauration terminée", - "unbackup_app": "L'application '{:s}' ne sera pas sauvegardée", - "unrestore_app": "L'application '{:s}' ne sera pas restaurée", - "field_invalid": "Champ incorrect : {:s}", - "mail_domain_unknown": "Domaine '{:s}' de l'adresse mail inconnu", - "mail_alias_remove_failed": "Impossible de supprimer l'adresse mail supplémentaire '{:s}'", - "mail_forward_remove_failed": "Impossible de supprimer l'adresse mail de transfert '{:s}'", - "system_username_exists": "Le nom d'utilisateur existe déjà dans les utilisateurs système", - "user_unknown": "Utilisateur inconnu", - "user_creation_failed": "Impossible de créer l'utilisateur", - "user_created": "Utilisateur créé avec succès", - "user_deletion_failed": "Impossible de supprimer l'utilisateur", - "user_deleted": "Utilisateur supprimé avec succès", - "user_update_failed": "Impossible de modifier l'utilisateur", - "user_updated": "Utilisateur modifié avec succès", - "user_info_failed": "Impossible de récupérer les informations de l'utilisateur", - "admin_password": "Mot de passe d'administration", - "ask_firstname": "Prénom", - "ask_lastname": "Nom", - "ask_email": "Adresse mail", - "ask_password": "Mot de passe", - "ask_current_admin_password": "Mot de passe d'administration actuel", - "ask_new_admin_password": "Nouveau mot de passe d'administration", - "ask_main_domain": "Domaine principal", - "ask_list_to_remove": "Liste à supprimer", - "pattern_username": "Doit être composé uniquement de caractères alphanumérique minuscule et de tiret bas", - "pattern_firstname": "Doit être un prénom valide", - "pattern_lastname": "Doit être un nom valide", - "pattern_email": "Doit être une adresse mail valide (ex. : someone@domain.org)", - "pattern_password": "Doit être composé d'au moins 3 caractères", - "pattern_domain": "Doit être un nom de domaine valide (ex : mon-domaine.org)", - "pattern_listname": "Doit être composé uniquement de caractères alphanumérique et de tiret bas", - "pattern_port": "Doit être un numéro de port valide (0-65535)", - "pattern_port_or_range": "Doit être un numéro de port valide (0-65535) ou une gamme de ports (ex : 100:200)", - "pattern_backup_archive_name": "Doit être un nom de fichier valide composé de caractères alphanumérique et -_. uniquement", - "format_datetime_short": "%d/%m/%Y %H:%M" + "yunohost_installing": "Installation de YunoHost...", + "yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter 'yunohost tools postinstall'." } diff --git a/locales/it.json b/locales/it.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/it.json @@ -0,0 +1 @@ +{} diff --git a/locales/nl.json b/locales/nl.json new file mode 100644 index 000000000..fca078dcf --- /dev/null +++ b/locales/nl.json @@ -0,0 +1,66 @@ +{ + "action_invalid": "Ongeldige actie '{:s}'", + "admin_password": "Administration password", + "app_already_installed": "{:s} is al geïnstalleerd", + "app_extraction_failed": "Kan installatiebestanden niet uitpakken", + "app_id_invalid": "Ongeldige app-id", + "app_install_files_invalid": "Ongeldige installatiebestanden", + "app_location_already_used": "Er is al een app geïnstalleerd op deze locatie", + "app_location_install_failed": "Kan app niet installeren op deze locatie", + "app_manifest_invalid": "Ongeldig app-manifest", + "app_no_upgrade": "Geen apps op te upgraden", + "app_not_installed": "{:s} is niet geinstalleerd ", + "app_recent_version_required": "{:s} vereist een nieuwere versie van moulinette", + "app_removed": "{:s} succesvol verwijderd", + "app_sources_fetch_failed": "Kan bronbestanden niet ophalen", + "app_unknown": "Onbekende app", + "app_upgrade_failed": "Kan niet alle apps updaten", + "app_upgraded": "{:s} succesvol geüpgrade ", + "appslist_fetched": "App-lijst succesvol aangemaakt.", + "appslist_removed": "App-lijst succesvol verwijderd", + "appslist_unknown": "Onbekende app-lijst", + "ask_current_admin_password": "Huidig administratorwachtwoord", + "ask_email": "Email-adres", + "ask_firstname": "Voornaam", + "ask_lastname": "Achternaam", + "ask_new_admin_password": "Nieuw administratorwachtwoord", + "ask_password": "Wachtwoord", + "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {:s} bij te werken", + "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst", + "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'", + "domain_cert_gen_failed": "Kan certificaat niet genereren", + "domain_created": "Domein succesvol aangemaakt", + "domain_creation_failed": "Kan domein niet aanmaken", + "domain_deleted": "Domein succesvol verwijderd", + "domain_deletion_failed": "Kan domein niet verwijderen", + "domain_dyndns_root_unknown": "Onbekend DynDNS root domein", + "domain_exists": "Domein bestaat al", + "domain_uninstall_app_first": "Een of meerdere apps zijn geïnstalleerd op dit domein, verwijder deze voordat u het domein verwijderd.", + "domain_unknown": "Onbekend domein", + "domain_zone_exists": "DNS zone bestand bestaat al", + "domain_zone_not_found": "DNS zone bestand niet gevonden voor domein: {:s}", + "done": "Voltooid.", + "downloading": "Downloaden...", + "dyndns_key_generating": "DNS sleutel word aangemaakt, wacht een moment...", + "executing_script": "Script uitvoeren...", + "extracting": "Uitpakken...", + "installation_complete": "Installatie voltooid", + "installation_failed": "Installatie gefaald", + "license_undefined": "undefined", + "mysql_db_creation_failed": "Aanmaken MySQL database gefaald", + "mysql_db_init_failed": "Initialiseren MySQL database gefaald", + "mysql_db_initialized": "MySQL database succesvol geïnitialiseerd", + "no_appslist_found": "Geen app-lijsten gevonden", + "no_internet_connection": "Server is niet verbonden met het internet", + "path_removal_failed": "Kan pad niet verwijderen {:s}", + "port_already_closed": "Poort {} is al gesloten voor {:s} verbindingen", + "port_already_opened": "Poort {} is al open voor {:s} verbindingen", + "port_available": "Poort {} is beschikbaar", + "port_unavailable": "Poort {} is niet beschikbaar", + "unexpected_error": "Er is een onbekende fout opgetreden", + "upgrade_complete": "Upgrade voltooid", + "upnp_dev_not_found": "Geen UPnP apparaten gevonden", + "upnp_disabled": "UPnP successvol uitgeschakeld", + "upnp_enabled": "UPnP succesvol ingeschakeld", + "upnp_port_open_failed": "Kan UPnP poorten niet openen" +} diff --git a/locales/pt.json b/locales/pt.json index 391e09e37..09a2e86b9 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,143 +1,143 @@ { - "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'", - "upgrade_complete": "Atualização completa", - "installation_complete": "Instalação concluída", - "installation_failed": "A instalação falhou", - "unexpected_error": "Ocorreu um erro inesperado", "action_invalid": "Invalid action '{:s}'", - "license_undefined": "indefinido", - "no_appslist_found": "Não foi encontrada a lista de aplicações", - "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", - "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas", - "appslist_fetched": "Lista de aplicações processada com êxito", - "appslist_unknown": "Lista de aplicaçoes desconhecida", - "appslist_removed": "Lista de aplicações removida com êxito", - "app_unknown": "Aplicação desconhecida", - "app_no_upgrade": "Não existem aplicações para atualizar", - "app_not_installed": "{:s} não está instalada", - "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {:s}", - "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette", - "app_upgraded": "{:s} atualizada com êxito", - "app_upgrade_failed": "Unable to upgrade all apps", - "app_id_invalid": "ID da aplicação invélida", + "admin_password": "Senha de administração", + "admin_password_change_failed": "Não foi possível alterar a senha", + "admin_password_changed": "Senha de administração alterada com êxito", "app_already_installed": "{:s} já está instalada", - "app_removed": "{:s} removida com êxito", + "app_extraction_failed": "Não foi possível extrair os ficheiros para instalação", + "app_id_invalid": "ID da aplicação invélida", + "app_install_files_invalid": "Ficheiros para instalação corrompidos", "app_location_already_used": "Já existe uma aplicação instalada neste diretório", "app_location_install_failed": "Não foi possível instalar a aplicação neste diretório", - "app_extraction_failed": "Não foi possível extrair os ficheiros para instalação", - "app_install_files_invalid": "Ficheiros para instalação corrompidos", "app_manifest_invalid": "Manifesto da aplicação inválido", + "app_no_upgrade": "Não existem aplicações para atualizar", + "app_not_installed": "{:s} não está instalada", + "app_recent_version_required": "{:s} requer uma versão mais recente da moulinette", + "app_removed": "{:s} removida com êxito", "app_sources_fetch_failed": "Impossível obter os códigos fontes", - "ssowat_conf_updated": "Configuração persistente SSOwat atualizada com êxito", - "ssowat_conf_generated": "Configuração SSOwat gerada com êxito", - "mysql_db_creation_failed": "Criação da base de dados MySQL falhou", - "mysql_db_init_failed": "Inicialização da base de dados MySQL falhou", - "mysql_db_initialized": "Base de dados MySQL iniciada com êxito", - "extracting": "Extração em curso...", - "downloading": "Transferência em curso...", - "executing_script": "A executar o script...", - "done": "Concluído.", - "path_removal_failed": "Incapaz remover o caminho {:s}", - "domain_unknown": "Domínio desconhecido", - "domain_dyndns_invalid": "Domínio inválido para ser utilizado com DynDNS", - "domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS", - "domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido", + "app_unknown": "Aplicação desconhecida", + "app_upgrade_failed": "Unable to upgrade all apps", + "app_upgraded": "{:s} atualizada com êxito", + "appslist_fetched": "Lista de aplicações processada com êxito", + "appslist_removed": "Lista de aplicações removida com êxito", + "appslist_retrieve_error": "Não foi possível obter a lista de aplicações remotas", + "appslist_unknown": "Lista de aplicaçoes desconhecida", + "ask_current_admin_password": "Senha de administração atual", + "ask_email": "Correio eletrónico", + "ask_firstname": "Primeiro nome", + "ask_lastname": "Último nome", + "ask_list_to_remove": "Lista para remover", + "ask_main_domain": "Domínio principal", + "ask_new_admin_password": "Senha de administração nova", + "ask_password": "Senha", + "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {:s}", + "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", "domain_cert_gen_failed": "Não foi possível gerar o certificado", + "domain_created": "Domínio criado com êxito", + "domain_creation_failed": "Não foi possível criar o domínio", + "domain_deleted": "Domínio removido com êxito", + "domain_deletion_failed": "Não foi possível eliminar o domínio", + "domain_dyndns_already_subscribed": "Já subscreveu um domínio DynDNS", + "domain_dyndns_invalid": "Domínio inválido para ser utilizado com DynDNS", + "domain_dyndns_root_unknown": "Domínio root (administrador) DynDNS desconhecido", "domain_exists": "O domínio já existe", + "domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.", + "domain_unknown": "Domínio desconhecido", "domain_zone_exists": "Ficheiro para zona DMZ já existe", "domain_zone_not_found": "Ficheiro para zona DMZ não encontrado no domínio {:s}", - "domain_creation_failed": "Não foi possível criar o domínio", - "domain_created": "Domínio criado com êxito", - "domain_uninstall_app_first": "Existem uma ou mais aplicações instaladas neste domínio. Por favor desinstale-as antes de proceder com a remoção do domínio.", - "domain_deletion_failed": "Não foi possível eliminar o domínio", - "domain_deleted": "Domínio removido com êxito", - "no_internet_connection": "O servidor não está ligado à Internet", - "dyndns_key_generating": "A chave DNS está a ser gerada, isto pode demorar um pouco...", - "dyndns_unavailable": "Subdomínio DynDNS indisponível", - "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {:s}", - "dyndns_registered": "Dom+inio DynDNS registado com êxito", - "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP a partir de DynDNS", - "dyndns_ip_updated": "Endereço IP atualizado com êxito a partir de DynDNS", + "done": "Concluído.", + "downloading": "Transferência em curso...", "dyndns_cron_installed": "Gestor de tarefas cron DynDNS instalado com êxito", "dyndns_cron_remove_failed": "Não foi possível remover o gestor de tarefas cron DynDNS", "dyndns_cron_removed": "Gestor de tarefas cron DynDNS removido com êxito", - "iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado.", + "dyndns_ip_update_failed": "Não foi possível atualizar o endereço IP a partir de DynDNS", + "dyndns_ip_updated": "Endereço IP atualizado com êxito a partir de DynDNS", + "dyndns_key_generating": "A chave DNS está a ser gerada, isto pode demorar um pouco...", + "dyndns_registered": "Dom+inio DynDNS registado com êxito", + "dyndns_registration_failed": "Não foi possível registar o domínio DynDNS: {:s}", + "dyndns_unavailable": "Subdomínio DynDNS indisponível", + "executing_script": "A executar o script...", + "extracting": "Extração em curso...", + "field_invalid": "Campo inválido '{:s}'", "firewall_reloaded": "Firewall recarregada com êxito", - "hook_choice_invalid": "Escolha inválida '{:s}'", "hook_argument_missing": "Argumento em falta '{:s}'", - "mountpoint_unknown": "Ponto de montagem desconhecido", - "unit_unknown": "Unidade desconhecida '{:s}'", - "monitor_period_invalid": "Período de tempo inválido", - "monitor_stats_no_update": "Não existem estatísticas de monitorização para atualizar", - "monitor_stats_file_not_found": "Ficheiro de estatísticas não encontrado", - "monitor_stats_period_unavailable": "Não existem estatísticas disponíveis para este período", - "monitor_enabled": "Monitorização do servidor ativada com êxito", + "hook_choice_invalid": "Escolha inválida '{:s}'", + "installation_complete": "Instalação concluída", + "installation_failed": "A instalação falhou", + "iptables_unavailable": "Não pode alterar aqui a iptables. Ou o seu kernel não o suporta ou está num espaço reservado.", + "ldap_initialized": "LDAP inicializada com êxito", + "license_undefined": "indefinido", + "mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{:s}'", + "mail_domain_unknown": "Domínio de endereço de correio desconhecido '{:s}'", + "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{:s}'", + "maindomain_change_failed": "Incapaz alterar o domínio raiz", + "maindomain_changed": "Domínio raiz alterado com êxito", "monitor_disabled": "Monitorização do servidor parada com êxito", - "monitor_not_enabled": "A monitorização do servidor não está ativa", + "monitor_enabled": "Monitorização do servidor ativada com êxito", "monitor_glances_con_failed": "Não foi possível ligar ao servidor Glances", - "service_unknown": "Serviço desconhecido '{:s}'", + "monitor_not_enabled": "A monitorização do servidor não está ativa", + "monitor_period_invalid": "Período de tempo inválido", + "monitor_stats_file_not_found": "Ficheiro de estatísticas não encontrado", + "monitor_stats_no_update": "Não existem estatísticas de monitorização para atualizar", + "monitor_stats_period_unavailable": "Não existem estatísticas disponíveis para este período", + "mountpoint_unknown": "Ponto de montagem desconhecido", + "mysql_db_creation_failed": "Criação da base de dados MySQL falhou", + "mysql_db_init_failed": "Inicialização da base de dados MySQL falhou", + "mysql_db_initialized": "Base de dados MySQL iniciada com êxito", + "new_domain_required": "Deve escrever um novo domínio principal", + "no_appslist_found": "Não foi encontrada a lista de aplicações", + "no_internet_connection": "O servidor não está ligado à Internet", + "packages_no_upgrade": "Não existem pacotes para atualizar", + "packages_upgrade_critical_later": "Os pacotes críticos ({:s}) serão atualizados depois", + "packages_upgrade_failed": "Não foi possível atualizar todos os pacotes", + "path_removal_failed": "Incapaz remover o caminho {:s}", + "pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)", + "pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)", + "pattern_firstname": "Deve ser um primeiro nome válido", + "pattern_lastname": "Deve ser um último nome válido", + "pattern_listname": "Apenas são permitidos caracteres alfanuméricos e travessões", + "pattern_password": "Deve ter no mínimo 3 caracteres", + "pattern_port": "Deve ser um número de porta válido (entre 0-65535)", + "pattern_username": "Must be lower-case alphanumeric and underscore characters only", "service_add_failed": "Incapaz adicionar serviço '{:s}'", "service_added": "Serviço adicionado com êxito", + "service_already_started": "O serviço '{:s}' já está em execussão", + "service_already_stopped": "O serviço '{:s}' já está parado", + "service_cmd_exec_failed": "Incapaz executar o comando '{:s}'", + "service_disable_failed": "Incapaz desativar o serviço '{:s}'", + "service_disabled": "O serviço '{:s}' foi desativado com êxito", + "service_enable_failed": "Incapaz de ativar o serviço '{:s}'", + "service_enabled": "Serviço '{:s}' ativado com êxito", + "service_no_log": "Não existem registos para mostrar do serviço '{:s}'", "service_remove_failed": "Incapaz de remover o serviço '{:s}'", "service_removed": "Serviço eliminado com êxito", "service_start_failed": "Não foi possível iniciar o serviço '{:s}'", - "service_already_started": "O serviço '{:s}' já está em execussão", "service_started": "O serviço '{:s} foi iniciado com êxito", - "service_stop_failed": "Incapaz parar o serviço '{:s}", - "service_already_stopped": "O serviço '{:s}' já está parado", - "service_stopped": "O serviço '{:s}' foi parado com êxito", - "service_enable_failed": "Incapaz de ativar o serviço '{:s}'", - "service_enabled": "Serviço '{:s}' ativado com êxito", - "service_disable_failed": "Incapaz desativar o serviço '{:s}'", - "service_disabled": "O serviço '{:s}' foi desativado com êxito", "service_status_failed": "Incapaz determinar o estado do serviço '{:s}'", - "service_no_log": "Não existem registos para mostrar do serviço '{:s}'", - "service_cmd_exec_failed": "Incapaz executar o comando '{:s}'", - "ldap_initialized": "LDAP inicializada com êxito", - "admin_password_change_failed": "Não foi possível alterar a senha", - "admin_password_changed": "Senha de administração alterada com êxito", - "new_domain_required": "Deve escrever um novo domínio principal", - "maindomain_change_failed": "Incapaz alterar o domínio raiz", - "maindomain_changed": "Domínio raiz alterado com êxito", - "yunohost_installing": "A instalar a YunoHost...", + "service_stop_failed": "Incapaz parar o serviço '{:s}", + "service_stopped": "O serviço '{:s}' foi parado com êxito", + "service_unknown": "Serviço desconhecido '{:s}'", + "ssowat_conf_generated": "Configuração SSOwat gerada com êxito", + "ssowat_conf_updated": "Configuração persistente SSOwat atualizada com êxito", + "system_upgraded": "Sistema atualizado com êxito", + "system_username_exists": "O utilizador já existe no registo do sistema", + "unexpected_error": "Ocorreu um erro inesperado", + "unit_unknown": "Unidade desconhecida '{:s}'", + "update_cache_failed": "Não foi possível atualizar os cabeçalhos APT", + "updating_apt_cache": "A atualizar a lista de pacotes disponíveis...", + "upgrade_complete": "Atualização completa", + "upgrading_packages": "Atualização de pacotes em curso...", + "user_created": "Utilizador criado com êxito", + "user_creation_failed": "Não foi possível criar o utilizador", + "user_deleted": "Utilizador eliminado com êxito", + "user_deletion_failed": "Incapaz eliminar o utilizador", + "user_info_failed": "Incapaz obter informações sobre o utilizador", + "user_unknown": "Utilizador desconhecido", + "user_update_failed": "Não foi possível atualizar o utilizador", + "user_updated": "Utilizador atualizado com êxito", "yunohost_already_installed": "A YunoHost já está instalada...", "yunohost_ca_creation_failed": "Incapaz criar o certificado de autoridade", "yunohost_configured": "YunoHost configurada com êxito", - "updating_apt_cache": "A atualizar a lista de pacotes disponíveis...", - "update_cache_failed": "Não foi possível atualizar os cabeçalhos APT", - "packages_no_upgrade": "Não existem pacotes para atualizar", - "packages_upgrade_critical_later": "Os pacotes críticos ({:s}) serão atualizados depois", - "upgrading_packages": "Atualização de pacotes em curso...", - "packages_upgrade_failed": "Não foi possível atualizar todos os pacotes", - "system_upgraded": "Sistema atualizado com êxito", - "field_invalid": "Campo inválido '{:s}'", - "mail_domain_unknown": "Domínio de endereço de correio desconhecido '{:s}'", - "mail_alias_remove_failed": "Não foi possível remover a etiqueta de correio '{:s}'", - "mail_forward_remove_failed": "Não foi possível remover o reencaminhamento de correio '{:s}'", - "user_unknown": "Utilizador desconhecido", - "system_username_exists": "O utilizador já existe no registo do sistema", - "user_creation_failed": "Não foi possível criar o utilizador", - "user_created": "Utilizador criado com êxito", - "user_deletion_failed": "Incapaz eliminar o utilizador", - "user_deleted": "Utilizador eliminado com êxito", - "user_update_failed": "Não foi possível atualizar o utilizador", - "user_updated": "Utilizador atualizado com êxito", - "user_info_failed": "Incapaz obter informações sobre o utilizador", - "admin_password": "Senha de administração", - "ask_firstname": "Primeiro nome", - "ask_lastname": "Último nome", - "ask_email": "Correio eletrónico", - "ask_password": "Senha", - "ask_current_admin_password": "Senha de administração atual", - "ask_new_admin_password": "Senha de administração nova", - "ask_main_domain": "Domínio principal", - "ask_list_to_remove": "Lista para remover", - "pattern_username": "Must be lower-case alphanumeric and underscore characters only", - "pattern_firstname": "Deve ser um primeiro nome válido", - "pattern_lastname": "Deve ser um último nome válido", - "pattern_email": "Deve ser um endereço de correio válido (p.e. alguem@dominio.org)", - "pattern_password": "Deve ter no mínimo 3 caracteres", - "pattern_domain": "Deve ser um nome de domínio válido (p.e. meu-dominio.org)", - "pattern_listname": "Apenas são permitidos caracteres alfanuméricos e travessões", - "pattern_port": "Deve ser um número de porta válido (entre 0-65535)" + "yunohost_installing": "A instalar a YunoHost...", + "yunohost_not_installed": "YunoHost ainda não está corretamente configurado. Por favor execute as 'ferramentas pós-instalação yunohost'" } diff --git a/locales/tr.json b/locales/tr.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/locales/tr.json @@ -0,0 +1 @@ +{} diff --git a/lib/yunohost/__init__.py b/src/yunohost/__init__.py similarity index 71% rename from lib/yunohost/__init__.py rename to src/yunohost/__init__.py index 97cd1f5f3..fd8b653bd 100644 --- a/lib/yunohost/__init__.py +++ b/src/yunohost/__init__.py @@ -24,15 +24,26 @@ ## Packages versions def get_version(package): + """Get the version of package""" from moulinette.utils import process return process.check_output( "dpkg-query -W -f='${{Version}}' {0}".format(package) ).strip() def get_versions(*args, **kwargs): + """Get the version of each YunoHost package""" from collections import OrderedDict return OrderedDict([ ('moulinette', get_version('moulinette')), - ('moulinette-yunohost', get_version('moulinette-yunohost')), + ('yunohost', get_version('yunohost')), ('yunohost-admin', get_version('yunohost-admin')), ]) + +def has_min_version(min_version, package='yunohost', strict=False): + """Check if a package has minimum version""" + from distutils.version import LooseVersion, StrictVersion + cmp_cls = StrictVersion if strict else LooseVersion + version = cmp_cls(get_version(package)) + if version >= cmp_cls(min_version): + return True + return False diff --git a/lib/yunohost/app.py b/src/yunohost/app.py similarity index 76% rename from lib/yunohost/app.py rename to src/yunohost/app.py index 688f8a4f7..6b40b3b55 100644 --- a/lib/yunohost/app.py +++ b/src/yunohost/app.py @@ -39,6 +39,9 @@ import subprocess from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +from . import has_min_version +from .service import service_log + logger = getActionLogger('yunohost.app') repo_path = '/var/cache/yunohost/repo' @@ -70,7 +73,7 @@ def app_fetchlist(url=None, name=None): Keyword argument: name -- Name of the list (default yunohost) - url -- URL of remote JSON list (default https://yunohost.org/list.json) + url -- URL of remote JSON list (default https://yunohost.org/official.json) """ # Create app path if not exists @@ -78,7 +81,7 @@ def app_fetchlist(url=None, name=None): except OSError: os.makedirs(repo_path) if url is None: - url = 'https://yunohost.org/list.json' + url = 'https://yunohost.org/official.json' name = 'yunohost' else: if name is None: @@ -166,24 +169,29 @@ def app_list(offset=None, limit=None, filter=None, raw=False): sorted_app_dict[sorted_keys] = app_dict[sorted_keys] i = 0 - for app_id, app_info in sorted_app_dict.items(): + for app_id, app_info_dict in sorted_app_dict.items(): if i < limit: - if (filter and ((filter in app_id) or (filter in app_info['manifest']['name']))) or not filter: + if (filter and ((filter in app_id) or (filter in app_info_dict['manifest']['name']))) or not filter: installed = _is_installed(app_id) if raw: - app_info['installed'] = installed + app_info_dict['installed'] = installed if installed: - app_info['status'] = _get_app_status(app_id) - list_dict[app_id] = app_info + app_info_dict['status'] = _get_app_status(app_id) + list_dict[app_id] = app_info_dict else: + label = None + if installed: + app_info_dict_raw = app_info(app=app_id, raw=True) + label = app_info_dict_raw['settings']['label'] list_dict.append({ 'id': app_id, - 'name': app_info['manifest']['name'], + 'name': app_info_dict['manifest']['name'], + 'label': label, 'description': _value_for_locale( - app_info['manifest']['description']), + app_info_dict['manifest']['description']), # FIXME: Temporarly allow undefined license - 'license': app_info['manifest'].get('license', + 'license': app_info_dict['manifest'].get('license', m18n.n('license_undefined')), 'installed': installed }) @@ -207,11 +215,10 @@ def app_info(app, show_status=False, raw=False): """ if not _is_installed(app): raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app)) + m18n.n('app_not_installed', app=app)) if raw: ret = app_list(filter=app, raw=True)[app] - with open(apps_setting_path + app +'/settings.yml') as f: - ret['settings'] = yaml.load(f) + ret['settings'] = _get_app_settings(app) return ret app_setting_path = apps_setting_path + app @@ -245,34 +252,43 @@ def app_map(app=None, raw=False, user=None): app -- Specific app to map """ - + apps = [] result = {} - for app_id in os.listdir(apps_setting_path): - if app and (app != app_id): + if app is not None: + if not _is_installed(app): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_not_installed', app=app)) + apps = [app,] + else: + apps = os.listdir(apps_setting_path) + + for app_id in apps: + app_settings = _get_app_settings(app_id) + if not app_settings: continue - - if user is not None: - app_dict = app_info(app=app_id, raw=True) - if ('mode' not in app_dict['settings']) or ('mode' in app_dict['settings'] and app_dict['settings']['mode'] == 'private'): - if 'allowed_users' in app_dict['settings'] and user not in app_dict['settings']['allowed_users'].split(','): - continue - - with open(apps_setting_path + app_id +'/settings.yml') as f: - app_settings = yaml.load(f) - if 'domain' not in app_settings: continue + if user is not None: + if ('mode' not in app_settings \ + or ('mode' in app_settings \ + and app_settings['mode'] == 'private')) \ + and 'allowed_users' in app_settings \ + and user not in app_settings['allowed_users'].split(','): + continue + + domain = app_settings['domain'] + path = app_settings.get('path', '/') if raw: - if app_settings['domain'] not in result: - result[app_settings['domain']] = {} - result[app_settings['domain']][app_settings['path']] = { - 'label': app_settings['label'], - 'id': app_settings['id'] + if domain not in result: + result[domain] = {} + result[domain][path] = { + 'label': app_settings['label'], + 'id': app_settings['id'] } else: - result[app_settings['domain']+app_settings['path']] = app_settings['label'] + result[domain + path] = app_settings['label'] return result @@ -307,18 +323,13 @@ def app_upgrade(auth, app=[], url=None, file=None): installed = _is_installed(app_id) if not installed: raise MoulinetteError(errno.ENOPKG, - m18n.n('app_not_installed', app_id)) + m18n.n('app_not_installed', app=app_id)) if app_id in upgraded_apps: continue - if '__' in app_id: - original_app_id = app_id[:app_id.index('__')] - else: - original_app_id = app_id - current_app_dict = app_info(app_id, raw=True) - new_app_dict = app_info(original_app_id, raw=True) + new_app_dict = app_info(app_id, raw=True) if file: manifest = _extract_app_from_file(file) @@ -337,9 +348,11 @@ def app_upgrade(auth, app=[], url=None, file=None): continue # Check min version - if 'min_version' in manifest and __version__ < manifest['min_version']: + if 'min_version' in manifest \ + and not has_min_version(manifest['min_version']): raise MoulinetteError(errno.EPERM, - m18n.n('app_recent_version_required', app_id)) + m18n.n('app_recent_version_required', + app=app_id)) app_setting_path = apps_setting_path +'/'+ app_id @@ -347,36 +360,20 @@ def app_upgrade(auth, app=[], url=None, file=None): status = _get_app_status(app_id) status['remote'] = manifest.get('remote', None) - if original_app_id != app_id: - # Replace original_app_id with the forked one in scripts - for script in os.listdir(app_tmp_folder +'/scripts'): - #TODO: do it with sed ? - if script[:1] != '.': - with open(app_tmp_folder +'/scripts/'+ script, "r") as sources: - lines = sources.readlines() - with open(app_tmp_folder +'/scripts/'+ script, "w") as sources: - for line in lines: - sources.write(re.sub(r''+ original_app_id +'', app_id, line)) - - if 'hooks' in os.listdir(app_tmp_folder): - for hook in os.listdir(app_tmp_folder +'/hooks'): - #TODO: do it with sed ? - if hook[:1] != '.': - with open(app_tmp_folder +'/hooks/'+ hook, "r") as sources: - lines = sources.readlines() - with open(app_tmp_folder +'/hooks/'+ hook, "w") as sources: - for line in lines: - sources.write(re.sub(r''+ original_app_id +'', app_id, line)) - # Clean hooks and add new ones hook_remove(app_id) if 'hooks' in os.listdir(app_tmp_folder): for hook in os.listdir(app_tmp_folder +'/hooks'): hook_add(app_id, app_tmp_folder +'/hooks/'+ hook) + # Retrieve arguments list for upgrade script + # TODO: Allow to specify arguments + args_list = _parse_args_from_manifest(manifest, 'upgrade', auth=auth) + args_list.append(app_id) + # Execute App upgrade script os.system('chown -hR admin: %s' % install_tmp) - if hook_exec(app_tmp_folder +'/scripts/upgrade') != 0: + if hook_exec(app_tmp_folder +'/scripts/upgrade', args_list) != 0: raise MoulinetteError(errno.EIO, m18n.n('installation_failed')) else: now = int(time.time()) @@ -394,7 +391,7 @@ def app_upgrade(auth, app=[], url=None, file=None): # So much win upgraded_apps.append(app_id) - msignals.display(m18n.n('app_upgraded', app_id), 'success') + msignals.display(m18n.n('app_upgraded', app=app_id), 'success') if not upgraded_apps: raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) @@ -441,9 +438,11 @@ def app_install(auth, app, label=None, args=None): app_id = manifest['id'] # Check min version - if 'min_version' in manifest and __version__ < manifest['min_version']: + if 'min_version' in manifest \ + and not has_min_version(manifest['min_version']): raise MoulinetteError(errno.EPERM, - m18n.n('app_recent_version_required', app_id)) + m18n.n('app_recent_version_required', + app=app_id)) # Check if app can be forked instance_number = _installed_instance_number(app_id, last=True) + 1 @@ -452,30 +451,14 @@ def app_install(auth, app, label=None, args=None): raise MoulinetteError(errno.EEXIST, m18n.n('app_already_installed', app_id)) - app_id_forked = app_id + '__' + str(instance_number) + # Change app_id to the forked app id + app_id = app_id + '__' + str(instance_number) - # Replace app_id with the new one in scripts - for script in os.listdir(app_tmp_folder +'/scripts'): - #TODO: do it with sed ? - if script[:1] != '.': - with open(app_tmp_folder +'/scripts/'+ script, "r") as sources: - lines = sources.readlines() - with open(app_tmp_folder +'/scripts/'+ script, "w") as sources: - for line in lines: - sources.write(re.sub(r''+ app_id +'', app_id_forked, line)) - - if 'hooks' in os.listdir(app_tmp_folder): - for hook in os.listdir(app_tmp_folder +'/hooks'): - #TODO: do it with sed ? - if hook[:1] != '.': - with open(app_tmp_folder +'/hooks/'+ hook, "r") as sources: - lines = sources.readlines() - with open(app_tmp_folder +'/hooks/'+ hook, "w") as sources: - for line in lines: - sources.write(re.sub(r''+ app_id +'', app_id_forked, line)) - - # Change app_id for the rest of the process - app_id = app_id_forked + # Retrieve arguments list for install script + args_dict = {} if not args else \ + dict(urlparse.parse_qsl(args, keep_blank_values=True)) + args_list = _parse_args_from_manifest(manifest, 'install', args_dict, auth) + args_list.append(app_id) # Prepare App settings app_setting_path = apps_setting_path +'/'+ app_id @@ -504,20 +487,13 @@ def app_install(auth, app, label=None, args=None): os.system('chown -R admin: '+ app_tmp_folder) - try: - if args is None: - args = '' - args_dict = dict(urlparse.parse_qsl(args, keep_blank_values=True)) - except: - args_dict = {} - # Execute App install script os.system('chown -hR admin: %s' % install_tmp) # Move scripts and manifest to the right place os.system('cp %s/manifest.json %s' % (app_tmp_folder, app_setting_path)) os.system('cp -R %s/scripts %s' % (app_tmp_folder, app_setting_path)) try: - if hook_exec(app_tmp_folder + '/scripts/install', args_dict) == 0: + if hook_exec(app_tmp_folder + '/scripts/install', args_list) == 0: # Store app status with open(app_setting_path + '/status.json', 'w+') as f: json.dump(status, f) @@ -561,7 +537,8 @@ def app_remove(auth, app): from yunohost.hook import hook_exec, hook_remove if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app)) + raise MoulinetteError(errno.EINVAL, + m18n.n('app_not_installed', app=app)) app_setting_path = apps_setting_path + app @@ -574,8 +551,10 @@ def app_remove(auth, app): os.system('chown -R admin: /tmp/yunohost_remove') os.system('chmod -R u+rX /tmp/yunohost_remove') - if hook_exec('/tmp/yunohost_remove/scripts/remove') == 0: - msignals.display(m18n.n('app_removed', app), 'success') + args_list = [app] + + if hook_exec('/tmp/yunohost_remove/scripts/remove', args_list) == 0: + msignals.display(m18n.n('app_removed', app=app), 'success') if os.path.exists(app_setting_path): shutil.rmtree(app_setting_path) shutil.rmtree('/tmp/yunohost_remove') @@ -595,47 +574,50 @@ def app_addaccess(auth, apps, users=[]): from yunohost.user import user_list, user_info from yunohost.hook import hook_callback + result = {} + if not users: users = user_list(auth)['users'].keys() - - if not isinstance(users, list): users = [users] - if not isinstance(apps, list): apps = [apps] + elif not isinstance(users, list): + users = [users,] + if not isinstance(apps, list): + apps = [apps,] for app in apps: - if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app)) - - with open(apps_setting_path + app +'/settings.yml') as f: - app_settings = yaml.load(f) + app_settings = _get_app_settings(app) + if not app_settings: + continue if 'mode' not in app_settings: app_setting(app, 'mode', 'private') app_settings['mode'] = 'private' if app_settings['mode'] == 'private': + allowed_users = set() if 'allowed_users' in app_settings: - new_users = app_settings['allowed_users'] - else: - new_users = '' + allowed_users = set(app_settings['allowed_users'].split(',')) for allowed_user in users: - if allowed_user not in new_users.split(','): + if allowed_user not in allowed_users: try: user_info(auth, allowed_user) except MoulinetteError: + # FIXME: Add username keyword in user_unknown + logger.warning('{0}{1}'.format( + m18n.g('colon', m18n.n('user_unknown')), + allowed_user)) continue - if new_users == '': - new_users = allowed_user - else: - new_users = new_users +','+ allowed_user + allowed_users.add(allowed_user) - app_setting(app, 'allowed_users', new_users.strip()) + new_users = ','.join(allowed_users) + app_setting(app, 'allowed_users', new_users) hook_callback('post_app_addaccess', args=[app, new_users]) + result[app] = allowed_users + app_ssowatconf(auth) - return { 'allowed_users': new_users.split(',') } + return { 'allowed_users': result } def app_removeaccess(auth, apps, users=[]): @@ -650,45 +632,43 @@ def app_removeaccess(auth, apps, users=[]): from yunohost.user import user_list from yunohost.hook import hook_callback + result = {} + remove_all = False if not users: remove_all = True - if not isinstance(users, list): users = [users] - if not isinstance(apps, list): apps = [apps] + elif not isinstance(users, list): + users = [users,] + if not isinstance(apps, list): + apps = [apps,] + for app in apps: - new_users = '' + app_settings = _get_app_settings(app) + if not app_settings: + continue + allowed_users = set() - if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app)) - - with open(apps_setting_path + app +'/settings.yml') as f: - app_settings = yaml.load(f) - - if 'skipped_uris' not in app_settings or app_settings['skipped_uris'] != '/': + if app_settings.get('skipped_uris', '') != '/': if remove_all: - new_users = '' + pass elif 'allowed_users' in app_settings: for allowed_user in app_settings['allowed_users'].split(','): if allowed_user not in users: - if new_users == '': - new_users = allowed_user - else: - new_users = new_users +','+ allowed_user + allowed_users.add(allowed_user) else: - new_users = '' - for username in user_list(auth)['users'].keys(): - if username not in users: - if new_users == '': - new_users = username - new_users += ',' + username + for allowed_user in user_list(auth)['users'].keys(): + if allowed_user not in users: + allowed_users.add(allowed_user) - app_setting(app, 'allowed_users', new_users.strip()) + new_users = ','.join(allowed_users) + app_setting(app, 'allowed_users', new_users) hook_callback('post_app_removeaccess', args=[app, new_users]) + result[app] = allowed_users + app_ssowatconf(auth) - return { 'allowed_users': new_users.split(',') } + return { 'allowed_users': result } def app_clearaccess(auth, apps): @@ -704,12 +684,9 @@ def app_clearaccess(auth, apps): if not isinstance(apps, list): apps = [apps] for app in apps: - if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app)) - - with open(apps_setting_path + app +'/settings.yml') as f: - app_settings = yaml.load(f) + app_settings = _get_app_settings(app) + if not app_settings: + continue if 'mode' in app_settings: app_setting(app, 'mode', delete=True) @@ -722,6 +699,29 @@ def app_clearaccess(auth, apps): app_ssowatconf(auth) +def app_debug(app): + """ + Display debug informations for an app + + Keyword argument: + app + """ + with open(apps_setting_path + app + '/manifest.json') as f: + manifest = json.loads(f.read()) + + return { + 'name': manifest['id'], + 'label': manifest['name'], + 'services': [{ + "name": x, + "logs": [{ + "file_name": y, + "file_content": "\n".join(z), + } for (y, z) in sorted(service_log(x).items(), key=lambda x: x[0])], + } for x in sorted(manifest.get("services", []))] + } + + def app_makedefault(auth, app, domain=None): """ Redirect domain root to an app @@ -733,12 +733,7 @@ def app_makedefault(auth, app, domain=None): """ from yunohost.domain import domain_list - if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, m18n.n('app_not_installed', app)) - - with open(apps_setting_path + app +'/settings.yml') as f: - app_settings = yaml.load(f) - + app_settings = _get_app_settings(app) app_domain = app_settings['domain'] app_path = app_settings['path'] @@ -781,23 +776,13 @@ def app_setting(app, key, value=None, delete=False): delete -- Delete the key """ - if not _is_installed(app): - raise MoulinetteError(errno.EINVAL, - m18n.n('app_not_installed', app)) - - settings_file = apps_setting_path + app +'/settings.yml' - try: - with open(settings_file) as f: - app_settings = yaml.load(f) - except IOError: - # Do not fail if setting file is not there - app_settings = {} + app_settings = _get_app_settings(app) if value is None and not delete: try: return app_settings[key] except: - logger.exception("cannot get app setting '%s' for '%s'", key, app) + logger.info("cannot get app setting '%s' for '%s'", key, app) return None else: yaml_settings=['redirected_urls','redirected_regex'] @@ -811,7 +796,8 @@ def app_setting(app, key, value=None, delete=False): value=yaml.load(value) app_settings[key] = value - with open(settings_file, 'w') as f: + with open(os.path.join( + apps_setting_path, app, 'settings.yml'), 'w') as f: yaml.safe_dump(app_settings, f, default_flow_style=False) @@ -1011,6 +997,29 @@ def app_ssowatconf(auth): msignals.display(m18n.n('ssowat_conf_generated'), 'success') +def _get_app_settings(app_id): + """ + Get settings of an installed app + + Keyword arguments: + app_id -- The app id + + """ + if not _is_installed(app_id): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_not_installed', app=app_id)) + try: + with open(os.path.join( + apps_setting_path, app_id, 'settings.yml')) as f: + settings = yaml.load(f) + if app_id == settings['id']: + return settings + except (IOError, TypeError, KeyError): + logger.exception(m18n.n('app_not_correctly_installed', + app=app_id)) + return {} + + def _get_app_status(app_id, format_date=False): """ Get app status or create it if needed @@ -1304,6 +1313,116 @@ def _encode_string(value): return value +def _parse_args_from_manifest(manifest, action, args={}, auth=None): + """Parse arguments needed for an action from the manifest + + Retrieve specified arguments for the action from the manifest, and parse + given args according to that. If some required arguments are not provided, + its values will be asked if interaction is possible. + Parsed arguments will be returned as a list of strings to pass directly + to the proper script. + + Keyword arguments: + manifest -- The app manifest to use + action -- The action to retrieve arguments for + args -- A dictionnary of arguments to parse + + """ + from yunohost.domain import domain_list + from yunohost.user import user_info + + args_list = [] + try: + action_args = manifest['arguments'][action] + except KeyError: + logger.debug("no arguments found for '%s' in manifest", action) + else: + for arg in action_args: + arg_name = arg['name'] + arg_type = arg.get('type', 'string') + arg_default = arg.get('default', None) + arg_choices = arg.get('choices', []) + arg_value = None + + # Transpose default value for boolean type and set it to + # false if not defined. + if arg_type == 'boolean': + arg_default = 1 if arg_default else 0 + + # Attempt to retrieve argument value + if arg_name in args: + arg_value = args[arg_name] + else: + if os.isatty(1) and 'ask' in arg: + # Retrieve proper ask string + ask_string = _value_for_locale(arg['ask']) + + # Append extra strings + if arg_type == 'boolean': + ask_string += ' [0 | 1]' + elif arg_choices: + ask_string += ' [{0}]'.format(' | '.join(arg_choices)) + if arg_default is not None: + ask_string += ' (default: {0})'.format(arg_default) + + input_string = msignals.prompt(ask_string) + if (input_string == '' or input_string is None) \ + and arg_default is not None: + arg_value = arg_default + else: + arg_value = input_string + elif arg_default is not None: + arg_value = arg_default + + # Validate argument value + if (arg_value is None or arg_value == '') \ + and not arg.get('optional', False): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_required', name=arg_name)) + elif not arg_value: + args_list.append('') + continue + + # Validate argument choice + if arg_choices and arg_value not in arg_choices: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_choice_invalid', + name=arg_name, choices=', '.join(arg_choices))) + + # Validate argument type + if arg_type == 'domain': + if arg_value not in domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=m18n.n('domain_unknown'))) + elif arg_type == 'user': + try: + user_info(auth, arg_value) + except MoulinetteError as e: + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=e.strerror)) + elif arg_type == 'app': + if not _is_installed(arg_value): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_invalid', + name=arg_name, error=m18n.n('app_unknown'))) + elif arg_type == 'boolean': + if isinstance(arg_value, bool): + arg_value = 1 if arg_value else 0 + else: + try: + arg_value = int(arg_value) + if arg_value not in [0, 1]: + raise ValueError() + except (TypeError, ValueError): + raise MoulinetteError(errno.EINVAL, + m18n.n('app_argument_choice_invalid', + name=arg_name, choices='0, 1')) + args_list.append(arg_value) + return args_list + + def is_true(arg): """ Convert a string into a boolean diff --git a/lib/yunohost/backup.py b/src/yunohost/backup.py similarity index 58% rename from lib/yunohost/backup.py rename to src/yunohost/backup.py index 0e9ab63c5..0fe3a1baa 100644 --- a/lib/yunohost/backup.py +++ b/src/yunohost/backup.py @@ -32,6 +32,7 @@ import time import tarfile import shutil import subprocess +from glob import glob from collections import OrderedDict from moulinette.core import MoulinetteError @@ -62,27 +63,27 @@ def backup_create(name=None, description=None, output_directory=None, """ # TODO: Add a 'clean' argument to clean output directory - from yunohost.hook import hook_callback, hook_exec + from yunohost.hook import hook_info, hook_callback, hook_exec tmp_dir = None # Validate what to backup if ignore_hooks and ignore_apps: raise MoulinetteError(errno.EINVAL, - m18n.n('backup_action_required')) + m18n.n('backup_action_required')) # Validate and define backup name timestamp = int(time.time()) if not name: - name = str(timestamp) + name = time.strftime('%Y%m%d-%H%M%S') if name in backup_list()['archives']: raise MoulinetteError(errno.EINVAL, - m18n.n('backup_archive_name_exists')) + m18n.n('backup_archive_name_exists')) # Validate additional arguments if no_compress and not output_directory: raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_required')) + m18n.n('backup_output_directory_required')) if output_directory: output_directory = os.path.abspath(output_directory) @@ -90,19 +91,17 @@ def backup_create(name=None, description=None, output_directory=None, if output_directory.startswith(archives_path) or \ re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$', output_directory): - logger.error("forbidden output directory '%'", output_directory) raise MoulinetteError(errno.EINVAL, - m18n.n('backup_output_directory_forbidden')) + m18n.n('backup_output_directory_forbidden')) # Create the output directory if not os.path.isdir(output_directory): - logger.info("creating output directory '%s'", output_directory) + logger.debug("creating output directory '%s'", output_directory) os.makedirs(output_directory, 0750) # Check that output directory is empty elif no_compress and os.listdir(output_directory): - logger.error("not empty output directory '%'", output_directory) raise MoulinetteError(errno.EIO, - m18n.n('backup_output_directory_not_empty')) + m18n.n('backup_output_directory_not_empty')) # Define temporary directory if no_compress: @@ -114,8 +113,8 @@ def backup_create(name=None, description=None, output_directory=None, if not tmp_dir: tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): - logger.warning("temporary directory for backup '%s' already exists", - tmp_dir) + logger.debug("temporary directory for backup '%s' already exists", + tmp_dir) filesystem.rm(tmp_dir, recursive=True) filesystem.mkdir(tmp_dir, 0750, parents=True, uid='admin') @@ -124,7 +123,7 @@ def backup_create(name=None, description=None, output_directory=None, if not ret['failed']: filesystem.rm(tmp_dir, True, True) else: - msignals.display(m18n.n('backup_cleaning_failed'), 'warning') + logger.warning(m18n.n('backup_cleaning_failed')) # Initialize backup info info = { @@ -136,9 +135,35 @@ def backup_create(name=None, description=None, output_directory=None, # Run system hooks if not ignore_hooks: - msignals.display(m18n.n('backup_running_hooks')) - hooks_ret = hook_callback('backup', hooks, args=[tmp_dir]) - info['hooks'] = hooks_ret['succeed'] + # Check hooks availibility + hooks_filtered = set() + if hooks: + for hook in hooks: + try: + hook_info('backup', hook) + except: + logger.error(m18n.n('backup_hook_unknown', hook=hook)) + else: + hooks_filtered.add(hook) + + if not hooks or hooks_filtered: + logger.info(m18n.n('backup_running_hooks')) + ret = hook_callback('backup', hooks_filtered, args=[tmp_dir]) + if ret['succeed']: + info['hooks'] = ret['succeed'] + + # Save relevant restoration hooks + tmp_hooks_dir = tmp_dir + '/hooks/restore' + filesystem.mkdir(tmp_hooks_dir, 0750, True, uid='admin') + for h in ret['succeed'].keys(): + try: + i = hook_info('restore', h) + except: + logger.warning(m18n.n('restore_hook_unavailable', + hook=h), exc_info=1) + else: + for f in i['hooks']: + shutil.copy(f['path'], tmp_hooks_dir) # Backup apps if not ignore_apps: @@ -150,8 +175,7 @@ def backup_create(name=None, description=None, output_directory=None, if apps: for a in apps: if a not in apps_list: - logger.warning("app '%s' not found", a) - msignals.display(m18n.n('unbackup_app', a), 'warning') + logger.warning(m18n.n('unbackup_app', app=a)) else: apps_filtered.add(a) else: @@ -162,33 +186,18 @@ def backup_create(name=None, description=None, output_directory=None, for app_id in apps_filtered: app_setting_path = '/etc/yunohost/apps/' + app_id - tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_id) - - # Check if the app has a backup script + # Check if the app has a backup and restore script app_script = app_setting_path + '/scripts/backup' - if not os.path.isfile(app_script): - logger.warning("backup script '%s' not found", app_script) - msignals.display(m18n.n('unbackup_app', app_id), - 'warning') - continue - - # Copy the app restore script app_restore_script = app_setting_path + '/scripts/restore' - if os.path.isfile(app_script): - try: - filesystem.mkdir(tmp_app_dir, 0750, True, uid='admin') - shutil.copy(app_restore_script, tmp_app_dir) - except: - logger.exception("error while copying restore script of '%s'", app_id) - msignals.display(m18n.n('restore_app_copy_failed', app=app_id), - 'warning') - else: - logger.warning("restore script '%s' not found", app_script) - msignals.display(m18n.n('unrestorable_app', app_id), - 'warning') + if not os.path.isfile(app_script): + logger.warning(m18n.n('unbackup_app', app=app_id)) + continue + elif not os.path.isfile(app_restore_script): + logger.warning(m18n.n('unrestore_app', app=app_id)) + tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_id) tmp_app_bkp_dir = tmp_app_dir + '/backup' - msignals.display(m18n.n('backup_running_app_script', app_id)) + logger.info(m18n.n('backup_running_app_script', app=app_id)) try: # Prepare backup directory for the app filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin') @@ -196,11 +205,10 @@ def backup_create(name=None, description=None, output_directory=None, # Copy app backup script in a temporary folder and execute it subprocess.call(['install', '-Dm555', app_script, tmp_script]) - hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_id]) + hook_exec(tmp_script, args=[tmp_app_bkp_dir, app_id], + raise_on_error=True) except: - logger.exception("error while executing backup of '%s'", app_id) - msignals.display(m18n.n('backup_app_failed', app=app_id), - 'error') + logger.exception(m18n.n('backup_app_failed', app=app_id)) # Cleaning app backup directory shutil.rmtree(tmp_app_dir, ignore_errors=True) else: @@ -214,10 +222,10 @@ def backup_create(name=None, description=None, output_directory=None, finally: filesystem.rm(tmp_script, force=True) - # Check if something has been saved - if ignore_hooks and not info['apps']: - _clean_tmp_dir(1) - raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) + # Check if something has been saved + if not info['hooks'] and not info['apps']: + _clean_tmp_dir(1) + raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done')) # Create backup info file with open("%s/info.json" % tmp_dir, 'w') as f: @@ -225,7 +233,7 @@ def backup_create(name=None, description=None, output_directory=None, # Create the archive if not no_compress: - msignals.display(m18n.n('backup_creating_archive')) + logger.info(m18n.n('backup_creating_archive')) archive_file = "%s/%s.tar.gz" % (output_directory, name) try: tar = tarfile.open(archive_file, "w:gz") @@ -238,17 +246,16 @@ def backup_create(name=None, description=None, output_directory=None, try: tar = tarfile.open(archive_file, "w:gz") except: - logger.exception("unable to open '%s' for writing " - "after creating directory '%s'", - archive_file, archives_path) + logger.debug("unable to open '%s' for writing", + archive_file, exc_info=1) tar = None else: - logger.exception("unable to open the archive '%s' for writing", - archive_file) + logger.debug("unable to open '%s' for writing", + archive_file, exc_info=1) if tar is None: _clean_tmp_dir(2) raise MoulinetteError(errno.EIO, - m18n.n('backup_archive_open_failed')) + m18n.n('backup_archive_open_failed')) tar.add(tmp_dir, arcname='') tar.close() @@ -260,7 +267,7 @@ def backup_create(name=None, description=None, output_directory=None, if tmp_dir != output_directory: _clean_tmp_dir() - msignals.display(m18n.n('backup_complete'), 'success') + logger.success(m18n.n('backup_complete')) # Return backup info info['name'] = name @@ -279,9 +286,13 @@ def backup_restore(name, hooks=[], apps=[], ignore_apps=False, ignore_hooks=Fals force -- Force restauration on an already installed system """ - from yunohost.hook import hook_add - from yunohost.hook import hook_callback - from yunohost.hook import hook_exec + from yunohost.hook import hook_info, hook_callback, hook_exec + from yunohost.hook import custom_hook_folder + + # Validate what to restore + if ignore_hooks and ignore_apps: + raise MoulinetteError(errno.EINVAL, + m18n.n('restore_action_required')) # Retrieve and open the archive info = backup_info(name) @@ -289,37 +300,50 @@ def backup_restore(name, hooks=[], apps=[], ignore_apps=False, ignore_hooks=Fals try: tar = tarfile.open(archive_file, "r:gz") except: - logger.exception("unable to open the archive '%s' for reading", - archive_file) + logger.debug("cannot open backup archive '%s'", + archive_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_archive_open_failed')) # Check temporary directory tmp_dir = "%s/tmp/%s" % (backup_path, name) if os.path.isdir(tmp_dir): - logger.warning("temporary directory for restoration '%s' already exists", - tmp_dir) + logger.debug("temporary directory for restoration '%s' already exists", + tmp_dir) os.system('rm -rf %s' % tmp_dir) + def _clean_tmp_dir(retcode=0): + ret = hook_callback('post_backup_restore', args=[tmp_dir, retcode]) + if not ret['failed']: + filesystem.rm(tmp_dir, True, True) + else: + logger.warning(m18n.n('restore_cleaning_failed')) + # Extract the tarball - msignals.display(m18n.n('backup_extracting_archive')) + logger.info(m18n.n('backup_extracting_archive')) tar.extractall(tmp_dir) tar.close() # Retrieve backup info + info_file = "%s/info.json" % tmp_dir try: - with open("%s/info.json" % tmp_dir, 'r') as f: + with open(info_file, 'r') as f: info = json.load(f) except IOError: - logger.error("unable to retrieve backup info from '%s/info.json'", - tmp_dir) + logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) else: - logger.info("restoring from backup '%s' created on %s", name, - time.ctime(info['created_at'])) + logger.debug("restoring from backup '%s' created on %s", name, + time.ctime(info['created_at'])) + + # Initialize restauration summary result + result = { + 'apps': [], + 'hooks': {}, + } # Check if YunoHost is installed if os.path.isfile('/etc/yunohost/installed'): - msignals.display(m18n.n('yunohost_already_installed'), 'warning') + logger.warning(m18n.n('yunohost_already_installed')) if not force: try: # Ask confirmation for restoring @@ -340,73 +364,116 @@ def backup_restore(name, hooks=[], apps=[], ignore_apps=False, ignore_hooks=Fals with open("%s/yunohost/current_host" % tmp_dir, 'r') as f: domain = f.readline().rstrip() except IOError: - logger.error("unable to retrieve domain from '%s/yunohost/current_host'", - tmp_dir) + logger.debug("unable to retrieve domain from " + "'%s/yunohost/current_host'", tmp_dir, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) - logger.info("executing the post-install...") + logger.debug("executing the post-install...") tools_postinstall(domain, 'yunohost', True) - # Run hooks + # Run system hooks if not ignore_hooks: - if hooks is None or len(hooks)==0: - hooks=info['hooks'].keys() - - hooks_filtered=list(set(hooks) & set(info['hooks'].keys())) - hooks_unexecuted=set(hooks) - set(info['hooks'].keys()) - for hook in hooks_unexecuted: - logger.warning("hook '%s' not in this backup", hook) - msignals.display(m18n.n('backup_hook_unavailable', hook), 'warning') - msignals.display(m18n.n('restore_running_hooks')) - hook_callback('restore', hooks_filtered, args=[tmp_dir]) - + # Filter hooks to execute + hooks_list = set(info['hooks'].keys()) + _is_hook_in_backup = lambda h: True + if hooks: + def _is_hook_in_backup(h): + if h in hooks_list: + return True + logger.error(m18n.n('backup_archive_hook_not_exec', hook=h)) + return False + else: + hooks = hooks_list + + # Check hooks availibility + hooks_filtered = set() + for h in hooks: + if not _is_hook_in_backup(h): + continue + try: + hook_info('restore', h) + except: + tmp_hooks = glob('{:s}/hooks/restore/*-{:s}'.format(tmp_dir, h)) + if not tmp_hooks: + logger.exception(m18n.n('restore_hook_unavailable', hook=h)) + continue + # Add restoration hook from the backup to the system + # FIXME: Refactor hook_add and use it instead + restore_hook_folder = custom_hook_folder + 'restore' + filesystem.mkdir(restore_hook_folder, 755, True) + for f in tmp_hooks: + logger.debug("adding restoration hook '%s' to the system " + "from the backup archive '%s'", f, archive_file) + shutil.copy(f, restore_hook_folder) + hooks_filtered.add(h) + + if hooks_filtered: + logger.info(m18n.n('restore_running_hooks')) + ret = hook_callback('restore', hooks_filtered, args=[tmp_dir]) + result['hooks'] = ret['succeed'] + # Add apps restore hook if not ignore_apps: + from yunohost.app import _is_installed + # Filter applications to restore apps_list = set(info['apps'].keys()) apps_filtered = set() - if not apps: - apps=apps_list - - from yunohost.app import _is_installed - for app_id in apps: - if app_id not in apps_list: - logger.warning("app '%s' not found", app_id) - msignals.display(m18n.n('unrestore_app', app_id), 'warning') - elif _is_installed(app_id): - logger.warning("app '%s' already installed", app_id) - msignals.display(m18n.n('restore_already_installed_app', app=app_id), 'warning') - elif not os.path.isfile('{:s}/apps/{:s}/restore'.format(tmp_dir, app_id)): - logger.warning("backup for '%s' doesn't contain a restore script", app_id) - msignals.display(m18n.n('no_restore_script', app=app_id), 'warning') - else: - apps_filtered.add(app_id) + if apps: + for a in apps: + if a not in apps_list: + logger.error(m18n.n('backup_archive_app_not_found', app=a)) + else: + apps_filtered.add(a) + else: + apps_filtered = apps_list - for app_id in apps_filtered: - app_bkp_dir='{:s}/apps/{:s}'.format(tmp_dir, app_id) + for app_id in apps_filtered: + tmp_app_dir = '{:s}/apps/{:s}'.format(tmp_dir, app_id) + + # Check if the app is not already installed + if _is_installed(app_id): + logger.error(m18n.n('restore_already_installed_app', + app=app_id)) + continue + + # Check if the app has a restore script + app_script = tmp_app_dir + '/settings/scripts/restore' + if not os.path.isfile(app_script): + logger.warning(m18n.n('unrestore_app', app=app_id)) + continue + + tmp_script = '/tmp/restore_' + app_id + app_setting_path = '/etc/yunohost/apps/' + app_id + logger.info(m18n.n('restore_running_app_script', app=app_id)) try: - # Copy app settings - app_setting_path = '/etc/yunohost/apps/' + app_id - shutil.copytree(app_bkp_dir + '/settings', app_setting_path ) - + # Copy app settings and set permissions + shutil.copytree(tmp_app_dir + '/settings', app_setting_path) + filesystem.chmod(app_setting_path, 0555, 0444, True) + filesystem.chmod(app_setting_path + '/settings.yml', 0400) + # Execute app restore script - app_restore_script=app_bkp_dir+'/restore' - tmp_script = '/tmp/restore_%s_%s' % (name,app_id) - subprocess.call(['install', '-Dm555', app_restore_script, tmp_script]) - hook_exec(tmp_script, args=[app_bkp_dir+'/backup', app_id]) - + subprocess.call(['install', '-Dm555', app_script, tmp_script]) + hook_exec(tmp_script, args=[tmp_app_dir + '/backup', app_id], + raise_on_error=True) except: - logger.exception("error while restoring backup of '%s'", app_id) - msignals.display(m18n.n('restore_app_failed', app=app_id), - 'error') - # Cleaning settings directory - shutil.rmtree(app_setting_path + '/settings', ignore_errors=True) + logger.exception(m18n.n('restore_app_failed', app=app_id)) + # Cleaning app directory + shutil.rmtree(app_setting_path, ignore_errors=True) + else: + result['apps'].append(app_id) + finally: + filesystem.rm(tmp_script, force=True) + # Check if something has been restored + if not result['hooks'] and not result['apps']: + _clean_tmp_dir(1) + raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done')) - # Remove temporary directory - os.system('rm -rf %s' % tmp_dir) + _clean_tmp_dir() + logger.success(m18n.n('restore_complete')) - msignals.display(m18n.n('restore_complete'), 'success') + return result def backup_list(with_info=False, human_readable=False): @@ -423,8 +490,8 @@ def backup_list(with_info=False, human_readable=False): try: # Retrieve local archives archives = os.listdir(archives_path) - except OSError as e: - logger.info("unable to iterate over local archives: %s", str(e)) + except OSError: + logger.debug("unable to iterate over local archives", exc_info=1) else: # Iterate over local archives for f in archives: @@ -458,8 +525,8 @@ def backup_info(name, with_details=False, human_readable=False): archive_file = '%s/%s.tar.gz' % (archives_path, name) if not os.path.isfile(archive_file): - logger.error("no local backup archive found at '%s'", archive_file) - raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown',name)) + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_name_unknown', name=name)) info_file = "%s/%s.info.json" % (archives_path, name) try: @@ -468,8 +535,7 @@ def backup_info(name, with_details=False, human_readable=False): info = json.load(f) except: # TODO: Attempt to extract backup info file from tarball - logger.exception("unable to retrive backup info file '%s'", - info_file) + logger.debug("unable to load '%s'", info_file, exc_info=1) raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive')) size = os.path.getsize(archive_file) @@ -506,15 +572,15 @@ def backup_delete(name): info_file = "%s/%s.info.json" % (archives_path, name) for backup_file in [archive_file,info_file]: if not os.path.isfile(backup_file): - logger.error("no local backup archive found at '%s'", backup_file) - raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown', backup_file)) + raise MoulinetteError(errno.EIO, + m18n.n('backup_archive_name_unknown', name=backup_file)) try: os.remove(backup_file) except: - logger.exception("unable to delete '%s'", backup_file) + logger.debug("unable to delete '%s'", backup_file, exc_info=1) raise MoulinetteError(errno.EIO, - m18n.n('backup_delete_error',backup_file)) + m18n.n('backup_delete_error', path=backup_file)) hook_callback('post_backup_delete', args=[name]) - msignals.display(m18n.n('backup_deleted'), 'success') + logger.success(m18n.n('backup_deleted')) diff --git a/lib/yunohost/domain.py b/src/yunohost/domain.py similarity index 86% rename from lib/yunohost/domain.py rename to src/yunohost/domain.py index ee53bb39e..535b94d40 100644 --- a/lib/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -80,6 +80,7 @@ def domain_add(auth, domain, dyndns=False): """ from yunohost.service import service_regenconf + from yunohost.hook import hook_callback attr_dict = { 'objectClass' : ['mailDomain', 'top'] } try: @@ -172,6 +173,8 @@ def domain_add(auth, domain, dyndns=False): except: pass raise + hook_callback('post_domain_add', args=[domain]) + msignals.display(m18n.n('domain_created'), 'success') @@ -185,6 +188,7 @@ def domain_remove(auth, domain, force=False): """ from yunohost.service import service_regenconf + from yunohost.hook import hook_callback if not force and domain not in domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) @@ -211,4 +215,41 @@ def domain_remove(auth, domain, force=False): service_regenconf(service='dnsmasq') os.system('yunohost app ssowatconf > /dev/null 2>&1') + hook_callback('post_domain_remove', args=[domain]) + msignals.display(m18n.n('domain_deleted'), 'success') + + +def domain_dns_conf(domain): + """ + Generate DNS configuration for a domain + + Keyword argument: + domain -- Domain name + """ + + ip4 = urlopen("http://ip.yunohost.org").read().strip() + + result = "@ 1400 IN A {ip4}\n* 1400 IN A {ip4}\n".format(ip4=ip4) + + ip6 = None + + try: + ip6 = urlopen("http://ip6.yunohost.org").read().strip() + except Exception: + pass + else: + result += "@ 1400 IN AAAA {ip6}\n* 1400 IN AAAA {ip6}\n".format(ip6=ip6) + + result += "\n_xmpp-client._tcp 14400 IN SRV 0 5 5222 {domain}.\n_xmpp-server._tcp 14400 IN SRV 0 5 5269 {domain}.\n".format(domain=domain) + + result += "muc 1800 IN CNAME @\npubsub 1800 IN CNAME @\nvjud 1800 IN CNAME @\n\n" + + result += "@ 1400 IN MX 10 {domain}.\n".format(domain=domain) + + if ip6 is None: + result += '@ 1400 IN TXT "v=spf1 a mx ip4:{ip4} -all"\n'.format(ip4=ip4) + else: + result += '@ 1400 IN TXT "v=spf1 a mx ip4:{ip4} ip6:{ip6} -all"\n'.format(ip4=ip4, ip6=ip6) + + return result diff --git a/lib/yunohost/dyndns.py b/src/yunohost/dyndns.py similarity index 99% rename from lib/yunohost/dyndns.py rename to src/yunohost/dyndns.py index b4eb376dd..e1d0afd40 100644 --- a/lib/yunohost/dyndns.py +++ b/src/yunohost/dyndns.py @@ -24,7 +24,6 @@ Subscribe and Update DynDNS Hosts """ import os -import sys import requests import re import json @@ -97,7 +96,7 @@ def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None # Send subscription try: r = requests.post('https://%s/key/%s' % (subscribe_host, base64.b64encode(key)), data={ 'subdomain': domain }) - except ConnectionError: + except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) if r.status_code != 201: try: error = json.loads(r.text)['error'] @@ -130,7 +129,7 @@ def dyndns_update(dyn_host="dynhost.yunohost.org", domain=None, key=None, ip=Non if ip is None: try: new_ip = requests.get('http://ip.yunohost.org').text - except ConnectionError: + except requests.ConnectionError: raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) else: new_ip = ip diff --git a/lib/yunohost/firewall.py b/src/yunohost/firewall.py similarity index 86% rename from lib/yunohost/firewall.py rename to src/yunohost/firewall.py index c0b06e64b..678ff7db5 100644 --- a/lib/yunohost/firewall.py +++ b/src/yunohost/firewall.py @@ -83,8 +83,7 @@ def firewall_allow(protocol, port, ipv4_only=False, ipv6_only=False, firewall[i][p].append(port) else: ipv = "IPv%s" % i[3] - msignals.display(m18n.n('port_already_opened', port, ipv), - 'warning') + logger.warning(m18n.n('port_already_opened', port, ipv)) # Add port forwarding with UPnP if not no_upnp and port not in firewall['uPnP'][p]: firewall['uPnP'][p].append(port) @@ -112,7 +111,7 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, firewall = firewall_list(raw=True) # Validate port - if ':' not in port: + if not isinstance(port, int) and ':' not in port: port = int(port) # Validate protocols @@ -141,8 +140,7 @@ def firewall_disallow(protocol, port, ipv4_only=False, ipv6_only=False, firewall[i][p].remove(port) else: ipv = "IPv%s" % i[3] - msignals.display(m18n.n('port_already_closed', port, ipv), - 'warning') + logger.warning(m18n.n('port_already_closed', port, ipv)) # Remove port forwarding with UPnP if upnp and port in firewall['uPnP'][p]: firewall['uPnP'][p].remove(port) @@ -188,10 +186,12 @@ def firewall_list(raw=False, by_ip_version=False, list_forwarded=False): return ret -def firewall_reload(): +def firewall_reload(skip_upnp=False): """ Reload all firewall rules + Keyword arguments: + skip_upnp -- Do not refresh port forwarding using UPnP """ from yunohost.hook import hook_callback @@ -206,15 +206,15 @@ def firewall_reload(): # Retrieve firewall rules and UPnP status firewall = firewall_list(raw=True) - upnp = firewall_upnp()['enabled'] + upnp = firewall_upnp()['enabled'] if not skip_upnp else False # IPv4 try: process.check_output("iptables -L") except process.CalledProcessError as e: - logger.info('iptables seems to be not available, it outputs:\n%s', - prependlines(e.output.rstrip(), '> ')) - msignals.display(m18n.n('iptables_unavailable'), 'info') + logger.debug('iptables seems to be not available, it outputs:\n%s', + prependlines(e.output.rstrip(), '> ')) + logger.warning(m18n.n('iptables_unavailable')) else: rules = [ "iptables -F", @@ -241,9 +241,9 @@ def firewall_reload(): try: process.check_output("ip6tables -L") except process.CalledProcessError as e: - logger.info('ip6tables seems to be not available, it outputs:\n%s', - prependlines(e.output.rstrip(), '> ')) - msignals.display(m18n.n('ip6tables_unavailable'), 'info') + logger.debug('ip6tables seems to be not available, it outputs:\n%s', + prependlines(e.output.rstrip(), '> ')) + logger.warning(m18n.n('ip6tables_unavailable')) else: rules = [ "ip6tables -F", @@ -280,9 +280,9 @@ def firewall_reload(): os.system("service fail2ban restart") if errors: - msignals.display(m18n.n('firewall_rules_cmd_failed'), 'warning') + logger.warning(m18n.n('firewall_rules_cmd_failed')) else: - msignals.display(m18n.n('firewall_reloaded'), 'success') + logger.success(m18n.n('firewall_reloaded')) return firewall_list() @@ -304,7 +304,7 @@ def firewall_upnp(action='status', no_refresh=False): # Compatibility with previous version if action == 'reload': - logger.warning("'reload' action is deprecated and will be removed") + logger.info("'reload' action is deprecated and will be removed") try: # Remove old cron job os.remove('/etc/cron.d/yunohost-firewall') @@ -320,6 +320,11 @@ def firewall_upnp(action='status', no_refresh=False): with open(upnp_cron_job, 'w+') as f: f.write('*/50 * * * * root ' '/usr/bin/yunohost firewall upnp status >>/dev/null\n') + # Open port 1900 to receive discovery message + if 1900 not in firewall['ipv4']['UDP']: + firewall_allow('UDP', 1900, no_upnp=True, no_reload=True) + if not enabled: + firewall_reload(skip_upnp=True) enabled = True elif action == 'disable' or (not enabled and action == 'status'): try: @@ -342,14 +347,14 @@ def firewall_upnp(action='status', no_refresh=False): nb_dev = upnpc.discover() logger.debug('found %d UPnP device(s)', int(nb_dev)) if nb_dev < 1: - msignals.display(m18n.n('upnp_dev_not_found'), 'error') + logger.error(m18n.n('upnp_dev_not_found')) enabled = False else: try: # Select UPnP device upnpc.selectigd() except: - logger.exception('unable to select UPnP device') + logger.info('unable to select UPnP device', exc_info=1) enabled = False else: # Iterate over ports @@ -367,11 +372,12 @@ def firewall_upnp(action='status', no_refresh=False): upnpc.addportmapping(port, protocol, upnpc.lanaddr, port, 'yunohost firewall: port %d' % port, '') except: - logger.exception('unable to add port %d using UPnP', - port) + logger.info('unable to add port %d using UPnP', + port, exc_info=1) enabled = False if enabled != firewall['uPnP']['enabled']: + firewall = firewall_list(raw=True) firewall['uPnP']['enabled'] = enabled # Make a backup and update firewall file @@ -382,13 +388,19 @@ def firewall_upnp(action='status', no_refresh=False): if not no_refresh: # Display success message if needed if action == 'enable' and enabled: - msignals.display(m18n.n('upnp_enabled'), 'success') + logger.success(m18n.n('upnp_enabled')) elif action == 'disable' and not enabled: - msignals.display(m18n.n('upnp_disabled'), 'success') + logger.success(m18n.n('upnp_disabled')) # Make sure to disable UPnP elif action != 'disable' and not enabled: firewall_upnp('disable', no_refresh=True) + if not enabled and (action == 'enable' or 1900 in firewall['ipv4']['UDP']): + # Close unused port 1900 + firewall_disallow('UDP', 1900, no_reload=True) + if not no_refresh: + firewall_reload(skip_upnp=True) + if action == 'enable' and not enabled: raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed')) return { 'enabled': enabled } @@ -441,6 +453,6 @@ def _update_firewall_file(rules): def _on_rule_command_error(returncode, cmd, output): """Callback for rules commands error""" # Log error and continue commands execution - logger.error('"%s" returned non-zero exit status %d:\n%s', - cmd, returncode, prependlines(output.rstrip(), '> ')) + logger.info('"%s" returned non-zero exit status %d:\n%s', + cmd, returncode, prependlines(output.rstrip(), '> ')) return True diff --git a/lib/yunohost/hook.py b/src/yunohost/hook.py similarity index 69% rename from lib/yunohost/hook.py rename to src/yunohost/hook.py index 96ffff35d..2b0902b6f 100644 --- a/lib/yunohost/hook.py +++ b/src/yunohost/hook.py @@ -29,14 +29,15 @@ import re import json import errno import subprocess +from glob import iglob from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger +from moulinette.utils import log hook_folder = '/usr/share/yunohost/hooks/' custom_hook_folder = '/etc/yunohost/hooks.d/' -logger = getActionLogger('yunohost.hook') +logger = log.getActionLogger('yunohost.hook') def hook_add(app, file): @@ -77,6 +78,46 @@ def hook_remove(app): except OSError: pass +def hook_info(action, name): + """ + Get information about a given hook + + Keyword argument: + action -- Action name + name -- Hook name + + """ + hooks = [] + priorities = set() + + # Search in custom folder first + for h in iglob('{:s}{:s}/*-{:s}'.format( + custom_hook_folder, action, name)): + priority, _ = _extract_filename_parts(os.path.basename(h)) + priorities.add(priority) + hooks.append({ + 'priority': priority, + 'path': h, + }) + # Append non-overwritten system hooks + for h in iglob('{:s}{:s}/*-{:s}'.format( + hook_folder, action, name)): + priority, _ = _extract_filename_parts(os.path.basename(h)) + if priority not in priorities: + hooks.append({ + 'priority': priority, + 'path': h, + }) + + if not hooks: + raise MoulinetteError(errno.EINVAL, m18n.n('hook_name_unknown', name)) + return { + 'action': action, + 'name': name, + 'hooks': hooks, + } + + def hook_list(action, list_by='name', show_info=False): """ List available hooks for an action @@ -194,7 +235,7 @@ def hook_callback(action, hooks=[], args=None): if key == n or key.startswith("%s_" % n) \ and key not in all_hooks: all_hooks.append(key) - + # Iterate over given hooks names list for n in all_hooks: try: @@ -223,14 +264,9 @@ def hook_callback(action, hooks=[], args=None): state = 'succeed' filename = '%s-%s' % (priority, name) try: - ret = hook_exec(info['path'], args=args) - except: - logger.exception("error while executing hook '%s'", - info['path']) - state = 'failed' - if ret != 0: - logger.error("error while executing hook '%s', retcode: %d", - info['path'], ret) + hook_exec(info['path'], args=args, raise_on_error=True) + except MoulinetteError as e: + logger.error(str(e)) state = 'failed' try: result[state][name].append(info['path']) @@ -239,111 +275,67 @@ def hook_callback(action, hooks=[], args=None): return result -def hook_check(file): - """ - Parse the script file and get arguments - - Keyword argument: - file -- File to check - - """ - try: - with open(file[:file.index('scripts/')] + 'manifest.json') as f: - manifest = json.loads(str(f.read())) - except: - raise MoulinetteError(errno.EIO, m18n.n('app_manifest_invalid')) - - action = file[file.index('scripts/') + 8:] - if 'arguments' in manifest and action in manifest['arguments']: - return manifest['arguments'][action] - else: - return {} - - -def hook_exec(file, args=None): +def hook_exec(path, args=None, raise_on_error=False, no_trace=False): """ Execute hook from a file with arguments Keyword argument: - file -- Script to execute - args -- Arguments to pass to the script + path -- Path of the script to execute + args -- A list of arguments to pass to the script + raise_on_error -- Raise if the script returns a non-zero exit code + no_trace -- Do not print each command that will be executed """ - from moulinette.utils.stream import NonBlockingStreamReader + from moulinette.utils.process import call_async_output from yunohost.app import _value_for_locale - if isinstance(args, list): - arg_list = args - else: - required_args = hook_check(file) - if args is None: - args = {} + # Validate hook path + if path[0] != '/': + path = os.path.realpath(path) + if not os.path.isfile(path): + raise MoulinetteError(errno.EIO, m18n.g('file_not_exist')) - arg_list = [] - for arg in required_args: - if arg['name'] in args: - if 'choices' in arg and args[arg['name']] not in arg['choices']: - raise MoulinetteError(errno.EINVAL, - m18n.n('hook_choice_invalid', args[arg['name']])) - arg_list.append(args[arg['name']]) - else: - if os.isatty(1) and 'ask' in arg: - # Retrieve proper ask string - ask_string = _value_for_locale(arg['ask']) + # Construct command variables + cmd_fdir, cmd_fname = os.path.split(path) + cmd_fname = './{0}'.format(cmd_fname) - # Append extra strings - if 'choices' in arg: - ask_string += ' ({:s})'.format('|'.join(arg['choices'])) - if 'default' in arg: - ask_string += ' (default: {:s})'.format(arg['default']) - - input_string = msignals.prompt(ask_string) - - if input_string == '' and 'default' in arg: - input_string = arg['default'] - - arg_list.append(input_string) - elif 'default' in arg: - arg_list.append(arg['default']) - else: - raise MoulinetteError(errno.EINVAL, - m18n.n('hook_argument_missing', arg['name'])) - - file_path = "./" - if "/" in file and file[0:2] != file_path: - file_path = os.path.dirname(file) - file = file.replace(file_path +"/", "") - - #TODO: Allow python script - - arg_str = '' - if arg_list: + cmd_args = '' + if args and isinstance(args, list): # Concatenate arguments and escape them with double quotes to prevent # bash related issue if an argument is empty and is not the last - arg_str = '"{:s}"'.format('" "'.join(str(s) for s in arg_list)) + cmd_args = '"{:s}"'.format('" "'.join(str(s) for s in args)) - msignals.display(m18n.n('executing_script')) + # Construct command to execute + command = ['sudo', '-u', 'admin', '-H', 'sh', '-c'] + if no_trace: + cmd = 'cd "{0:s}" && /bin/bash "{1:s}" {2:s}' + else: + # use xtrace on fd 7 which is redirected to stdout + cmd = 'cd "{0:s}" && BASH_XTRACEFD=7 /bin/bash -x "{1:s}" {2:s} 7>&1' + command.append(cmd.format(cmd_fdir, cmd_fname, cmd_args)) - p = subprocess.Popen( - ['sudo', '-u', 'admin', '-H', 'sh', '-c', 'cd "{:s}" && ' \ - '/bin/bash -x "{:s}" {:s}'.format( - file_path, file, arg_str)], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=False) + if logger.isEnabledFor(log.DEBUG): + logger.info(m18n.n('executing_command', command=' '.join(command))) + else: + logger.info(m18n.n('executing_script', script='{0}/{1}'.format( + cmd_fdir, cmd_fname))) - # Wrap and get process ouput - stream = NonBlockingStreamReader(p.stdout) - while True: - line = stream.readline(True, 0.1) - if not line: - # Check if process has terminated - returncode = p.poll() - if returncode is not None: - break + # Define output callbacks and call command + callbacks = ( + lambda l: logger.info(l.rstrip()), + lambda l: logger.warning(l.rstrip()), + ) + returncode = call_async_output(command, callbacks, shell=False) + + # Check and return process' return code + if returncode is None: + if raise_on_error: + raise MoulinetteError(m18n.n('hook_exec_not_terminated')) else: - msignals.display(line.rstrip(), 'log') - stream.close() - + logger.error(m18n.n('hook_exec_not_terminated')) + return 1 + elif raise_on_error and returncode != 0: + raise MoulinetteError(m18n.n('hook_exec_failed')) return returncode diff --git a/lib/yunohost/monitor.py b/src/yunohost/monitor.py similarity index 100% rename from lib/yunohost/monitor.py rename to src/yunohost/monitor.py diff --git a/lib/yunohost/service.py b/src/yunohost/service.py similarity index 90% rename from lib/yunohost/service.py rename to src/yunohost/service.py index 2a69b4d2d..3823aef4a 100644 --- a/lib/yunohost/service.py +++ b/src/yunohost/service.py @@ -34,6 +34,7 @@ import difflib import hashlib from moulinette.core import MoulinetteError +from moulinette.utils import log template_dir = os.getenv( 'YUNOHOST_TEMPLATE_DIR', @@ -44,6 +45,9 @@ conf_backup_dir = os.getenv( '/home/yunohost.backup/conffiles' ) +logger = log.getActionLogger('yunohost.service') + + def service_add(name, status=None, log=None, runlevel=None): """ Add a custom service @@ -73,7 +77,7 @@ def service_add(name, status=None, log=None, runlevel=None): except: raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', name)) - msignals.display(m18n.n('service_added'), 'success') + logger.success(m18n.n('service_added')) def service_remove(name): @@ -96,7 +100,7 @@ def service_remove(name): except: raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', name)) - msignals.display(m18n.n('service_removed'), 'success') + logger.success(m18n.n('service_removed')) def service_start(names): @@ -111,12 +115,12 @@ def service_start(names): names = [names] for name in names: if _run_service_command('start', name): - msignals.display(m18n.n('service_started', name), 'success') + logger.success(m18n.n('service_started', name)) else: if service_status(name)['status'] != 'running': raise MoulinetteError(errno.EPERM, m18n.n('service_start_failed', name)) - msignals.display(m18n.n('service_already_started', name)) + logger.info(m18n.n('service_already_started', name)) def service_stop(names): @@ -131,12 +135,12 @@ def service_stop(names): names = [names] for name in names: if _run_service_command('stop', name): - msignals.display(m18n.n('service_stopped', name), 'success') + logger.success(m18n.n('service_stopped', name)) else: if service_status(name)['status'] != 'inactive': raise MoulinetteError(errno.EPERM, m18n.n('service_stop_failed', name)) - msignals.display(m18n.n('service_already_stopped', name)) + logger.info(m18n.n('service_already_stopped', name)) def service_enable(names): @@ -151,7 +155,7 @@ def service_enable(names): names = [names] for name in names: if _run_service_command('enable', name): - msignals.display(m18n.n('service_enabled', name), 'success') + logger.success(m18n.n('service_enabled', name)) else: raise MoulinetteError(errno.EPERM, m18n.n('service_enable_failed', name)) @@ -169,7 +173,7 @@ def service_disable(names): names = [names] for name in names: if _run_service_command('disable', name): - msignals.display(m18n.n('service_disabled', name), 'success') + logger.success(m18n.n('service_disabled', name)) else: raise MoulinetteError(errno.EPERM, m18n.n('service_disable_failed', name)) @@ -217,8 +221,7 @@ def service_status(names=[]): shell=True) except subprocess.CalledProcessError as e: if 'usage:' in e.output.lower(): - msignals.display(m18n.n('service_status_failed', name), - 'warning') + logger.warning(m18n.n('service_status_failed', name)) else: result[name]['status'] = 'inactive' else: @@ -288,7 +291,7 @@ def service_regenconf(service=None, force=False): hook_callback('conf_regen', [service], args=[force]) else: hook_callback('conf_regen', args=[force]) - msignals.display(m18n.n('services_configured'), 'success') + logger.success(m18n.n('services_configured')) def _run_service_command(action, service): @@ -317,8 +320,7 @@ def _run_service_command(action, service): ret = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # TODO: Log output? - msignals.display(m18n.n('service_cmd_exec_failed', ' '.join(e.cmd)), - 'warning') + logger.warning(m18n.n('service_cmd_exec_failed', ' '.join(e.cmd))) return False return True @@ -467,12 +469,9 @@ def service_saferemove(service, conf_file, force=False): else: services[service]['conffiles'][conf_file] = previous_hash os.remove(conf_backup_file) - if os.isatty(1) and \ - (len(previous_hash) == 32 or previous_hash[-32:] != current_hash): - msignals.display( - m18n.n('service_configuration_changed', conf_file), - 'warning' - ) + if len(previous_hash) == 32 or previous_hash[-32:] != current_hash: + logger.warning(m18n.n('service_configuration_conflict', + file=conf_file)) _save_services(services) @@ -495,7 +494,7 @@ def service_safecopy(service, new_conf_file, conf_file, force=False): services = _get_services() if not os.path.exists(new_conf_file): - raise MoulinetteError(errno.EIO, m18n.n('no_such_conf_file', new_conf_file)) + raise MoulinetteError(errno.EIO, m18n.n('no_such_conf_file', file=new_conf_file)) with open(new_conf_file, 'r') as f: new_conf = ''.join(f.readlines()).rstrip() @@ -509,8 +508,7 @@ def service_safecopy(service, new_conf_file, conf_file, force=False): ) process.wait() else: - msignals.display(m18n.n('service_add_configuration', conf_file), - 'info') + logger.info(m18n.n('service_add_configuration', file=conf_file)) # Add the service if it does not exist if service not in services.keys(): @@ -539,15 +537,10 @@ def service_safecopy(service, new_conf_file, conf_file, force=False): else: new_hash = previous_hash if (len(previous_hash) == 32 or previous_hash[-32:] != current_hash): - msignals.display( - m18n.n('service_configuration_conflict', conf_file), - 'warning' - ) - print('\n' + conf_file) - for line in diff: - print(line.strip()) - print('') - + logger.warning('{0} {1}'.format( + m18n.n('service_configuration_conflict', file=conf_file), + m18n.n('show_diff', diff=''.join(diff)))) + # Remove the backup file if the configuration has not changed if new_hash == previous_hash: try: diff --git a/lib/yunohost/tools.py b/src/yunohost/tools.py similarity index 98% rename from lib/yunohost/tools.py rename to src/yunohost/tools.py index 867fc7759..daaae9aa1 100644 --- a/lib/yunohost/tools.py +++ b/src/yunohost/tools.py @@ -269,13 +269,13 @@ def tools_postinstall(domain, password, ignore_dyndns=False): # Enable UPnP silently and reload firewall firewall_upnp('enable', no_refresh=True) - firewall_reload() - - # Enable iptables at boot time - os.system('update-rc.d yunohost-firewall defaults') os.system('touch /etc/yunohost/installed') + # Enable and start YunoHost firewall at boot time + os.system('update-rc.d yunohost-firewall enable') + os.system('service yunohost-firewall start') + service_regenconf(force=True) msignals.display(m18n.n('yunohost_configured'), 'success') @@ -373,8 +373,8 @@ def tools_upgrade(auth, ignore_apps=False, ignore_packages=False): # If API call if is_api: - critical_packages = ("moulinette", "moulinette-yunohost", - "yunohost-admin", "yunohost-config-nginx", "ssowat", "python") + critical_packages = ("moulinette", "yunohost", + "yunohost-admin", "ssowat", "python") critical_upgrades = set() for pkg in cache.get_changes(): diff --git a/lib/yunohost/user.py b/src/yunohost/user.py similarity index 100% rename from lib/yunohost/user.py rename to src/yunohost/user.py