Merge branch 'stretch-unstable' into use_getops

This commit is contained in:
Maniack Crudelis 2018-12-29 19:07:25 +01:00 committed by GitHub
commit 4bac17a162
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 2289 additions and 1350 deletions

3
.gitignore vendored
View file

@ -32,3 +32,6 @@ pip-log.txt
# moulinette lib
src/yunohost/locales
# Test
src/yunohost/tests/apps

View file

@ -26,7 +26,7 @@ This repository is the core of YunoHost code.
- [Modules for the XMPP server Metronome](https://github.com/YunoHost/yunohost/tree/stable/lib/metronome/modules).
- [Debian files](https://github.com/YunoHost/yunohost/tree/stable/debian) for package creation.
## How does it works?
## How does it work?
- Python core scripts are accessible through two interfaces thanks to the [moulinette framework](https://github.com/YunoHost/moulinette):
- [CLI](https://en.wikipedia.org/wiki/Command-line_interface) for `yunohost` command.
- [API](https://en.wikipedia.org/wiki/Application_programming_interface) for [web administration module](https://github.com/YunoHost/yunohost-admin) (other modules could be implemented).

View file

@ -13,7 +13,8 @@ DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 6787
# Level for which loggers will log
LOGGERS_LEVEL = 'INFO'
LOGGERS_LEVEL = 'DEBUG'
API_LOGGER_LEVEL = 'INFO'
# Handlers that will be used by loggers
# - file: log to the file LOG_DIR/LOG_FILE
@ -97,8 +98,10 @@ def _init_moulinette(use_websocket=True, debug=False, verbose=False):
# Define loggers level
level = LOGGERS_LEVEL
api_level = API_LOGGER_LEVEL
if debug:
level = 'DEBUG'
api_level = 'DEBUG'
# Custom logging configuration
logging = {
@ -119,6 +122,7 @@ def _init_moulinette(use_websocket=True, debug=False, verbose=False):
},
'handlers': {
'api': {
'level': api_level,
'class': 'moulinette.interfaces.api.APIQueueHandler',
},
'file': {

View file

@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address)
# Fetch SSH fingerprints
i=0
for key in /etc/ssh/ssh_host_*_key.pub ; do
for key in $(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key.pub 2> /dev/null) ; do
output=$(ssh-keygen -l -f $key)
fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)"
i=$(($i + 1))

View file

@ -105,7 +105,7 @@ user:
ask: ask_lastname
required: True
pattern: &pattern_lastname
- !!str ^([^\W\d_]{2,30}[ ,.']{0,3})+$
- !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$
- "pattern_lastname"
-m:
full: --mail
@ -125,6 +125,7 @@ user:
pattern: &pattern_password
- !!str ^.{3,}$
- "pattern_password"
comment: good_practices_about_user_password
-q:
full: --mailbox-quota
help: Mailbox size quota
@ -147,6 +148,7 @@ user:
extra:
pattern: *pattern_username
--purge:
help: Purge user's home and mail directories
action: store_true
### user_update()
@ -556,6 +558,10 @@ app:
full: --no-remove-on-failure
help: Debug option to avoid removing the app on a failed installation
action: store_true
-f:
full: --force
help: Do not ask confirmation if the app is not safe to use (low quality, experimental or 3rd party)
action: store_true
### app_remove() TODO: Write help
remove:
@ -1445,7 +1451,7 @@ tools:
### tools_adminpw()
adminpw:
action_help: Change admin password
action_help: Change password of admin and root users
api: PUT /adminpw
configuration:
authenticate: all
@ -1456,6 +1462,7 @@ tools:
password: ask_new_admin_password
pattern: *pattern_password
required: True
comment: good_practices_about_admin_password
### tools_maindomain()
maindomain:
@ -1493,9 +1500,13 @@ tools:
password: ask_new_admin_password
pattern: *pattern_password
required: True
comment: good_practices_about_admin_password
--ignore-dyndns:
help: Do not subscribe domain to a DynDNS service
action: store_true
--force-password:
help: Use this if you really want to set a weak password
action: store_true
### tools_update()
update:

View file

@ -260,11 +260,7 @@ ynh_add_fpm_config () {
if [ -e "../conf/php-fpm.ini" ]
then
finalphpini="$fpm_config_dir/conf.d/20-$app.ini"
ynh_backup_if_checksum_is_different --file="$finalphpini"
sudo cp ../conf/php-fpm.ini "$finalphpini"
sudo chown root: "$finalphpini"
ynh_store_file_checksum --file="$finalphpini"
echo "Please do not use a separate ini file, merge you directives in the pool file instead." &>2
fi
sudo systemctl reload $fpm_service
}
@ -275,10 +271,10 @@ ynh_add_fpm_config () {
ynh_remove_fpm_config () {
local fpm_config_dir=$(ynh_app_setting_get --app=$app --key=fpm_config_dir)
local fpm_service=$(ynh_app_setting_get --app=$app --key=fpm_service)
# Assume php version 5 if not set
# Assume php version 7 if not set
if [ -z "$fpm_config_dir" ]; then
fpm_config_dir="/etc/php5/fpm"
fpm_service="php5-fpm"
fpm_config_dir="/etc/php/7.0/fpm"
fpm_service="php7.0-fpm"
fi
ynh_secure_remove --file="$fpm_config_dir/pool.d/$app.conf"
ynh_secure_remove --file="$fpm_config_dir/conf.d/20-$app.ini" 2>&1

View file

@ -10,12 +10,14 @@ CAN_BIND=${CAN_BIND:-1}
#
# If DEST is ended by a slash it complete this path with the basename of SRC.
#
# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big]
# usage: ynh_backup --src_path=src_path [--dest_path=dest_path] [--is_big] [--not_mandatory]
# | arg: -s, --src_path - file or directory to bind or symlink or copy. it shouldn't be in
# the backup dir.
# | arg: -d, --dest_path - destination file or directory inside the
# backup dir
# | arg: -b, --is_big - Indicate data are big (mail, video, image ...)
# | arg: -m, --not_mandatory - Indicate that if the file is missing, the backup can ignore it.
# | arg: arg - Deprecated arg
#
# example:
# # Wordpress app context
@ -44,14 +46,16 @@ ynh_backup() {
# TODO find a way to avoid injection by file strange naming !
# Declare an array to define the options of this helper.
declare -Ar args_array=( [s]=src_path= [d]=dest_path= [b]=is_big )
declare -Ar args_array=( [s]=src_path= [d]=dest_path= [b]=is_big [m]=not_mandatory )
local src_path
local dest_path
local is_big
local not_mandatory
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local dest_path="${dest_path:-}"
local is_big="${is_big:-0}"
local not_mandatory="${not_mandatory:-0}"
BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0}
@ -68,7 +72,19 @@ ynh_backup() {
# Be sure the source path is not empty
[[ -e "${src_path}" ]] || {
echo "Source path '${src_path}' does not exist" >&2
if [ "$not_mandatory" == "0" ]
then
# This is a temporary fix for fail2ban config files missing after the migration to stretch.
if echo "${src_path}" | grep --quiet "/etc/fail2ban"
then
touch "${src_path}"
echo "The missing file will be replaced by a dummy one for the backup !!!" >&2
else
return 1
fi
else
return 0
fi
}
# Transform the source path as an absolute path
@ -175,12 +191,13 @@ with open(sys.argv[1], 'r') as backup_file:
# Use the registered path in backup_list by ynh_backup to restore the file at
# the good place.
#
# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path]
# usage: ynh_restore_file --origin_path=origin_path [--dest_path=dest_path] [--not_mandatory]
# | arg: -o, --origin_path - Path where was located the file or the directory before
# to be backuped or relative path to $YNH_CWD where it is located in the backup archive
# | arg: -d, --dest_path - Path where restore the file or the dir, if unspecified,
# the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in
# the archive, the destination will be searched into backup.csv
# | arg: -m, --not_mandatory - Indicate that if the file is missing, the restore process can ignore it.
#
# If DEST_PATH already exists and is lighter than 500 Mo, a backup will be made in
# /home/yunohost.conf/backup/. Otherwise, the existing file is removed.
@ -197,20 +214,27 @@ with open(sys.argv[1], 'r') as backup_file:
#
ynh_restore_file () {
# Declare an array to define the options of this helper.
declare -Ar args_array=( [o]=origin_path= [d]=dest_path= )
declare -Ar args_array=( [o]=origin_path= [d]=dest_path= [m]=not_mandatory )
local origin_path
local archive_path
local dest_path
local not_mandatory
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local origin_path="/${origin_path#/}"
local archive_path="$YNH_CWD${origin_path}"
# Default value for dest_path = /$origin_path
local dest_path="${dest_path:-$origin_path}"
local not_mandatory="${not_mandatory:-0}"
# If archive_path doesn't exist, search for a corresponding path in CSV
if [ ! -d "$archive_path" ] && [ ! -f "$archive_path" ] && [ ! -L "$archive_path" ]; then
if [ "$not_mandatory" == "0" ]
then
archive_path="$YNH_BACKUP_DIR/$(_get_archive_path \"$origin_path\")"
else
return 0
fi
fi
# Move the old directory if it already exists

View file

@ -129,6 +129,8 @@ ynh_handle_getopts_args () {
shift_value=$(( shift_value - 1 ))
fi
# Declare the content of option_var as a variable.
eval ${option_var}=""
# Then read the array value per value
for i in `seq 0 $(( ${#all_args[@]} - 1 ))`
do
@ -140,8 +142,6 @@ ynh_handle_getopts_args () {
break
fi
else
# Declare the content of option_var as a variable.
eval ${option_var}=""
# Else, add this value to this option
# Each value will be separated by ';'
if [ -n "${!option_var}" ]

View file

@ -1,3 +1,26 @@
# Check if apt is free to use, or wait, until timeout.
#
# [internal]
#
# usage: ynh_wait_dpkg_free
ynh_wait_dpkg_free() {
local try
# With seq 1 17, timeout will be almost 30 minutes
for try in `seq 1 17`
do
# Check if /var/lib/dpkg/lock is used by another process
if sudo lsof /var/lib/dpkg/lock > /dev/null
then
echo "apt is already in use..."
# Sleep an exponential time at each round
sleep $(( try * try ))
else
return 0
fi
done
echo "apt still used, but timeout reached !"
}
# Check either a package is installed or not
#
# example: ynh_package_is_installed --package=yunohost && echo "ok"
@ -11,6 +34,7 @@ ynh_package_is_installed() {
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
ynh_wait_dpkg_free
dpkg-query -W -f '${Status}' "$package" 2>/dev/null \
| grep -c "ok installed" &>/dev/null
}
@ -42,6 +66,7 @@ ynh_package_version() {
#
# usage: ynh_apt update
ynh_apt() {
ynh_wait_dpkg_free
DEBIAN_FRONTEND=noninteractive sudo apt-get -y $@
}
@ -117,6 +142,7 @@ ynh_package_install_from_equivs () {
# Create a fake deb package with equivs-build and the given control file
# Install the fake package without its dependencies with dpkg
# Install missing dependencies with ynh_package_install
ynh_wait_dpkg_free
(cp "$controlfile" "${TMPDIR}/control" && cd "$TMPDIR" \
&& equivs-build ./control 1>/dev/null \
&& sudo dpkg --force-depends \

View file

@ -14,7 +14,7 @@ ynh_die() {
# Display a message in the 'INFO' logging category
#
# usage: ynh_info --message="Some message"
# usage: ynh_print_info --message="Some message"
ynh_print_info() {
# Declare an array to define the options of this helper.
declare -Ar args_array=( [m]=message= )

View file

@ -12,7 +12,7 @@ ynh_string_random() {
ynh_handle_getopts_args "$@"
length=${length:-24}
dd if=/dev/urandom bs=1 count=200 2> /dev/null \
dd if=/dev/urandom bs=1 count=1000 2> /dev/null \
| tr -c -d 'A-Za-z0-9' \
| sed -n 's/\(.\{'"$length"'\}\).*/\1/p'
}

View file

@ -259,6 +259,9 @@ ynh_local_curl () {
POST_data="--data ${POST_data::-1}"
fi
# Wait untils nginx has fully reloaded (avoid curl fail with http2)
sleep 2
# Curl the URL
curl --silent --show-error -kL -H "Host: $domain" --resolve $domain:443:127.0.0.1 $POST_data "$full_page_url"
}
@ -274,6 +277,7 @@ ynh_local_curl () {
ynh_render_template() {
local template_path=$1
local output_path=$2
mkdir -p "$(dirname $output_path)"
# Taken from https://stackoverflow.com/a/35009576
python2.7 -c 'import os, sys, jinja2; sys.stdout.write(
jinja2.Template(sys.stdin.read()

View file

@ -24,46 +24,57 @@ do_init_regen() {
# initialize some files
[[ -f "${ssl_dir}/serial" ]] \
|| echo "00" > "${ssl_dir}/serial"
|| openssl rand -hex 19 > "${ssl_dir}/serial"
[[ -f "${ssl_dir}/index.txt" ]] \
|| touch "${ssl_dir}/index.txt"
openssl_conf="/usr/share/yunohost/templates/ssl/openssl.cnf"
ynh_ca="/etc/yunohost/certs/yunohost.org/ca.pem"
ynh_crt="/etc/yunohost/certs/yunohost.org/crt.pem"
ynh_key="/etc/yunohost/certs/yunohost.org/key.pem"
# create default certificates
if [[ ! -f /etc/yunohost/certs/yunohost.org/ca.pem ]]; then
if [[ ! -f "$ynh_ca" ]]; then
echo -e "\n# Creating the CA key (?)\n" >>$LOGFILE
openssl req -x509 -new -config "$openssl_conf" \
-days 3650 -out "${ssl_dir}/ca/cacert.pem" \
-keyout "${ssl_dir}/ca/cakey.pem" -nodes -batch >>$LOGFILE 2>&1
cp "${ssl_dir}/ca/cacert.pem" \
/etc/yunohost/certs/yunohost.org/ca.pem
ln -sf /etc/yunohost/certs/yunohost.org/ca.pem \
/etc/ssl/certs/ca-yunohost_crt.pem
openssl req -x509 \
-new \
-config "$openssl_conf" \
-days 3650 \
-out "${ssl_dir}/ca/cacert.pem" \
-keyout "${ssl_dir}/ca/cakey.pem" \
-nodes -batch >>$LOGFILE 2>&1
cp "${ssl_dir}/ca/cacert.pem" "$ynh_ca"
ln -sf "$ynh_ca" /etc/ssl/certs/ca-yunohost_crt.pem
update-ca-certificates
fi
if [[ ! -f /etc/yunohost/certs/yunohost.org/crt.pem ]]; then
if [[ ! -f "$ynh_crt" ]]; then
echo -e "\n# Creating initial key and certificate (?)\n" >>$LOGFILE
openssl req -new -config "$openssl_conf" \
-days 730 -out "${ssl_dir}/certs/yunohost_csr.pem" \
-keyout "${ssl_dir}/certs/yunohost_key.pem" -nodes -batch >>$LOGFILE 2>&1
openssl ca -config "$openssl_conf" \
-days 730 -in "${ssl_dir}/certs/yunohost_csr.pem" \
-out "${ssl_dir}/certs/yunohost_crt.pem" -batch >>$LOGFILE 2>&1
openssl req -new \
-config "$openssl_conf" \
-days 730 \
-out "${ssl_dir}/certs/yunohost_csr.pem" \
-keyout "${ssl_dir}/certs/yunohost_key.pem" \
-nodes -batch >>$LOGFILE 2>&1
openssl ca \
-config "$openssl_conf" \
-days 730 \
-in "${ssl_dir}/certs/yunohost_csr.pem" \
-out "${ssl_dir}/certs/yunohost_crt.pem" \
-batch >>$LOGFILE 2>&1
last_cert=$(ls $ssl_dir/newcerts/*.pem | sort -V | tail -n 1)
chmod 640 "${ssl_dir}/certs/yunohost_key.pem"
chmod 640 "$last_cert"
cp "${ssl_dir}/certs/yunohost_key.pem" \
/etc/yunohost/certs/yunohost.org/key.pem
cp "$last_cert" \
/etc/yunohost/certs/yunohost.org/crt.pem
ln -sf /etc/yunohost/certs/yunohost.org/crt.pem \
/etc/ssl/certs/yunohost_crt.pem
ln -sf /etc/yunohost/certs/yunohost.org/key.pem \
/etc/ssl/private/yunohost_key.pem
cp "${ssl_dir}/certs/yunohost_key.pem" "$ynh_key"
cp "$last_cert" "$ynh_crt"
ln -sf "$ynh_crt" /etc/ssl/certs/yunohost_crt.pem
ln -sf "$ynh_key" /etc/ssl/private/yunohost_key.pem
fi
}

View file

@ -2,28 +2,53 @@
set -e
. /usr/share/yunohost/helpers.d/utils
do_pre_regen() {
pending_dir=$1
# If the (legacy) 'from_script' flag is here,
# we won't touch anything in the ssh config.
[[ ! -f /etc/yunohost/from_script ]] || return 0
cd /usr/share/yunohost/templates/ssh
# only overwrite SSH configuration on an ISO installation
if [[ ! -f /etc/yunohost/from_script ]]; then
# do not listen to IPv6 if unavailable
[[ -f /proc/net/if_inet6 ]] \
|| sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config
[[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false
install -D -m 644 sshd_config "${pending_dir}/etc/ssh/sshd_config"
# Support legacy setting (this setting might be disabled by a user during a migration)
ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null)
if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then
ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)"
fi
ssh_keys=$(ls /etc/ssh/ssh_host_{ed25519,rsa,ecdsa}_key 2>/dev/null)
# Support legacy setting (this setting might be disabled by a user during a migration)
if [[ "$(yunohost settings get 'service.ssh.allow_deprecated_dsa_hostkey')" == "True" ]]; then
ssh_keys="$ssh_keys $(ls /etc/ssh/ssh_host_dsa_key 2>/dev/null)"
fi
export ssh_keys
export ipv6_enabled
ynh_render_template "sshd_config" "${pending_dir}/etc/ssh/sshd_config"
}
do_post_regen() {
regen_conf_files=$1
if [[ ! -f /etc/yunohost/from_script ]]; then
[[ -z "$regen_conf_files" ]] \
|| sudo service ssh restart
fi
# If the (legacy) 'from_script' flag is here,
# we won't touch anything in the ssh config.
[[ ! -f /etc/yunohost/from_script ]] || return 0
# If no file changed, there's nothing to do
[[ -n "$regen_conf_files" ]] || return 0
# Enforce permissions for /etc/ssh/sshd_config
chown root:root "/etc/ssh/sshd_config"
chmod 644 "/etc/ssh/sshd_config"
systemctl restart ssh
}
FORCE=${2:-0}

View file

@ -2,6 +2,8 @@
set -e
. /usr/share/yunohost/helpers.d/utils
do_init_regen() {
if [[ $EUID -ne 0 ]]; then
echo "You must be root to run this script" 1>&2
@ -42,18 +44,18 @@ do_pre_regen() {
mkdir -p "$mail_autoconfig_dir"
# NGINX server configuration
cat server.tpl.conf \
| sed "s/{{ domain }}/${domain}/g" \
> "${nginx_conf_dir}/${domain}.conf"
cat autoconfig.tpl.xml \
| sed "s/{{ domain }}/${domain}/g" \
> "${mail_autoconfig_dir}/config-v1.1.xml"
export domain
export domain_cert_ca=$(yunohost domain cert-status $domain --json \
| jq ".certificates.\"$domain\".CA_type" \
| tr -d '"')
ynh_render_template "server.tpl.conf" "${nginx_conf_dir}/${domain}.conf"
ynh_render_template "autoconfig.tpl.xml" "${mail_autoconfig_dir}/config-v1.1.xml"
[[ $main_domain != $domain ]] \
&& touch "${domain_conf_dir}/yunohost_local.conf" \
|| cp yunohost_local.conf "${domain_conf_dir}/yunohost_local.conf"
done
# remove old domain conf files

Binary file not shown.

View file

@ -9,16 +9,64 @@
-- A table is a list of values, except each value has a name. An
-- example would be:
--
-- ssl = { key = "keyfile.key", certificate = "certificate.crt" }
-- ssl = { key = "keyfile.key", certificate = "certificate.cert" }
--
-- Tip: You can check that the syntax of this file is correct when you have finished
-- by running: luac -p metronome.cfg.lua
-- If there are any errors, it will let you know what and where they are, otherwise it
-- will keep quiet.
---------- Server-wide settings ----------
-- Settings in this section apply to the whole server and are the default settings
-- for any virtual hosts
-- Global settings go in this section
-- This is the list of modules Metronome will load on startup.
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended.
"saslauth"; -- Authentication for clients. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"disco"; -- Service discovery
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
"pep"; -- Allows setting of mood, tune, etc.
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
"bidi"; -- Enables Bidirectional Server-to-Server Streams.
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"stream_management"; -- Allows clients and servers to use Stream Management
"stanza_optimizations"; -- Allows clients to use Client State Indication and SIFT
"message_carbons"; -- Allows clients to enable carbon copies of messages
"mam"; -- Enable server-side message archives using Message Archive Management
"push"; -- Enable Push Notifications via PubSub using XEP-0357
"lastactivity"; -- Enables clients to know the last presence status of an user
"adhoc_cm"; -- Allow to set client certificates to login through SASL External via adhoc
"admin_adhoc"; -- administration adhoc commands
"bookmarks"; -- XEP-0048 Bookmarks synchronization between PEP and Private Storage
"sec_labels"; -- Allows to use a simplified version XEP-0258 Security Labels and related ACDFs.
"privacy"; -- Add privacy lists and simple blocking command support
-- Other specific functionality
--"admin_telnet"; -- administration console, telnet to port 5582
--"admin_web"; -- administration web interface
"bosh"; -- Enable support for BOSH clients, aka "XMPP over Bidirectional Streams over Synchronous HTTP"
--"compression"; -- Allow clients to enable Stream Compression
--"spim_block"; -- Require authorization via OOB form for messages from non-contacts and block unsollicited messages
--"gate_guard"; -- Enable config-based blacklisting and hit-based auto-banning features
--"incidents_handling"; -- Enable Incidents Handling support (can be administered via adhoc commands)
--"server_presence"; -- Enables Server Buddies extension support
--"service_directory"; -- Enables Service Directories extension support
--"public_service"; -- Enables Server vCard support for public services in directories and advertises in features
--"register_api"; -- Provides secure API for both Out-Of-Band and In-Band registration for E-Mail verification
"websocket"; -- Enable support for WebSocket clients, aka "XMPP over WebSockets"
};
-- Server PID
pidfile = "/var/run/metronome/metronome.pid"
@ -33,109 +81,25 @@ http_interfaces = { "127.0.0.1", "::1" }
-- Enable IPv6
use_ipv6 = true
-- This is the list of modules Metronome will load on startup.
-- It looks for mod_modulename.lua in the plugins folder, so make sure that exists too.
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
--"discoitems"; -- Service discovery items
--"extdisco"; -- External Service Discovery
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
"privacy"; -- Support privacy lists
-- These are commented by default as they have a performance impact
--"compression"; -- Stream compression (Debian: requires lua-zlib module to work)
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"message_carbons"; -- Allow clients to keep in sync with messages send on other resources
"register"; -- Allow users to register on this server using a client and change passwords
"adhoc"; -- Support for "ad-hoc commands" that can be executed with an XMPP client
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"websockets"; -- Enable WebSocket clients
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
-- "bidi"; -- Bidirectional Streams for S2S connections
-- "stream_management"; -- Stream Management support
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
"mam"; -- Nice archive management
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
"offline"; -- Store offline messages
"c2s"; -- Handle client connections
"s2s"; -- Handle server-to-server connections
-- Debian: do not remove this module, or you lose syslog
-- support
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
};
-- Discovery items
disco_items = {
{ "muc.{{ main_domain }}" },
{ "pubsub.{{ main_domain }}" },
{ "upload.{{ main_domain }}" },
{ "vjud.{{ main_domain }}" }
};
-- BOSH configuration (mod_bosh)
bosh_max_inactivity = 30
consider_bosh_secure = true
cross_domain_bosh = true
-- WebSocket configuration (mod_websocket)
consider_websocket_secure = true
cross_domain_websocket = true
-- Disable account creation by default, for security
allow_registration = false
-- SSL/TLS configuration
ssl = {
options = {
"no_sslv2",
"no_sslv3",
"no_ticket",
"no_compression",
"cipher_server_preference"
};
}
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
c2s_require_encryption = true
-- Force servers to use encrypted connections? This option will
-- prevent servers from connecting unless they are using encryption.
s2s_require_encryption = true
-- Allow servers to use an unauthenticated encryption channel
s2s_allow_encryption = true
allow_unencrypted_plain_auth = false;
s2s_secure = true
s2s_secure_auth = false
--anonymous_login = false
-- Use LDAP storage backend for all stores
storage = "ldap"
@ -147,7 +111,6 @@ log = {
-- "*console"; -- Log to the console, useful for debugging with daemonize=false
}
------ Components ------
-- You can specify components to add hosts that provide special services,
-- like multi-user conferences, and transports.
@ -163,23 +126,28 @@ Component "muc.{{ main_domain }}" "muc"
modules_enabled = {
"muc_limits";
"muc_log";
"muc_log_mam";
"muc_log_http";
"muc_vcard";
}
muc_event_rate = 0.5
muc_burst_factor = 10
muc_log_http_config = {
url_base = "logs";
theme = "metronome";
}
---Set up a PubSub server
Component "pubsub.{{ main_domain }}" "pubsub"
name = "{{ main_domain }} Publish/Subscribe"
unrestricted_node_creation = true -- Anyone can create a PubSub node (from any server)
---Set up a HTTP Upload service
Component "upload.{{ main_domain }}" "http_upload"
name = "{{ main_domain }} Sharing Service"
http_file_size_limit = 6*1024*1024
http_file_quota = 60*1024*1024
---Set up a VJUD service
Component "vjud.{{ main_domain }}" "vjud"
ud_disco_name = "{{ main_domain }} User Directory"
@ -190,4 +158,3 @@ Component "vjud.{{ main_domain }}" "vjud"
-- Settings under each VirtualHost entry apply *only* to that host.
Include "conf.d/*.cfg.lua"

View file

@ -73,6 +73,6 @@ server {
access_by_lua_file /usr/share/ssowat/access.lua;
}
include conf.d/yunohost_admin.conf.inc;
include conf.d/yunohost_api.conf.inc;
include /etc/nginx/conf.d/yunohost_admin.conf.inc;
include /etc/nginx/conf.d/yunohost_api.conf.inc;
}

View file

@ -4,6 +4,7 @@ location /yunohost/api/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
# Custom 502 error page
error_page 502 /yunohost/api/error/502;

View file

@ -5,13 +5,13 @@ server {
access_by_lua_file /usr/share/ssowat/access.lua;
include conf.d/{{ domain }}.d/*.conf;
include /etc/nginx/conf.d/{{ domain }}.d/*.conf;
location /yunohost/admin {
return 301 https://$http_host$request_uri;
}
location /.well-known/autoconfig/mail {
location /.well-known/autoconfig/mail/ {
alias /var/www/.well-known/{{ domain }}/autoconfig/mail;
}
@ -20,11 +20,8 @@ server {
}
server {
# Disabling http2 for now as it's causing weird issues with curl
#listen 443 ssl http2;
#listen [::]:443 ssl http2;
listen 443 ssl;
listen [::]:443 ssl;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {{ domain }};
ssl_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
@ -33,12 +30,7 @@ server {
ssl_session_cache shared:SSL:50m;
# As suggested by Mozilla : https://wiki.mozilla.org/Security/Server_Side_TLS and https://en.wikipedia.org/wiki/Curve25519
# (this doesn't work on jessie though ...?)
# ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
# As suggested by https://cipherli.st/
ssl_ecdh_curve secp384r1;
ssl_ecdh_curve secp521r1:secp384r1:prime256v1;
ssl_prefer_server_ciphers on;
# Ciphers with intermediate compatibility
@ -59,21 +51,30 @@ server {
# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
# https://observatory.mozilla.org/
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header Content-Security-Policy "upgrade-insecure-requests";
add_header Content-Security-Policy-Report-Only "default-src https: data: 'unsafe-inline' 'unsafe-eval'";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header X-Frame-Options "SAMEORIGIN";
more_set_headers "Strict-Transport-Security : max-age=63072000; includeSubDomains; preload";
more_set_headers "Content-Security-Policy : upgrade-insecure-requests";
more_set_headers "Content-Security-Policy-Report-Only : default-src https: data: 'unsafe-inline' 'unsafe-eval'";
more_set_headers "X-Content-Type-Options : nosniff";
more_set_headers "X-XSS-Protection : 1; mode=block";
more_set_headers "X-Download-Options : noopen";
more_set_headers "X-Permitted-Cross-Domain-Policies : none";
more_set_headers "X-Frame-Options : SAMEORIGIN";
{% if domain_cert_ca == "Let's Encrypt" %}
# OCSP settings
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/yunohost/certs/{{ domain }}/crt.pem;
resolver 127.0.0.1 127.0.1.1 valid=300s;
resolver_timeout 5s;
{% endif %}
access_by_lua_file /usr/share/ssowat/access.lua;
include conf.d/{{ domain }}.d/*.conf;
include /etc/nginx/conf.d/{{ domain }}.d/*.conf;
include conf.d/yunohost_admin.conf.inc;
include conf.d/yunohost_api.conf.inc;
include /etc/nginx/conf.d/yunohost_admin.conf.inc;
include /etc/nginx/conf.d/yunohost_api.conf.inc;
access_log /var/log/nginx/{{ domain }}-access.log;
error_log /var/log/nginx/{{ domain }}-error.log;

View file

@ -5,8 +5,8 @@
# See ldap.conf(5) for details
# This file should be world readable but not world writable.
#BASE dc=example,dc=com
#URI ldap://ldap.example.com ldap://ldap-master.example.com:666
BASE dc=yunohost,dc=org
URI ldap://localhost:389
#SIZELIMIT 12
#TIMELIMIT 15

View file

@ -1,96 +1,78 @@
# Package generated configuration file
# See the sshd_config(5) manpage for details
# This configuration has been automatically generated
# by YunoHost
# What ports, IPs and protocols we listen for
Port 22
# Use these options to restrict which interfaces/protocols sshd will bind to
ListenAddress ::
ListenAddress 0.0.0.0
Protocol 2
# HostKeys for protocol version 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_dsa_key
#Privilege Separation is turned on for security
UsePrivilegeSeparation yes
Port 22
# Lifetime and size of ephemeral version 1 server key
KeyRegenerationInterval 3600
ServerKeyBits 768
{% if ipv6_enabled == "true" %}ListenAddress ::{% endif %}
ListenAddress 0.0.0.0
# Logging
{% for key in ssh_keys.split() %}
HostKey {{ key }}{% endfor %}
# ##############################################
# Stuff recommended by Mozilla "modern" compat'
# https://infosec.mozilla.org/guidelines/openssh
# ##############################################
# Keys, ciphers and MACS
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com
# Use kernel sandbox mechanisms where possible in unprivileged processes
UsePrivilegeSeparation sandbox
# LogLevel VERBOSE logs user's key fingerprint on login.
# Needed to have a clear audit track of which key was using to log in.
SyslogFacility AUTH
LogLevel INFO
LogLevel VERBOSE
# #######################
# Authentication settings
# #######################
# Comment from Mozilla about the motivation behind disabling root login
#
# Root login is not allowed for auditing reasons. This is because it's difficult to track which process belongs to which root user:
#
# On Linux, user sessions are tracking using a kernel-side session id, however, this session id is not recorded by OpenSSH.
# Additionally, only tools such as systemd and auditd record the process session id.
# On other OSes, the user session id is not necessarily recorded at all kernel-side.
# Using regular users in combination with /bin/su or /usr/bin/sudo ensure a clear audit track.
# Authentication:
LoginGraceTime 120
PermitRootLogin no
StrictModes yes
RSAAuthentication yes
PubkeyAuthentication yes
#AuthorizedKeysFile %h/.ssh/authorized_keys
# Don't read the user's ~/.rhosts and ~/.shosts files
IgnoreRhosts yes
# For this to work you will also need host keys in /etc/ssh_known_hosts
RhostsRSAAuthentication no
# similar for protocol version 2
HostbasedAuthentication no
# Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication
#IgnoreUserKnownHosts yes
# To enable empty passwords, change to yes (NOT RECOMMENDED)
PermitEmptyPasswords no
# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
ChallengeResponseAuthentication no
# Change to no to disable tunnelled clear text passwords
#PasswordAuthentication yes
# Kerberos options
#KerberosAuthentication no
#KerberosGetAFSToken no
#KerberosOrLocalPasswd yes
#KerberosTicketCleanup yes
# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes
X11Forwarding yes
X11DisplayOffset 10
PrintMotd no
PrintLastLog yes
TCPKeepAlive yes
#UseLogin no
# keep ssh sessions fresh
ClientAliveInterval 60
#MaxStartups 10:30:60
Banner /etc/issue.net
# Allow client to pass locale environment variables
AcceptEnv LANG LC_*
Subsystem sftp internal-sftp
# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
UsePAM yes
# Change to no to disable tunnelled clear text passwords
# (i.e. everybody will need to authenticate using ssh keys)
#PasswordAuthentication yes
# Post-login stuff
Banner /etc/issue.net
PrintMotd no
PrintLastLog yes
ClientAliveInterval 60
AcceptEnv LANG LC_*
# SFTP stuff
Subsystem sftp internal-sftp
Match User sftpusers
ForceCommand internal-sftp
ChrootDirectory /home/%u
AllowTcpForwarding no
GatewayPorts no
X11Forwarding no
# root login is allowed on local networks
# It's meant to be a backup solution in case LDAP is down and
# user admin can't be used...
# If the server is a VPS, it's expected that the owner of the
# server has access to a web console through which to log in.
Match Address 192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,169.254.0.0/16,fe80::/10,fd00::/8
PermitRootLogin yes

90
debian/changelog vendored
View file

@ -1,3 +1,93 @@
yunohost (3.4.0) testing; urgency=low
* Misc fixes (#601, #600, #593)
* [fix] DEBUG-level messages not appearing in actions performed via the API (#603)
* [enh] Also remove /var/mail/<username> directory on user delete (with --purge option) (#602)
* [enh] Ask confirmation before installing low-quality, experimental or third party apps (#598)
* [fix] Repair tests (#595)
* [enh] Clean + harden sshd config using Mozilla recommendation (#590
* [fix] Add libpam-ldapd as dependency to be able to login through SSH with LDAP? (#587)
* [enh] Add post_cert_update hook each time certificate is updated (#586)
* [enh] Enable HTTP2 (#580)
* [enh] Update ECDH curves recommended by Mozilla, now that we are on stretch (#579)
* [enh] Allow to not fail on backup and restore for non-mandatory files (#576)
* [enh] Simplify error management (#574)
* [enh] Use more_set_headers in nginx config + fixes for path traversal issues (#564)
* [enh] Display human readable date and clarify timezone handling (#552)
* [fix] Do not use separate ini file for php pools anymore (#548)
* [enh] Improve UPnP support (#542)
* [fix] Standardize sshd configuration (#518)
* [fix] DKIM keys for new domains werent generated (0445aed)
* [i18n] Improve translations for Arabic, Italian and Spanish
Thanks to all contributors (Aleks, A. Pierré, ButterflyOfFire, Bram, irina11y, Josué, Maniack Crudelis, Sylkevicious, T. Hill, chateau, frju365, gdayon, liberodark, ljf, nqb, wilPoly) ! <3
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 20 Dec 2018 22:13:00 +0000
yunohost (3.3.3) stable; urgency=low
* [fix] ynh_wait_dpkg_free displaying a warning despite everything being okay (#593)
* [fix] Quotes for recommended CAA DNS record (#596)
* [fix] Manual migration and disclaimer behaviors (#594)
* [fix] Explicit root password change each time admin password is changed
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 09 Dec 2018 20:58:00 +0000
yunohost (3.3.2) stable; urgency=low
* [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588)
* [fix] Broken new settings and options to control passwords checks / constrains (#589)
* [fix] Log dyndns update only if we really update something (#591)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 02 Dec 2018 17:23:00 +0000
yunohost (3.3.1) stable; urgency=low
* [fix] Wait for dpkg lock to be free in apt helpers (#571)
* [fix] app_removeaccess call set.add (#573)
* [fix] Fix app_addaccess behaviour when 'allowed_users' is initially empty (#575)
* [fix] Typo in user_update when update password (#577)
* [fix] Do not fail on missing fail2ban config during the backup (#558)
* [fix] Generate a random serial for local certification auth (followup of #557)
* [i18n] Update Italian, Occitan, French translations
Thanks to all contributors (Maniack, airwoodix, Aleks, ljf, silkevicious, Quent-in, Jibec) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 23 Nov 2018 15:58:00 +0000
yunohost (3.3.0) testing; urgency=low
Highlights
==========
* [enh] Synchronize root password with admin password (#527)
* [enh] Check for weak passwords whenever a password is defined (#196)
* [fix] 'dyndns update' now checks the upstream DNS record (#519)
* [fix] Update Metronome configuration file to v3.11 standard (#559)
* [fix] Some php conf files wre not properly removed when an app was uninstalled (#566)
* [i18n] Improve Catalan, French, Occitan, Portuguese, Arabic, Italian translations
Misc
====
* [enh] Add OCSP Stapling to nginx configuration if using Lets Encrypt (#533)
* [enh] Add CAA record in recommended DNS conf (#528)
* [helpers] Add `ynh_delete_file_checksum` (#524)
* [helpers] When using `ynh_setup_source`, silent unecessary messages (#545)
* [helpers] Use more blocks for dd in `ynh_string_random` (#569)
* [fix] Potential key error when retrieving install_time (#551)
* [fix] Allow `-` in user last names (#565)
* [fix] Fix possible HTTP2 issue with curl (#547)
* [fix] Fix BASE/URI in ldap conf (#554)
* [fix] Use random serial number for CA (prevent browser from complaining about some selfsigned certs) (#557)
* [enh] Pass Host header to YunoHost API (#560)
* [enh] Sort backup list according to their date (#562)
* [fix] Improve UX when admin tries to allocate reserved email alias (#553)
Thanks to all contributors (ljf, irinia11y, Maniack, xaloc33, Bram, flashemade, Maranda, Josue, frju365, Aleks, randomstuff, jershon, Genma, Quent-in, MyNameIsTroll, ButterflyOfFire, Jibec, silkevicious) ! <3
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 08 Nov 2018 17:09:00 +0000
yunohost (3.2.2) stable; urgency=low
* [hotfix] mod_auth_ldap: reflect SASL API changes in latest Metronome (#546)

4
debian/control vendored
View file

@ -17,7 +17,7 @@ Depends: ${python:Depends}, ${misc:Depends}
, dnsutils, bind9utils, unzip, git, curl, cron, wget, jq
, ca-certificates, netcat-openbsd, iproute
, mariadb-server, php-mysql | php-mysqlnd
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, unscd, libpam-ldapd
, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils, postsrsd
, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved
, dovecot-antispam, fail2ban
@ -26,7 +26,7 @@ Depends: ${python:Depends}, ${misc:Depends}
, metronome
, rspamd (>= 1.6.0), redis-server, opendkim-tools
, haveged, fake-hwclock
, equivs
, equivs, lsof
Recommends: yunohost-admin
, openssh-server, ntp, inetutils-ping | iputils-ping
, bash-completion, rsyslog, etckeeper

1
debian/install vendored
View file

@ -4,6 +4,7 @@ data/bash-completion.d/yunohost /etc/bash_completion.d/
data/actionsmap/* /usr/share/moulinette/actionsmap/
data/hooks/* /usr/share/yunohost/hooks/
data/other/yunoprompt.service /etc/systemd/system/
data/other/password/* /usr/share/yunohost/other/password/
data/other/* /usr/share/yunohost/yunohost-config/moulinette/
data/templates/* /usr/share/yunohost/templates/
data/helpers /usr/share/yunohost/

View file

@ -11,7 +11,7 @@ def render(data):
from ansi2html.style import get_styles
conv = Ansi2HTMLConverter()
shell_css = "\n".join(map(str, get_styles(conv.dark_bg, conv.scheme)))
shell_css = "\n".join(map(str, get_styles(conv.dark_bg)))
def shell_to_html(shell):
return conv.convert(shell, False)
@ -28,6 +28,7 @@ class Parser():
def __init__(self, filename):
self.filename = filename
self.file = open(filename, "r").readlines()
self.blocks = None
@ -43,6 +44,9 @@ class Parser():
for i, line in enumerate(self.file):
if line.startswith("#!/bin/bash"):
continue
line = line.rstrip().replace("\t", " ")
if current_reading == "void":
@ -64,7 +68,7 @@ class Parser():
else:
# We're getting out of a comment bloc, we should find
# the name of the function
assert len(line.split()) >= 1
assert len(line.split()) >= 1, "Malformed line %s in %s" % (i, self.filename)
current_block["line"] = i
current_block["name"] = line.split()[0].strip("(){")
# Then we expect to read the function
@ -143,11 +147,11 @@ class Parser():
b["usage"] = b["usage"].strip()
def is_global_comment(line):
return line.startswith('#')
def malformed_error(line_number):
import pdb; pdb.set_trace()
return "Malformed file line {} ?".format(line_number)
def main():

View file

@ -8,9 +8,9 @@
"app_already_up_to_date": "{app:s} تم تحديثه مِن قَبل",
"app_argument_choice_invalid": "",
"app_argument_invalid": "",
"app_argument_required": "",
"app_change_no_change_url_script": "",
"app_change_url_failed_nginx_reload": "",
"app_argument_required": "المُعامِل '{name:s}' مطلوب",
"app_change_no_change_url_script": "إنّ التطبيق {app_name:s} لا يدعم تغيير الرابط، مِن الممكن أنه يتوجب عليكم تحدثيه.",
"app_change_url_failed_nginx_reload": "فشلت عملية إعادة تشغيل nginx. ها هي نتيجة الأمر 'nginx -t':\n{nginx_errors:s}",
"app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.",
"app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.",
"app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}",
@ -152,11 +152,11 @@
"domain_dyndns_dynette_is_unreachable": "Unable to reach YunoHost dynette, either your YunoHost is not correctly connected to the internet or the dynette server is down. Error: {error}",
"domain_dyndns_invalid": "Invalid domain to use with DynDNS",
"domain_dyndns_root_unknown": "Unknown DynDNS root domain",
"domain_exists": "Domain already exists",
"domain_exists": "اسم النطاق موجود مِن قبل",
"domain_hostname_failed": "Failed to set new hostname",
"domain_uninstall_app_first": "One or more apps are installed on this domain. Please uninstall them before proceeding to domain removal",
"domain_unknown": "النطاق مجهول",
"domain_zone_exists": "DNS zone file already exists",
"domain_zone_exists": "ملف منطقة أسماء النطاقات موجود مِن قبل",
"domain_zone_not_found": "DNS zone file not found for domain {:s}",
"domains_available": "النطاقات المتوفرة :",
"done": "تم",
@ -166,9 +166,9 @@
"dyndns_cron_remove_failed": "Unable to remove the DynDNS cron job",
"dyndns_cron_removed": "The DynDNS cron job has been removed",
"dyndns_ip_update_failed": "Unable to update IP address on DynDNS",
"dyndns_ip_updated": "Your IP address has been updated on DynDNS",
"dyndns_key_generating": "DNS key is being generated, it may take a while...",
"dyndns_key_not_found": "DNS key not found for the domain",
"dyndns_ip_updated": "لقد تم تحديث عنوان الإيبي الخاص بك على نظام أسماء النطاقات الديناميكي",
"dyndns_key_generating": "عملية توليد مفتاح نظام أسماء النطاقات جارية. يمكن للعملية أن تستغرق بعضا من الوقت…",
"dyndns_key_not_found": "لم يتم العثور على مفتاح DNS الخاص باسم النطاق هذا",
"dyndns_no_domain_registered": "No domain has been registered with DynDNS",
"dyndns_registered": "The DynDNS domain has been registered",
"dyndns_registration_failed": "Unable to register DynDNS domain: {error:s}",
@ -380,5 +380,38 @@
"service_description_nginx": "يقوم بتوفير النفاذ و السماح بالوصول إلى كافة مواقع الويب المستضافة على خادومك",
"service_description_php5-fpm": "يقوم بتشغيل تطبيقات الـ PHP مع خادوم الويب nginx",
"service_description_postfix": "يقوم بإرسال و تلقي الرسائل البريدية الإلكترونية",
"service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام"
"service_description_yunohost-api": "يقوم بإدارة التفاعلات ما بين واجهة الويب لواي يونوهوست و النظام",
"log_category_404": "فئةالسجل '{category}' لا وجود لها",
"log_app_fetchlist": "إضافة قائمة للتطبيقات",
"log_app_removelist": "حذف قائمة للتطبيقات",
"log_app_change_url": "تعديل رابط تطبيق '{}'",
"log_app_install": "تنصيب تطبيق '{}'",
"log_app_remove": "حذف تطبيق '{}'",
"log_app_upgrade": "تحديث تطبيق '{}'",
"log_app_makedefault": "تعيين '{}' كتطبيق افتراضي",
"log_available_on_yunopaste": "هذا السجل متوفر الآن على {url}",
"log_backup_restore_system": "استرجاع النظام مِن نسخة احتياطية",
"log_backup_restore_app": "استرجاع '{}' مِن نسخة احتياطية",
"log_remove_on_failed_install": "حذف '{}' بعد فشل التنصيب",
"log_domain_add": "إضافة النطاق '{}' إلى إعدادات النظام",
"log_domain_remove": "حذف النطاق '{}' مِن إعدادات النظام",
"log_dyndns_subscribe": "تسجيل اسم نطاق واي يونوهوست فرعي '{}'",
"log_dyndns_update": "تحديث عنوان الإيبي ذي الصلة مع اسم النطاق الفرعي واي يونوهوست '{}'",
"log_letsencrypt_cert_install": "تنصيب شهادة Lets Encrypt على النطاق '{}'",
"log_selfsigned_cert_install": "تنصيب شهادة موقَّعَة ذاتيا على اسم النطاق '{}'",
"log_letsencrypt_cert_renew": "تجديد شهادة Let's Encrypt لـ '{}'",
"log_service_enable": "تنشيط خدمة '{}'",
"log_user_create": "إضافة المستخدم '{}'",
"log_user_delete": "حذف المستخدم '{}'",
"log_user_update": "تحديث معلومات المستخدم '{}'",
"log_tools_maindomain": "جعل '{}' كنطاق أساسي",
"log_tools_upgrade": "تحديث حُزم ديبيان",
"log_tools_shutdown": "إطفاء الخادم",
"log_tools_reboot": "إعادة تشغيل الخادم",
"migration_description_0005_postgresql_9p4_to_9p6": "تهجير قواعد البيانات مِن postgresql 9.4 إلى 9.6",
"service_description_dnsmasq": "مُكلَّف بتحليل أسماء النطاقات (DNS)",
"service_description_mysql": "يقوم بتخزين بيانات التطبيقات (قواعد بيانات SQL)",
"service_description_rspamd": "يقوم بتصفية البريد المزعج و إدارة ميزات أخرى للبريد",
"service_description_yunohost-firewall": "يريد فتح و غلق منافذ الإتصال إلى الخدمات",
"users_available": "المستخدمون المتوفرون:"
}

25
locales/ca.json Normal file
View file

@ -0,0 +1,25 @@
{
"action_invalid": "Acció '{action:s}' invàlida",
"admin_password": "Contrasenya d'administració",
"admin_password_change_failed": "No s'ha pogut canviar la contrasenya",
"admin_password_changed": "S'ha canviat la contrasenya d'administració",
"app_already_installed": "{app:s} ja està instal·lada",
"app_already_installed_cant_change_url": "Aquesta aplicació ja està instal·lada. La URL no és pot canviar únicament amb aquesta funció. Mireu a \"app changeurl\" si està disponible.",
"app_already_up_to_date": "{app:s} ja està actualitzada",
"app_argument_choice_invalid": "Aquesta opció no és vàlida per l'argument '{name:s}', ha de ser una de {choices:s}",
"app_argument_invalid": "Valor invàlid per l'argument '{name:s}':{error:s}",
"app_argument_required": "Es necessita l'argument '{name:s}'",
"app_change_no_change_url_script": "L'aplicació {app_name:s} encara no permet canviar la seva URL, es possible que s'hagi d'actualitzar.",
"app_change_url_failed_nginx_reload": "No s'ha pogut tornar a carregar nginx. Aquí teniu el resultat de \"nginx -t\":\n{nginx_errors:s}",
"app_change_url_identical_domains": "L'antic i el nou domini/camí són idèntics ('{domain:s}{path:s}'), no hi ha res per fer.",
"app_change_url_no_script": "Aquesta aplicació '{app_name:s}' encara no permet modificar la URL. Potser s'ha d'actualitzar l'aplicació.",
"app_change_url_success": "La URL de {app:s} s'ha canviat correctament a {domain:s}{path:s}",
"app_extraction_failed": "No s'han pogut extreure els fitxers d'instal·lació",
"app_id_invalid": "Id de l'aplicació incorrecte",
"app_incompatible": "L'aplicació {app} no és compatible amb la teva versió de YunoHost",
"app_install_files_invalid": "Fitxers d'instal·lació invàlids",
"app_location_already_used": "L'aplicació '{app}' ja està instal·lada en aquest camí ({path})",
"app_make_default_location_already_used": "No es pot fer l'aplicació '{app}' per defecte en el domini {domain} ja que ja és utilitzat per una altra aplicació '{other_app}'",
"app_location_install_failed": "No s'ha pogut instal·lar l'aplicació en aquest camí ja que entra en conflicte amb l'aplicació '{other_app}' ja instal·lada a '{other_path}'",
"app_location_unavailable": "Aquesta url no està disponible o entra en conflicte amb aplicacions ja instal·lades:\n{apps:s}"
}

View file

@ -1,4 +1,5 @@
{
"aborting": "Aborting.",
"action_invalid": "Invalid action '{action:s}'",
"admin_password": "Administration password",
"admin_password_change_failed": "Unable to change password",
@ -132,6 +133,9 @@
"certmanager_no_cert_file": "Unable to read certificate file for domain {domain:s} (file: {file:s})",
"certmanager_self_ca_conf_file_not_found": "Configuration file not found for self-signing authority (file: {file:s})",
"certmanager_unable_to_parse_self_CA_name": "Unable to parse name of self-signing authority (file: {file:s})",
"confirm_app_install_warning": "Warning: this application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available. Install anyway? [{answers:s}] ",
"confirm_app_install_danger": "WARNING! This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ",
"confirm_app_install_thirdparty": "WARNING! Installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing. Are you willing to take that risk? [{answers:s}] ",
"custom_app_url_required": "You must provide a URL to upgrade your custom app {app:s}",
"custom_appslist_name_required": "You must provide a name for your custom app list",
"diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}",
@ -194,8 +198,13 @@
"global_settings_setting_example_enum": "Example enum option",
"global_settings_setting_example_int": "Example int option",
"global_settings_setting_example_string": "Example string option",
"global_settings_setting_security_password_admin_strength": "Admin password strength",
"global_settings_setting_security_password_user_strength": "User password strength",
"global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json",
"global_settings_setting_service_ssh_allow_deprecated_dsa_hostkey": "Allow the use of (deprecated) DSA hostkey for the SSH daemon configuration",
"global_settings_unknown_type": "Unexpected situation, the setting {setting:s} appears to have the type {unknown_type:s} but it's not a type supported by the system.",
"good_practices_about_admin_password": "You are now about to define a new administration password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
"good_practices_about_user_password": "You are now about to define a new user password. The password should be at least 8 characters - though it is good practice to use longer password (i.e. a passphrase) and/or to use various kind of characters (uppercase, lowercase, digits and special characters).",
"hook_exec_failed": "Script execution failed: {path:s}",
"hook_exec_not_terminated": "Script execution hasn\u2019t terminated: {path:s}",
"hook_list_by_invalid": "Invalid property to list hook by",
@ -209,7 +218,7 @@
"log_category_404": "The log category '{category}' does not exist",
"log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'",
"log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please <a href=\"#/tools/logs/{name}\">provide the full log of this operation</a>",
"log_link_to_failed_log": "The operation '{desc}' has failed ! To get help, please <a href=\"#/tools/logs/{name}\">provide the full log of this operation by clicking here</a>",
"log_help_to_get_failed_log": "The operation '{desc}' has failed ! To get help, please share the full log of this operation using the command 'yunohost log display {name} --share'",
"log_category_404": "The log category '{category}' does not exist",
"log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'",
@ -255,6 +264,7 @@
"mail_domain_unknown": "Unknown mail address domain '{domain:s}'",
"mail_forward_remove_failed": "Unable to remove mail forward '{mail:s}'",
"mailbox_used_space_dovecot_down": "Dovecot mailbox service need to be up, if you want to get mailbox used space",
"mail_unavailable": "This email address is reserved and shall be automatically allocated to the very first user",
"maindomain_change_failed": "Unable to change the main domain",
"maindomain_changed": "The main domain has been changed",
"migrate_tsig_end": "Migration to hmac-sha512 finished",
@ -270,6 +280,9 @@
"migration_description_0003_migrate_to_stretch": "Upgrade the system to Debian Stretch and YunoHost 3.0",
"migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5",
"migration_description_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6",
"migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords",
"migration_description_0007_ssh_conf_managed_by_yunohost_step1": "Let the SSH configuration be managed by YunoHost (step 1, automatic)",
"migration_description_0008_ssh_conf_managed_by_yunohost_step2": "Let the SSH configuration be managed by YunoHost (step 2, manual)",
"migration_0003_backward_impossible": "The stretch migration cannot be reverted.",
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
"migration_0003_patching_sources_list": "Patching the sources.lists ...",
@ -286,6 +299,15 @@
"migration_0005_postgresql_94_not_installed": "Postgresql was not installed on your system. Nothing to do!",
"migration_0005_postgresql_96_not_installed": "Postgresql 9.4 has been found to be installed, but not postgresql 9.6 !? Something weird might have happened on your system :( ...",
"migration_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.",
"migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.",
"migration_0007_cancelled": "YunoHost has failed to improve the way your SSH conf is managed.",
"migration_0007_cannot_restart": "SSH can't be restarted after trying to cancel migration number 6.",
"migration_0008_general_disclaimer": "To improve the security of your server, it is recommended to let YunoHost manage the SSH configuration. Your current SSH configuration differs from the recommended configuration. If you let YunoHost reconfigure it, the way you connect to your server through SSH will change in the following way:",
"migration_0008_port": " - you will have to connect using port 22 instead of your current custom SSH port. Feel free to reconfigure it ;",
"migration_0008_root": " - you will not be able to connect as root through SSH. Instead you should use the admin user ;",
"migration_0008_dsa": " - the DSA key will be disabled. Hence, you might need to invalidate a spooky warning from your SSH client, and recheck the fingerprint of your server ;",
"migration_0008_warning": "If you understand those warnings and agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
"migration_0008_no_warning": "No major risk has been indentified about overriding your SSH configuration - but we can't be absolutely sure ;) ! If you agree to let YunoHost override your current configuration, run the migration. Otherwise, you can also skip the migration - though it is not recommended.",
"migrations_backward": "Migrating backward.",
"migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}",
"migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
@ -299,6 +321,7 @@
"migrations_show_currently_running_migration": "Running migration {number} {name}...",
"migrations_show_last_migration": "Last ran migration is {}",
"migrations_skip_migration": "Skipping migration {number} {name}...",
"migrations_success": "Successfully ran migration {number} {name}!",
"migrations_to_be_ran_manually": "Migration {number} {name} has to be ran manually. Please go to Tools > Migrations on the webadmin, or run `yunohost tools migrations migrate`.",
"migrations_need_to_accept_disclaimer": "To run the migration {number} {name}, your must accept the following disclaimer:\n---\n{disclaimer}\n---\nIf you accept to run the migration, please re-run the command with the option --accept-disclaimer.",
"monitor_disabled": "The server monitoring has been disabled",
@ -328,6 +351,11 @@
"packages_no_upgrade": "There is no package to upgrade",
"packages_upgrade_critical_later": "Critical packages ({packages:s}) will be upgraded later",
"packages_upgrade_failed": "Unable to upgrade all of the packages",
"password_listed": "This password is among the most used password in the world. Please choose something a bit more unique.",
"password_too_simple_1": "Password needs to be at least 8 characters long",
"password_too_simple_2": "Password needs to be at least 8 characters long and contains digit, upper and lower characters",
"password_too_simple_3": "Password needs to be at least 8 characters long and contains digit, upper, lower and special characters",
"password_too_simple_4": "Password needs to be at least 12 characters long and contains digit, upper, lower and special characters",
"path_removal_failed": "Unable to remove path {:s}",
"pattern_backup_archive_name": "Must be a valid filename with max 30 characters, and alphanumeric and -_. characters only",
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
@ -363,6 +391,8 @@
"restore_running_app_script": "Running restore script of app '{app:s}'...",
"restore_running_hooks": "Running restoration hooks...",
"restore_system_part_failed": "Unable to restore the '{part:s}' system part",
"root_password_desynchronized": "The admin password has been changed, but YunoHost was unable to propagate this on the root password !",
"root_password_replaced_by_admin_password": "Your root password have been replaced by your admin password.",
"server_shutdown": "The server will shutdown",
"server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]",
"server_reboot": "The server will reboot",
@ -380,7 +410,7 @@
"service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'",
"service_conf_file_removed": "The configuration file '{conf}' has been removed",
"service_conf_file_updated": "The configuration file '{conf}' has been updated",
"service_conf_new_managed_file": "The configuration file '{conf}' is now managed by the service {service}.",
"service_conf_now_managed_by_yunohost": "The configuration file '{conf}' is now managed by YunoHost.",
"service_conf_up_to_date": "The configuration is already up-to-date for service '{service}'",
"service_conf_updated": "The configuration has been updated for service '{service}'",
"service_conf_would_be_updated": "The configuration would have been updated for service '{service}'",

View file

@ -97,7 +97,7 @@
"dyndns_no_domain_registered": "Ningún dominio ha sido registrado con DynDNS",
"dyndns_registered": "El dominio DynDNS ha sido registrado",
"dyndns_registration_failed": "No se pudo registrar el dominio DynDNS: {error:s}",
"dyndns_unavailable": "El subdominio DynDNS no está disponible",
"dyndns_unavailable": "El dominio {domain:s} no está disponible.",
"executing_command": "Ejecutando el comando '{command:s}'...",
"executing_script": "Ejecutando el script '{script:s}'...",
"extracting": "Extrayendo...",
@ -174,7 +174,7 @@
"restore_complete": "Restauración finalizada",
"restore_confirm_yunohost_installed": "¿Realmente desea restaurar un sistema ya instalado? [{answers:s}]",
"restore_failed": "No se pudo restaurar el sistema",
"restore_hook_unavailable": "El hook de restauración '{hook:s}' no está disponible en su sistema",
"restore_hook_unavailable": "El script de restauración '{part:s}' no está disponible en su sistema y tampoco en el archivo",
"restore_nothings_done": "No se ha restaurado nada",
"restore_running_app_script": "Ejecutando el script de restauración de la aplicación '{app:s}'...",
"restore_running_hooks": "Ejecutando los hooks de restauración...",
@ -204,7 +204,7 @@
"service_regenconf_pending_applying": "Aplicando la configuración pendiente para el servicio '{service}'...",
"service_remove_failed": "No se pudo desinstalar el servicio '{service:s}'",
"service_removed": "El servicio '{service:s}' ha sido desinstalado",
"service_start_failed": "No se pudo iniciar el servicio '{service:s}'",
"service_start_failed": "No se pudo iniciar el servicio '{service:s}'\n\nRegistros de servicio recientes : {logs:s}",
"service_started": "El servicio '{service:s}' ha sido iniciado",
"service_status_failed": "No se pudo determinar el estado del servicio '{service:s}'",
"service_stop_failed": "No se pudo detener el servicio '{service:s}'",
@ -281,7 +281,7 @@
"app_change_url_identical_domains": "El antiguo y nuevo dominio/url_path son idénticos ('{domain:s} {path:s}'), no se realizarán cambios.",
"app_change_url_no_script": "Esta aplicación '{app_name:s}' aún no permite modificar su URL. Quizás debería actualizar la aplicación.",
"app_change_url_success": "El URL de la aplicación {app:s} ha sido cambiado correctamente a {domain:s} {path:s}",
"app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada",
"app_location_unavailable": "Este URL no está disponible o está en conflicto con otra aplicación instalada:\n{apps:s}",
"app_already_up_to_date": "La aplicación {app:s} ya está actualizada",
"appslist_name_already_tracked": "Ya existe una lista de aplicaciones registrada con el nombre {name:s}.",
"appslist_url_already_tracked": "Ya existe una lista de aplicaciones registrada con el URL {url:s}.",
@ -307,5 +307,15 @@
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar el archivo",
"backup_couldnt_bind": "No puede enlazar {src:s} con {dest:s}",
"backup_csv_addition_failed": "No puede añadir archivos al backup en el archivo CSV",
"backup_csv_creation_failed": "No se puede crear el archivo CSV necesario para futuras operaciones de restauración"
"backup_csv_creation_failed": "No se puede crear el archivo CSV necesario para futuras operaciones de restauración",
"backup_custom_mount_error": "Fracaso del método de copia de seguridad personalizada en la etapa \"mount\"",
"backup_custom_need_mount_error": "Fracaso del método de copia de seguridad personalizada en la étapa \"need_mount\"",
"backup_no_uncompress_archive_dir": "El directorio del archivo descomprimido no existe",
"backup_php5_to_php7_migration_may_fail": "No se ha podido convertir su archivo para soportar php7, la restauración de sus aplicaciones php puede fallar (razón : {error:s})",
"backup_system_part_failed": "No se puede hacer una copia de seguridad de la parte \"{part:s}\" del sistema",
"backup_with_no_backup_script_for_app": "La aplicación {app:s} no tiene script de respaldo. Se ha ignorado.",
"backup_with_no_restore_script_for_app": "La aplicación {app:s} no tiene script de restauración, no podrá restaurar automáticamente la copia de seguridad de esta aplicación.",
"dyndns_could_not_check_provide": "No se pudo verificar si {provider:s} puede ofrecer {domain:s}.",
"dyndns_domain_not_provided": "El proveedor Dyndns {provider:s} no puede proporcionar el dominio {domain:s}.",
"experimental_feature": "Cuidado : esta funcionalidad es experimental y no es considerada estable, no debería usarla excepto si sabe lo que hace."
}

View file

@ -1,25 +1,25 @@
{
"action_invalid": "Action « {action:s} » incorrecte",
"admin_password": "Mot de passe d'administration",
"admin_password": "Mot de passe dadministration",
"admin_password_change_failed": "Impossible de changer le mot de passe",
"admin_password_changed": "Le mot de passe d'administration a été modifié",
"admin_password_changed": "Le mot de passe dadministration a été modifié",
"app_already_installed": "{app:s} est déjà installé",
"app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être l'un de {choices:s}",
"app_argument_choice_invalid": "Choix invalide pour le paramètre « {name:s} », il doit être lun de {choices:s}",
"app_argument_invalid": "Valeur invalide pour le paramètre « {name:s} » : {error:s}",
"app_argument_missing": "Paramètre manquant « {:s} »",
"app_argument_required": "Le paramètre « {name:s} » est requis",
"app_extraction_failed": "Impossible d'extraire les fichiers d'installation",
"app_id_invalid": "Id d'application incorrect",
"app_incompatible": "L'application {app} est incompatible avec votre version de YunoHost",
"app_install_files_invalid": "Fichiers d'installation incorrects",
"app_location_already_used": "L'application '{app}' est déjà installée à cet emplacement ({path})",
"app_location_install_failed": "Impossible d'installer l'application à cet emplacement pour cause de conflit avec l'app '{other_app}' déjà installée sur '{other_path}'",
"app_manifest_invalid": "Manifeste d'application incorrect : {error}",
"app_extraction_failed": "Impossible dextraire les fichiers dinstallation",
"app_id_invalid": "Id dapplication incorrect",
"app_incompatible": "Lapplication {app} est incompatible avec votre version de YunoHost",
"app_install_files_invalid": "Fichiers dinstallation incorrects",
"app_location_already_used": "Lapplication « {app} » est déjà installée à cet emplacement ({path})",
"app_location_install_failed": "Impossible dinstaller lapplication à cet emplacement pour cause de conflit avec lapp « {other_app} » déjà installée sur « {other_path} »",
"app_manifest_invalid": "Manifeste dapplication incorrect : {error}",
"app_no_upgrade": "Aucune application à mettre à jour",
"app_not_correctly_installed": "{app:s} semble être mal installé",
"app_not_installed": "{app:s} n'est pas installé",
"app_not_properly_removed": "{app:s} n'a pas été supprimé correctement",
"app_package_need_update": "Le paquet de l'application {app} doit être mis à jour pour suivre les changements de YunoHost",
"app_not_installed": "{app:s} nest pas installé",
"app_not_properly_removed": "{app:s} na pas été supprimé correctement",
"app_package_need_update": "Le paquet de lapplication {app} doit être mis à jour pour suivre les changements de YunoHost",
"app_recent_version_required": "{app:s} nécessite une version plus récente de YunoHost",
"app_removed": "{app:s} a été supprimé",
"app_requirements_checking": "Vérification des paquets requis pour {app}...",
@ -27,45 +27,45 @@
"app_requirements_unmeet": "Les pré-requis de {app} ne sont pas satisfaits, le paquet {pkgname} ({version}) doit être {spec}",
"app_sources_fetch_failed": "Impossible de récupérer les fichiers sources",
"app_unknown": "Application inconnue",
"app_unsupported_remote_type": "Le type distant utilisé par l'application n'est pas supporté",
"app_unsupported_remote_type": "Le type distant utilisé par lapplication nest pas pris en charge",
"app_upgrade_failed": "Impossible de mettre à jour {app:s}",
"app_upgraded": "{app:s} a été mis à jour",
"appslist_fetched": "La liste dapplications {appslist:s} a été récupérée",
"appslist_removed": "La liste dapplications {appslist:s} a été supprimée",
"appslist_retrieve_error": "Impossible de récupérer la liste dapplications distante {appslist:s} : {error:s}",
"appslist_unknown": "La liste dapplications {appslist:s} est inconnue.",
"ask_current_admin_password": "Mot de passe d'administration actuel",
"ask_current_admin_password": "Mot de passe dadministration actuel",
"ask_email": "Adresse courriel",
"ask_firstname": "Prénom",
"ask_lastname": "Nom",
"ask_list_to_remove": "Liste à supprimer",
"ask_main_domain": "Domaine principal",
"ask_new_admin_password": "Nouveau mot de passe d'administration",
"ask_new_admin_password": "Nouveau mot de passe dadministration",
"ask_password": "Mot de passe",
"backup_action_required": "Vous devez préciser ce qui est à sauvegarder",
"backup_app_failed": "Impossible de sauvegarder l'application « {app:s} »",
"backup_archive_app_not_found": "L'application « {app:s} » n'a pas été trouvée dans l'archive de la sauvegarde",
"backup_app_failed": "Impossible de sauvegarder lapplication « {app:s} »",
"backup_archive_app_not_found": "Lapplication « {app:s} » na pas été trouvée dans larchive de la sauvegarde",
"backup_archive_hook_not_exec": "Le script « {hook:s} » n'a pas été exécuté dans cette sauvegarde",
"backup_archive_name_exists": "Une archive de sauvegarde avec ce nom existe déjà",
"backup_archive_name_unknown": "L'archive locale de sauvegarde nommée « {name:s} » est inconnue",
"backup_archive_open_failed": "Impossible d'ouvrir l'archive de sauvegarde",
"backup_archive_name_unknown": "Larchive locale de sauvegarde nommée « {name:s} » est inconnue",
"backup_archive_open_failed": "Impossible douvrir larchive de sauvegarde",
"backup_cleaning_failed": "Impossible de nettoyer le dossier temporaire de sauvegarde",
"backup_created": "Sauvegarde terminée",
"backup_creating_archive": "Création de l'archive de sauvegarde...",
"backup_creating_archive": "Création de larchive de sauvegarde...",
"backup_creation_failed": "Impossible de créer la sauvegarde",
"backup_delete_error": "Impossible de supprimer « {path:s} »",
"backup_deleted": "La sauvegarde a été supprimée",
"backup_extracting_archive": "Extraction de l'archive de sauvegarde...",
"backup_extracting_archive": "Extraction de larchive de sauvegarde...",
"backup_hook_unknown": "Script de sauvegarde « {hook:s} » inconnu",
"backup_invalid_archive": "Archive de sauvegarde incorrecte",
"backup_nothings_done": "Il n'y a rien à sauvegarder",
"backup_nothings_done": "Il ny a rien à sauvegarder",
"backup_output_directory_forbidden": "Dossier de destination interdit. Les sauvegardes ne peuvent être créées dans les dossiers /bin, /boot, /dev, /etc, /lib, /root, /run, /sbin, /sys, /usr, /var ou /home/yunohost.backup/archives",
"backup_output_directory_not_empty": "Le dossier de sortie n'est pas vide",
"backup_output_directory_not_empty": "Le dossier de sortie nest pas vide",
"backup_output_directory_required": "Vous devez spécifier un dossier de sortie pour la sauvegarde",
"backup_running_app_script": "Lancement du script de sauvegarde de l'application « {app:s} »...",
"backup_running_app_script": "Lancement du script de sauvegarde de lapplication « {app:s} »...",
"backup_running_hooks": "Exécution des scripts de sauvegarde...",
"custom_app_url_required": "Vous devez spécifier une URL pour mettre à jour votre application locale {app:s}",
"custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste d'applications personnalisée",
"custom_appslist_name_required": "Vous devez spécifier un nom pour votre liste dapplications personnalisée",
"diagnosis_debian_version_error": "Impossible de déterminer la version de Debian : {error}",
"diagnosis_kernel_version_error": "Impossible de récupérer la version du noyau : {error}",
"diagnosis_monitor_disk_error": "Impossible de superviser les disques : {error}",
@ -82,22 +82,22 @@
"domain_dyndns_invalid": "Domaine incorrect pour un usage avec DynDNS",
"domain_dyndns_root_unknown": "Domaine DynDNS principal inconnu",
"domain_exists": "Le domaine existe déjà",
"domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez d'abord les désinstaller avant de supprimer ce domaine",
"domain_uninstall_app_first": "Une ou plusieurs applications sont installées sur ce domaine. Veuillez dabord les désinstaller avant de supprimer ce domaine",
"domain_unknown": "Domaine inconnu",
"domain_zone_exists": "Le fichier de zone DNS existe déjà",
"domain_zone_not_found": "Fichier de zone DNS introuvable pour le domaine {:s}",
"done": "Terminé",
"downloading": "Téléchargement...",
"dyndns_cron_installed": "La tâche cron pour le domaine DynDNS a été installée",
"dyndns_cron_remove_failed": "Impossible d'enlever la tâche cron pour le domaine DynDNS",
"dyndns_cron_remove_failed": "Impossible denlever la tâche cron pour le domaine DynDNS",
"dyndns_cron_removed": "La tâche cron pour le domaine DynDNS a été enlevée",
"dyndns_ip_update_failed": "Impossible de mettre à jour l'adresse IP sur le domaine DynDNS",
"dyndns_ip_update_failed": "Impossible de mettre à jour ladresse IP sur le domaine DynDNS",
"dyndns_ip_updated": "Votre adresse IP a été mise à jour pour le domaine DynDNS",
"dyndns_key_generating": "La clé DNS est en cours de génération, cela peut prendre du temps...",
"dyndns_key_not_found": "Clé DNS introuvable pour le domaine",
"dyndns_no_domain_registered": "Aucun domaine n'a été enregistré avec DynDNS",
"dyndns_no_domain_registered": "Aucun domaine na été enregistré avec DynDNS",
"dyndns_registered": "Le domaine DynDNS a été enregistré",
"dyndns_registration_failed": "Impossible d'enregistrer le domaine DynDNS : {error:s}",
"dyndns_registration_failed": "Impossible denregistrer le domaine DynDNS : {error:s}",
"dyndns_unavailable": "Le domaine {domain:s} est indisponible.",
"executing_command": "Exécution de la commande « {command:s} »...",
"executing_script": "Exécution du script « {script:s} »...",
@ -105,7 +105,7 @@
"field_invalid": "Champ incorrect : « {:s} »",
"firewall_reload_failed": "Impossible de recharger le pare-feu",
"firewall_reloaded": "Le pare-feu a été rechargé",
"firewall_rules_cmd_failed": "Certaines règles du pare-feu n'ont pas pu être appliquées. Pour plus d'informations, consultez le journal.",
"firewall_rules_cmd_failed": "Certaines règles du pare-feu nont pas pu être appliquées. Pour plus dinformations, consultez le journal.",
"format_datetime_short": "%d/%m/%Y %H:%M",
"hook_argument_missing": "Argument manquant : '{:s}'",
"hook_choice_invalid": "Choix incorrect : '{:s}'",
@ -114,12 +114,12 @@
"hook_list_by_invalid": "La propriété de tri des actions est invalide",
"hook_name_unknown": "Nom de script « {name:s} » inconnu",
"installation_complete": "Installation terminée",
"installation_failed": "Échec de l'installation",
"ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas",
"iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le supporte pas",
"ldap_initialized": "L'annuaire LDAP a été initialisé",
"installation_failed": "Échec de linstallation",
"ip6tables_unavailable": "Vous ne pouvez pas jouer avec ip6tables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
"iptables_unavailable": "Vous ne pouvez pas jouer avec iptables ici. Vous êtes soit dans un conteneur, soit votre noyau ne le prend pas en charge",
"ldap_initialized": "Lannuaire LDAP a été initialisé",
"license_undefined": "indéfinie",
"mail_alias_remove_failed": "Impossible de supprimer l'alias courriel « {mail:s} »",
"mail_alias_remove_failed": "Impossible de supprimer lalias courriel « {mail:s} »",
"mail_domain_unknown": "Le domaine « {domain:s} » du courriel est inconnu",
"mail_forward_remove_failed": "Impossible de supprimer le courriel de transfert « {mail:s} »",
"maindomain_change_failed": "Impossible de modifier le domaine principal",
@ -127,29 +127,29 @@
"monitor_disabled": "La supervision du serveur a été désactivé",
"monitor_enabled": "La supervision du serveur a été activé",
"monitor_glances_con_failed": "Impossible de se connecter au serveur Glances",
"monitor_not_enabled": "Le suivi de l'état du serveur n'est pas activé",
"monitor_not_enabled": "Le suivi de létat du serveur nest pas activé",
"monitor_period_invalid": "Période de temps incorrecte",
"monitor_stats_file_not_found": "Le fichier de statistiques est introuvable",
"monitor_stats_no_update": "Aucune donnée de l'état du serveur à mettre à jour",
"monitor_stats_period_unavailable": "Aucune statistique n'est disponible pour la période",
"monitor_stats_no_update": "Aucune donnée de létat du serveur à mettre à jour",
"monitor_stats_period_unavailable": "Aucune statistique nest disponible pour la période",
"mountpoint_unknown": "Point de montage inconnu",
"mysql_db_creation_failed": "Impossible de créer la base de données MySQL",
"mysql_db_init_failed": "Impossible d'initialiser la base de données MySQL",
"mysql_db_init_failed": "Impossible dinitialiser la base de données MySQL",
"mysql_db_initialized": "La base de données MySQL a été initialisée",
"network_check_mx_ko": "L'enregistrement DNS MX n'est pas précisé",
"network_check_mx_ko": "Lenregistrement DNS MX nest pas précisé",
"network_check_smtp_ko": "Le trafic courriel sortant (port 25 SMTP) semble bloqué par votre réseau",
"network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) n'est pas bloqué",
"network_check_smtp_ok": "Le trafic courriel sortant (port 25 SMTP) nest pas bloqué",
"new_domain_required": "Vous devez spécifier le nouveau domaine principal",
"no_appslist_found": "Aucune liste dapplications na été trouvée",
"no_internet_connection": "Le serveur n'est pas connecté à Internet",
"no_ipv6_connectivity": "La connectivité IPv6 n'est pas disponible",
"no_restore_script": "Le script de sauvegarde n'a pas été trouvé pour l'application « {app:s} »",
"no_internet_connection": "Le serveur nest pas connecté à Internet",
"no_ipv6_connectivity": "La connectivité IPv6 nest pas disponible",
"no_restore_script": "Le script de sauvegarde na pas été trouvé pour lapplication « {app:s} »",
"no_such_conf_file": "Le fichier {file:s} nexiste pas, il ne peut pas être copié",
"not_enough_disk_space": "L'espace disque est insuffisant sur « {path:s} »",
"package_not_installed": "Le paquet « {pkgname} » n'est pas installé",
"not_enough_disk_space": "Lespace disque est insuffisant sur « {path:s} »",
"package_not_installed": "Le paquet « {pkgname} » nest pas installé",
"package_unexpected_error": "Une erreur inattendue est survenue avec le paquet « {pkgname} »",
"package_unknown": "Paquet « {pkgname} » inconnu",
"packages_no_upgrade": "Il n'y a aucun paquet à mettre à jour",
"packages_no_upgrade": "Il ny a aucun paquet à mettre à jour",
"packages_upgrade_critical_later": "Les paquets critiques ({packages:s}) seront mis à jour ultérieurement",
"packages_upgrade_failed": "Impossible de mettre à jour tous les paquets",
"path_removal_failed": "Impossible de supprimer le chemin {:s}",
@ -160,7 +160,7 @@
"pattern_lastname": "Doit être un nom valide",
"pattern_listname": "Doit être composé uniquement de caractères alphanumériques et de tirets bas",
"pattern_mailbox_quota": "Doit être une taille avec le suffixe b/k/M/G/T ou 0 pour désactiver le quota",
"pattern_password": "Doit être composé d'au moins 3 caractères",
"pattern_password": "Doit être composé dau moins 3 caractères",
"pattern_port": "Doit être un numéro de port valide (ex. : 0-65535)",
"pattern_port_or_range": "Doit être un numéro de port valide (ex. : 0-65535) ou une gamme de ports (ex. : 100:200)",
"pattern_positive_number": "Doit être un nombre positif",
@ -168,24 +168,24 @@
"port_already_closed": "Le port {port:d} est déjà fermé pour les connexions {ip_version:s}",
"port_already_opened": "Le port {port:d} est déjà ouvert pour les connexions {ip_version:s}",
"port_available": "Le port {port:d} est disponible",
"port_unavailable": "Le port {port:d} n'est pas disponible",
"port_unavailable": "Le port {port:d} nest pas disponible",
"restore_action_required": "Vous devez préciser ce qui est à restaurer",
"restore_already_installed_app": "Une application est déjà installée avec l'id « {app:s} »",
"restore_app_failed": "Impossible de restaurer l'application « {app:s} »",
"restore_already_installed_app": "Une application est déjà installée avec lid « {app:s} »",
"restore_app_failed": "Impossible de restaurer lapplication « {app:s} »",
"restore_cleaning_failed": "Impossible de nettoyer le dossier temporaire de restauration",
"restore_complete": "Restauration terminée",
"restore_confirm_yunohost_installed": "Voulez-vous vraiment restaurer un système déjà installé ? [{answers:s}]",
"restore_failed": "Impossible de restaurer le système",
"restore_hook_unavailable": "Le script de restauration « {part:s} » n'est pas disponible sur votre système, et nest pas non plus dans larchive",
"restore_nothings_done": "Rien n'a été restauré",
"restore_running_app_script": "Lancement du script de restauration pour l'application « {app:s} »...",
"restore_hook_unavailable": "Le script de restauration « {part:s} » nest pas disponible sur votre système, et nest pas non plus dans larchive",
"restore_nothings_done": "Rien na été restauré",
"restore_running_app_script": "Lancement du script de restauration pour lapplication « {app:s} »...",
"restore_running_hooks": "Exécution des scripts de restauration...",
"service_add_configuration": "Ajout du fichier de configuration {file:s}",
"service_add_failed": "Impossible d'ajouter le service « {service:s} »",
"service_add_failed": "Impossible dajouter le service « {service:s} »",
"service_added": "Le service « {service:s} » a été ajouté",
"service_already_started": "Le service « {service:s} » est déjà démarré",
"service_already_stopped": "Le service « {service:s} » est déjà arrêté",
"service_cmd_exec_failed": "Impossible d'exécuter la commande « {command:s} »",
"service_cmd_exec_failed": "Impossible dexécuter la commande « {command:s} »",
"service_conf_file_backed_up": "Le fichier de configuration « {conf} » a été sauvegardé dans « {backup} »",
"service_conf_file_copy_failed": "Impossible de copier le nouveau fichier de configuration « {new} » vers « {conf} »",
"service_conf_file_manually_modified": "Le fichier de configuration « {conf} » a été modifié manuellement et ne sera pas mis à jour",
@ -208,7 +208,7 @@
"service_regenconf_dry_pending_applying": "Vérification des configurations en attentes qui pourraient être appliquées pour le service « {service} »…",
"service_regenconf_failed": "Impossible de régénérer la configuration pour les services : {services}",
"service_regenconf_pending_applying": "Application des configurations en attentes pour le service « {service} »…",
"service_remove_failed": "Impossible d'enlever le service « {service:s} »",
"service_remove_failed": "Impossible denlever le service « {service:s} »",
"service_removed": "Le service « {service:s} » a été enlevé",
"service_start_failed": "Impossible de démarrer le service « {service:s} »\n\nJournaux récents : {logs:s}",
"service_started": "Le service « {service:s} » a été démarré",
@ -221,38 +221,38 @@
"ssowat_conf_generated": "La configuration de SSOwat a été générée",
"ssowat_conf_updated": "La configuration de SSOwat a été mise à jour",
"system_upgraded": "Le système a été mis à jour",
"system_username_exists": "Le nom d'utilisateur existe déjà dans les utilisateurs système",
"unbackup_app": "L'application « {app:s} » ne sera pas sauvegardée",
"system_username_exists": "Le nom dutilisateur existe déjà dans les utilisateurs système",
"unbackup_app": "Lapplication « {app:s} » ne sera pas sauvegardée",
"unexpected_error": "Une erreur inattendue est survenue",
"unit_unknown": "Unité « {unit:s} » inconnue",
"unlimit": "Pas de quota",
"unrestore_app": "L'application « {app:s} » ne sera pas restaurée",
"update_cache_failed": "Impossible de mettre à jour le cache de l'APT",
"unrestore_app": "Lapplication « {app:s} » ne sera pas restaurée",
"update_cache_failed": "Impossible de mettre à jour le cache de lAPT",
"updating_apt_cache": "Mise à jour de la liste des paquets disponibles...",
"upgrade_complete": "Mise à jour terminée",
"upgrading_packages": "Mise à jour des paquets...",
"upnp_dev_not_found": "Aucun périphérique compatible UPnP n'a été trouvé",
"upnp_dev_not_found": "Aucun périphérique compatible UPnP na été trouvé",
"upnp_disabled": "UPnP a été désactivé",
"upnp_enabled": "UPnP a été activé",
"upnp_port_open_failed": "Impossible d'ouvrir les ports avec UPnP",
"user_created": "L'utilisateur a été créé",
"user_creation_failed": "Impossible de créer l'utilisateur",
"user_deleted": "L'utilisateur a été supprimé",
"user_deletion_failed": "Impossible de supprimer l'utilisateur",
"user_home_creation_failed": "Impossible de créer le dossier personnel de l'utilisateur",
"user_info_failed": "Impossible de récupérer les informations de l'utilisateur",
"upnp_port_open_failed": "Impossible douvrir les ports avec UPnP",
"user_created": "Lutilisateur a été créé",
"user_creation_failed": "Impossible de créer lutilisateur",
"user_deleted": "Lutilisateur a été supprimé",
"user_deletion_failed": "Impossible de supprimer lutilisateur",
"user_home_creation_failed": "Impossible de créer le dossier personnel de lutilisateur",
"user_info_failed": "Impossible de récupérer les informations de lutilisateur",
"user_unknown": "Utilisateur « {user:s} » inconnu",
"user_update_failed": "Impossible de modifier l'utilisateur",
"user_updated": "L'utilisateur a été modifié",
"user_update_failed": "Impossible de modifier lutilisateur",
"user_updated": "Lutilisateur a été modifié",
"yunohost_already_installed": "YunoHost est déjà installé",
"yunohost_ca_creation_failed": "Impossible de créer l'autorité de certification",
"yunohost_ca_creation_failed": "Impossible de créer lautorité de certification",
"yunohost_configured": "YunoHost a été configuré",
"yunohost_installing": "Installation de YunoHost...",
"yunohost_not_installed": "YunoHost n'est pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »",
"yunohost_not_installed": "YunoHost nest pas ou pas correctement installé. Veuillez exécuter « yunohost tools postinstall »",
"certmanager_attempt_to_replace_valid_cert": "Vous êtes en train de remplacer un certificat correct et valide pour le domaine {domain:s} ! (Utilisez --force pour contourner)",
"certmanager_domain_unknown": "Domaine inconnu {domain:s}",
"certmanager_domain_cert_not_selfsigned": "Le certificat du domaine {domain:s} nest pas auto-signé. Voulez-vous vraiment le remplacer ? (Utilisez --force)",
"certmanager_certificate_fetching_or_enabling_failed": "Il semble que l'activation du nouveau certificat pour {domain:s} a échoué…",
"certmanager_certificate_fetching_or_enabling_failed": "Il semble que lactivation du nouveau certificat pour {domain:s} a échoué…",
"certmanager_attempt_to_renew_nonLE_cert": "Le certificat pour le domaine {domain:s} nest pas fourni par Lets Encrypt. Impossible de le renouveler automatiquement !",
"certmanager_attempt_to_renew_valid_cert": "Le certificat pour le domaine {domain:s} est sur le point dexpirer ! Utilisez --force pour contourner",
"certmanager_domain_http_not_working": "Il semble que le domaine {domain:s} nest pas accessible via HTTP. Veuillez vérifier que vos configuration DNS et nginx sont correctes",
@ -273,15 +273,15 @@
"domain_cannot_remove_main": "Impossible de retirer le domaine principal. Définissez un nouveau domaine principal au préalable.",
"certmanager_self_ca_conf_file_not_found": "Le fichier de configuration pour lautorité du certificat auto-signé est introuvable (fichier : {file:s})",
"certmanager_unable_to_parse_self_CA_name": "Impossible danalyser le nom de lautorité du certificat auto-signé (fichier : {file:s})",
"mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir l'espace disque occupé par la messagerie",
"mailbox_used_space_dovecot_down": "Le service de mail Dovecot doit être démarré, si vous souhaitez voir lespace disque occupé par la messagerie",
"domains_available": "Domaines disponibles :",
"backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path:s})",
"backup_archive_broken_link": "Impossible daccéder à larchive de sauvegarde (lien invalide vers {path:s})",
"certmanager_acme_not_configured_for_domain": "Le certificat du domaine {domain:s} ne semble pas être correctement installé. Veuillez préalablement exécuter cert-install pour ce domaine.",
"certmanager_domain_not_resolved_locally": "Le domaine {domain:s} ne peut être déterminé depuis votre serveur YunoHost. Cela peut arriver si vous avez récemment modifié votre enregistrement DNS. Auquel cas, merci dattendre quelques heures quil se propage. Si le problème persiste, envisager dajouter {domain:s} au fichier /etc/hosts. (Si vous savez ce que vous faites, utilisez --no-checks pour désactiver ces vérifications.)",
"certmanager_http_check_timeout": "Expiration du délai lors de la tentative du serveur de se contacter via HTTP en utilisant son adresse IP publique (domaine {domain:s} avec lIP {ip:s}). Vous rencontrez peut-être un problème dhairpinning ou alors le pare-feu/routeur en amont de votre serveur est mal configuré.",
"certmanager_couldnt_fetch_intermediate_cert": "Expiration du délai lors de la tentative de récupération du certificat intermédiaire depuis Lets Encrypt. Linstallation/le renouvellement du certificat a été interrompu - veuillez réessayer prochainement.",
"appslist_retrieve_bad_format": "Le fichier récupéré pour la liste dapplications {appslist:s} nest pas valide",
"domain_hostname_failed": "Échec de la création d'un nouveau nom d'hôte",
"domain_hostname_failed": "Échec de la création dun nouveau nom dhôte",
"yunohost_ca_creation_success": "Lautorité de certification locale a été créée.",
"appslist_name_already_tracked": "Il y a déjà une liste dapplications enregistrée avec le nom {name:s}.",
"appslist_url_already_tracked": "Il y a déjà une liste dapplications enregistrée avec lURL {url:s}.",
@ -294,7 +294,7 @@
"app_change_url_identical_domains": "Lancien et le nouveau couple domaine/chemin sont identiques pour {domain:s}{path:s}, aucune action.",
"app_change_url_no_script": "Lapplication {app_name:s} ne prend pas encore en charge le changement dURL. Vous devriez peut-être la mettre à jour.",
"app_change_url_success": "LURL de lapplication {app:s} a été changée en {domain:s}{path:s}",
"app_location_unavailable": "Cette URL nest pas disponible ou est en conflit avec une application existante",
"app_location_unavailable": "Cette URL nest pas disponible ou est en conflit avec une application existante\n{apps:s}",
"app_already_up_to_date": "{app:s} est déjà à jour",
"invalid_url_format": "Format dURL non valide",
"global_settings_bad_choice_for_enum": "La valeur du paramètre {setting:s} est incorrecte. Reçu : {received_type:s}; attendu : {expected_type:s}",
@ -368,8 +368,8 @@
"ask_path": "Chemin",
"dyndns_could_not_check_provide": "Impossible de vérifier si {provider:s} peut fournir {domain:s}.",
"dyndns_domain_not_provided": "Le fournisseur Dyndns {provider:s} ne peut pas fournir le domaine {domain:s}.",
"app_make_default_location_already_used": "Impossible de configurer l'app '{app}' par défaut pour le domaine {domain}, déjà utilisé par l'autre app '{other_app}'",
"app_upgrade_app_name": "Mise à jour de l'application {app}...",
"app_make_default_location_already_used": "Impossible de configurer lapp « {app} » par défaut pour le domaine {domain}, déjà utilisé par lautre app « {other_app} »",
"app_upgrade_app_name": "Mise à jour de lapplication {app}...",
"backup_output_symlink_dir_broken": "Vous avez un lien symbolique cassé à la place de votre dossier darchives « {path:s} ». Vous pourriez avoir une configuration personnalisée pour sauvegarder vos données sur un autre système de fichiers, dans ce cas, vous avez probablement oublié de monter ou de connecter votre disque / clef USB.",
"migrate_tsig_end": "La migration à hmac-sha512 est terminée",
"migrate_tsig_failed": "La migration du domaine dyndns {domain} à hmac-sha512 a échoué, annulation des modifications. Erreur : {error_code} - {error}",
@ -393,7 +393,7 @@
"migration_0003_not_jessie": "La distribution Debian actuelle nest pas Jessie !",
"migration_0003_system_not_fully_up_to_date": "Votre système nest pas complètement à jour. Veuillez mener une mise à jour classique avant de lancer à migration à Stretch.",
"migration_0003_still_on_jessie_after_main_upgrade": "Quelque chose sest ma passé pendant la mise à niveau principale : le système est toujours sur Jessie ?!? Pour investiguer le problème, veuillez regarder {log} 🙁…",
"migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si léquipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus dinformations sur https://yunohost.org/backup ;\n - dêtre patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusqu'à quelques heures pour que tout soit à niveau.\n\nDe plus, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). Lancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !",
"migration_0003_general_warning": "Veuillez noter que cette migration est une opération délicate. Si léquipe YunoHost a fait de son mieux pour la relire et la tester, la migration pourrait tout de même casser des parties de votre système ou de vos applications.\n\nEn conséquence, nous vous recommandons :\n - de lancer une sauvegarde de vos données ou applications critiques. Plus dinformations sur https://yunohost.org/backup ;\n - dêtre patient après avoir lancé la migration : selon votre connexion internet et matériel, cela pourrait prendre jusquà quelques heures pour que tout soit à niveau.\n\nDe plus, le port SMTP utilisé par les clients de messagerie externes comme (Thunderbird ou K9-Mail) a été changé de 465 (SSL/TLS) à 587 (STARTTLS). Lancien port 465 sera automatiquement fermé et le nouveau port 587 sera ouvert dans le pare-feu. Vous et vos utilisateurs *devront* adapter la configuration de vos clients de messagerie en conséquence !",
"migration_0003_problematic_apps_warning": "Veuillez noter que les applications suivantes, éventuellement problématiques, ont été détectées. Il semble quelles naient pas été installées depuis une liste dapplication ou quelles ne soit pas marquées «working ». En conséquence, nous ne pouvons pas garantir quelles fonctionneront après la mise à niveau : {problematic_apps}",
"migration_0003_modified_files": "Veuillez noter que les fichiers suivants ont été détectés comme modifiés manuellement et pourraient être écrasés à la fin de la mise à niveau : {manually_modified_files}",
"migrations_list_conflict_pending_done": "Vous ne pouvez pas utiliser --previous et --done simultanément.",
@ -416,5 +416,69 @@
"service_description_slapd": "stocke les utilisateurs, domaines et leurs informations liées",
"service_description_ssh": "vous permet de vous connecter à distance à votre serveur via un terminal (protocole SSH)",
"service_description_yunohost-api": "permet les interactions entre linterface web de YunoHost et le système",
"service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services"
"service_description_yunohost-firewall": "gère les ports de connexion ouverts et fermés aux services",
"experimental_feature": "Attention : cette fonctionnalité est expérimentale et ne doit pas être considérée comme stable, vous ne devriez pas lutiliser à moins que vous ne sachiez ce que vous faîtes.",
"log_corrupted_md_file": "Le fichier yaml de metadata associé aux logs est corrompu : {md_file}",
"log_category_404": "La catégorie de log « {category} » nexiste pas",
"log_link_to_log": "Log complet de cette opération : « <a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\"> {desc} </a> »",
"log_help_to_get_log": "Pour voir le log de cette opération « {desc} », utiliser la commande « yunohost log display {name} »",
"log_link_to_failed_log": "Lopération « {desc} » a échouée ! Pour avoir de laide, merci <a href=\"#/tools/logs/{name}\"> de fournir le log complet de lopération</a>",
"backup_php5_to_php7_migration_may_fail": "Impossible de convertir votre archive pour prendre en charge php7, la restauration de vos applications php peut ne pas aboutir (reason: {error:s})",
"log_help_to_get_failed_log": "Lopération « {desc} » a échouée ! Pour avoir de laide, merci de partager le log de cette opération en utilisant la commande « yunohost log display {name} --share »",
"log_does_exists": "Il nexiste pas de log de lopération ayant pour nom « {log} », utiliser « yunohost log list pour voir tous les fichiers de logs disponibles »",
"log_operation_unit_unclosed_properly": "Lopération ne sest pas terminée correctement",
"log_app_addaccess": "Ajouter laccès à « {} »",
"log_app_removeaccess": "Enlever laccès à « {} »",
"log_app_clearaccess": "Retirer tous les accès à « {} »",
"log_app_fetchlist": "Ajouter une liste dapplication",
"log_app_removelist": "Enlever une liste dapplication",
"log_app_change_url": "Changer lurl de lapplication « {} »",
"log_app_install": "Installer lapplication « {} »",
"log_app_remove": "Enlever lapplication « {} »",
"log_app_upgrade": "Mettre à jour lapplication « {} »",
"log_app_makedefault": "Faire de « {} » lapplication par défaut",
"log_available_on_yunopaste": "Le log est désormais disponible via {url}",
"log_backup_restore_system": "Restaurer le système depuis une sauvegarde",
"log_backup_restore_app": "Restaurer « {} » depuis une sauvegarde",
"log_remove_on_failed_restore": "Retirer « {} » après la restauration depuis une sauvegarde qui a échouée",
"log_remove_on_failed_install": "Enlever « {} » après une installation échouée",
"log_domain_add": "Ajouter le domaine « {} » dans la configuration du système",
"log_domain_remove": "Enlever le domaine « {} » de la configuration du système",
"log_dyndns_subscribe": "Souscrire au sous-domaine « {} » de Yunohost",
"log_dyndns_update": "Mettre à jour ladresse ip associée à votre sous-domaine Yunohost « {} »",
"log_letsencrypt_cert_install": "Installer le certificat Lets encryt sur le domaine « {} »",
"log_selfsigned_cert_install": "Installer le certificat auto-signé sur le domaine « {} »",
"log_letsencrypt_cert_renew": "Renouveler le certificat Lets encrypt de « {} »",
"log_service_enable": "Activer le service « {} »",
"log_service_regen_conf": "Régénérer la configuration système de « {} »",
"log_user_create": "Ajouter lutilisateur « {} »",
"log_user_delete": "Enlever lutilisateur « {} »",
"log_user_update": "Mettre à jour les informations de lutilisateur « {} »",
"log_tools_maindomain": "Faire de « {} » le domaine principal",
"log_tools_migrations_migrate_forward": "Migrer",
"log_tools_migrations_migrate_backward": "Revenir en arrière",
"log_tools_postinstall": "Faire la post-installation du serveur Yunohost",
"log_tools_upgrade": "Mise à jour des paquets Debian",
"log_tools_shutdown": "Eteindre votre serveur",
"log_tools_reboot": "Redémarrer votre serveur",
"mail_unavailable": "Cette adresse mail est réservée et doit être automatiquement attribuée au tout premier utilisateur",
"migration_description_0004_php5_to_php7_pools": "Reconfigurez le pool PHP pour utiliser PHP 7 au lieu de 5",
"migration_description_0005_postgresql_9p4_to_9p6": "Migration des bases de données de postgresql 9.4 vers 9.6",
"migration_0005_postgresql_94_not_installed": "Postgresql na pas été installé sur votre système. Rien à faire !",
"migration_0005_postgresql_96_not_installed": "Postgresql 9.4 a été trouvé et installé, mais pas Postgresql 9.6 !? Quelque chose détrange a dû arriver à votre système :( …",
"migration_0005_not_enough_space": "Il ny a pas assez despace libre de disponible sur {path} pour lancer maintenant la migration :(.",
"recommend_to_add_first_user": "La post-installation est terminée, mais YunoHost a besoin dau moins un utilisateur pour fonctionner correctement. Vous devez en ajouter un en utilisant « yunohost user create » ou linterface dadministration.",
"service_description_php7.0-fpm": "exécute des applications écrites en PHP avec nginx",
"users_available": "Liste des utilisateurs disponibles :",
"good_practices_about_admin_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe dadministration. Le mot de passe doit comporter au moins 8 caractères bien quil soit recommandé dutiliser un mot de passe plus long (cest-à-dire une phrase de chiffrement) et/ou dutiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
"good_practices_about_user_password": "Vous êtes maintenant sur le point de définir un nouveau mot de passe utilisateur. Le mot de passe doit comporter au moins 8 caractères bien quil soit recommandé dutiliser un mot de passe plus long (cest-à-dire une phrase de chiffrement) et/ou dutiliser différents types de caractères (majuscules, minuscules, chiffres et caractères spéciaux).",
"migration_description_0006_sync_admin_and_root_passwords": "Synchroniser les mots de passe admin et root",
"migration_0006_disclaimer": "Yunohost sattend maintenant à ce que les mots de passe admin et root soient synchronisés. En exécutant cette migration, votre mot de passe root sera remplacé par le mot de passe administrateur.",
"migration_0006_done": "Votre mot de passe root a été remplacé par celui de votre adminitrateur.",
"password_listed": "Ce mot de passe est lun des mots de passe les plus utilisés dans le monde. Veuillez choisir quelque chose dun peu plus unique.",
"password_too_simple_1": "Le mot de passe doit comporter au moins 8 caractères",
"password_too_simple_2": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules et minuscules",
"password_too_simple_3": "Le mot de passe doit comporter au moins 8 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux",
"password_too_simple_4": "Le mot de passe doit comporter au moins 12 caractères et contenir des chiffres, des caractères majuscules, minuscules et spéciaux",
"root_password_desynchronized": "Le mot de passe administrateur a été changé, mais YunoHost na pas pu le propager sur le mot de passe root !"
}

View file

@ -23,7 +23,7 @@
"service_disabled": "Il servizio '{service:s}' è stato disattivato",
"service_remove_failed": "Impossibile rimuovere il servizio '{service:s}'",
"service_removed": "Il servizio '{service:s}' è stato rimosso",
"service_stop_failed": "Impossibile fermare il servizio '{service:s}'",
"service_stop_failed": "Impossibile fermare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"system_username_exists": "il nome utente esiste già negli utenti del sistema",
"unrestore_app": "L'applicazione '{app:s}' non verrà ripristinata",
"upgrading_packages": "Aggiornamento dei pacchetti...",
@ -31,11 +31,11 @@
"admin_password": "Password dell'amministrazione",
"admin_password_change_failed": "Impossibile cambiare la password",
"admin_password_changed": "La password dell'amministrazione è stata cambiata",
"app_incompatible": "L'app non è compatibile con la tua versione di Yunohost",
"app_incompatible": "L'applicazione {app} è incompatibile con la tua versione YunoHost",
"app_install_files_invalid": "Non sono validi i file di installazione",
"app_location_already_used": "Un'app è già installata in questa posizione",
"app_location_install_failed": "Impossibile installare l'applicazione in questa posizione",
"app_manifest_invalid": "Manifesto dell'applicazione non valido",
"app_location_already_used": "L'applicazione '{app}' è già installata in questo percorso ({path})",
"app_location_install_failed": "Impossibile installare l'applicazione in questo percorso perchè andrebbe in conflitto con l'applicazione '{other_app}' già installata in '{other_path}'",
"app_manifest_invalid": "Manifesto dell'applicazione non valido: {error}",
"app_no_upgrade": "Nessun applicazione da aggiornare",
"app_not_correctly_installed": "{app:s} sembra di non essere installata correttamente",
"app_not_properly_removed": "{app:s} non è stata correttamente rimossa",
@ -44,13 +44,13 @@
"app_sources_fetch_failed": "Impossibile riportare i file sorgenti",
"app_upgrade_failed": "Impossibile aggiornare {app:s}",
"app_upgraded": "{app:s} è stata aggiornata",
"appslist_fetched": "La lista delle applicazioni è stata recuperata",
"appslist_removed": "La lista delle applicazioni è stata rimossa",
"app_package_need_update": "Il pacchetto dell'app deve esser aggiornato per seguire le modifiche di Yunohost",
"app_requirements_checking": "Controllo dei pacchetti necessari...",
"app_requirements_failed": "Impossibile rispondere ai requisiti: {error}",
"app_requirements_unmeet": "Non sono soddisfatti i requisiti, il pacchetto {pkgname} ({version}) deve esser {spec}",
"appslist_unknown": "Lista di applicazioni sconosciuta",
"appslist_fetched": "La lista delle applicazioni {appslist:s} è stata recuperata",
"appslist_removed": "La lista delle applicazioni {appslist:s} è stata rimossa",
"app_package_need_update": "Il pacchetto dell'applicazione {app} deve essere aggiornato per seguire i cambiamenti di YunoHost",
"app_requirements_checking": "Controllo i pacchetti richiesti per {app}...",
"app_requirements_failed": "Impossibile soddisfare i requisiti per {app}: {error}",
"app_requirements_unmeet": "Requisiti non soddisfatti per {app}, il pacchetto {pkgname} ({version}) deve essere {spec}",
"appslist_unknown": "Lista di applicazioni {appslist:s} sconosciuta.",
"ask_current_admin_password": "Password attuale dell'amministrazione",
"ask_firstname": "Nome",
"ask_lastname": "Cognome",
@ -65,8 +65,8 @@
"app_argument_required": "L'argomento '{name:s}' è requisito",
"app_id_invalid": "Identificativo dell'applicazione non valido",
"app_unsupported_remote_type": "Il tipo remoto usato per l'applicazione non è supportato",
"appslist_retrieve_error": "Non è possibile riportare la lista remota delle applicazioni: {error}",
"appslist_retrieve_bad_format": "Il file recuperato non è una lista di applicazioni valida",
"appslist_retrieve_error": "Impossibile recuperare la lista di applicazioni remote {appslist:s}: {error:s}",
"appslist_retrieve_bad_format": "Il file recuperato per la lista di applicazioni {appslist:s} non è valido",
"backup_archive_broken_link": "Non è possibile accedere al archivio di backup (link rotto verso {path:s})",
"backup_archive_hook_not_exec": "Il hook '{hook:s}' non è stato eseguito in questo backup",
"backup_archive_name_unknown": "Archivio di backup locale chiamato '{name:s}' sconosciuto",
@ -114,7 +114,7 @@
"dyndns_no_domain_registered": "Nessuno dominio è stato registrato con DynDNS",
"dyndns_registered": "Il dominio DynDNS è stato registrato",
"dyndns_registration_failed": "Non è possibile registrare il dominio DynDNS: {error:s}",
"dyndns_unavailable": "Il sottodominio DynDNS non è disponibile",
"dyndns_unavailable": "Dominio {domain:s} non disponibile.",
"executing_command": "Esecuzione del comando '{command:s}'...",
"executing_script": "Esecuzione dello script '{script:s}'...",
"extracting": "Estrazione...",
@ -126,7 +126,7 @@
"hook_exec_failed": "L'esecuzione dello script è fallita: {path:s}",
"hook_exec_not_terminated": "L'esecuzione dello script non è stata terminata: {path:s}",
"hook_name_unknown": "Nome di hook '{name:s}' sconosciuto",
"installation_complete": "Installazione finita",
"installation_complete": "Installazione completata",
"installation_failed": "Installazione fallita",
"ip6tables_unavailable": "Non puoi giocare con ip6tables qui. O sei in un container o il tuo kernel non lo supporta",
"iptables_unavailable": "Non puoi giocare con iptables qui. O sei in un container o il tuo kernel non lo supporta",
@ -185,7 +185,7 @@
"network_check_smtp_ok": "La posta in uscita (SMTP porta 25) non è bloccata",
"no_restore_script": "Nessuno script di ripristino trovato per l'applicazone '{app:s}'",
"package_unexpected_error": "Un'errore inaspettata si è verificata durante il trattamento del pacchetto '{pkgname}'",
"restore_hook_unavailable": "Il hook di ripristino '{hook:s}' non è disponibile sul tuo sistema",
"restore_hook_unavailable": "Lo script di ripristino per '{part:s}' non è disponibile per il tuo sistema e non è nemmeno nell'archivio",
"restore_nothings_done": "Non è stato ripristinato nulla",
"restore_running_app_script": "Esecuzione dello script di ripristino dell'applcicazione '{app:s}'...",
"restore_running_hooks": "Esecuzione dei hook di ripristino...",
@ -203,14 +203,14 @@
"service_conf_up_to_date": "La configurazione è già aggiornata per il servizio '{service}'",
"service_conf_updated": "La configurazione è stata aggiornata per il servizio '{service}'",
"service_conf_would_be_updated": "La configurazione sarebbe stata aggiornata per il servizio '{service}'",
"service_disable_failed": "Impossibile disattivare il servizio '{service:s}'",
"service_enable_failed": "Impossibile attivare il servizio '{service:s}'",
"service_disable_failed": "Impossibile disabilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enable_failed": "Impossibile abilitare il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_enabled": "Il servizio '{service:s}' è stato attivato",
"service_no_log": "Nessuno registro da visualizzare per il servizio '{service:s}'",
"service_regenconf_dry_pending_applying": "Verificazione della configurazione in attesa che sarebbe stata applicata per il servizio '{service}'...",
"service_regenconf_failed": "Impossibile rigenerare la configurazione per il/i servizio/i: {services}",
"service_regenconf_pending_applying": "Applicazione della configurazione in attesa per il servizio '{service}'...",
"service_start_failed": "Impossibile avviare il servizio '{service:s}'",
"service_start_failed": "Impossibile eseguire il servizio '{service:s}'\n\nRegistri di servizio recenti:{logs:s}",
"service_started": "Il servizio '{service:s}' è stato avviato",
"service_status_failed": "Impossibile determinare lo stato del servizio '{service:s}'",
"service_stopped": "Il servizio '{service:s}' è stato fermato",
@ -250,5 +250,53 @@
"certmanager_certificate_fetching_or_enabling_failed": "L'attivazione del nuovo certificato per {domain:s} sembra fallita in qualche modo...",
"certmanager_attempt_to_renew_nonLE_cert": "Il certificato per il dominio {domain:s} non è emesso da Let's Encrypt. Impossibile rinnovarlo automaticamente!",
"certmanager_attempt_to_renew_valid_cert": "Il certificato per il dominio {domain:s} non è a scadere! Usa --force per ignorare",
"certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx"
"certmanager_domain_http_not_working": "Sembra che non sia possibile accedere al dominio {domain:s} attraverso HTTP. Verifica la configurazione del DNS e di nginx",
"app_already_installed_cant_change_url": "Questa applicazione è già installata. L'URL non può essere cambiato solo da questa funzione. Guarda se `app changeurl` è disponibile.",
"app_already_up_to_date": "{app:s} è già aggiornata",
"app_change_no_change_url_script": "L'applicazione {app_name:s} non supporta ancora il cambio del proprio URL, potrebbe essere necessario aggiornarla.",
"app_change_url_failed_nginx_reload": "Riavvio di nginx fallito. Questo è il risultato di 'nginx -t':\n{nginx_errors:s}",
"app_change_url_identical_domains": "Il vecchio ed il nuovo dominio/percorso_url sono identici ('{domain:s}{path:s}'), nessuna operazione necessaria.",
"app_change_url_no_script": "L'applicazione '{app_name:s}' non supporta ancora la modifica dell'URL. Forse dovresti aggiornare l'applicazione.",
"app_change_url_success": "URL dell'applicazione {app:s} cambiato con successo in {domain:s}{path:s}",
"app_make_default_location_already_used": "Impostazione dell'applicazione '{app}' come predefinita del dominio {domain} non riuscita perchè è già stata impostata per l'altra applicazione '{other_app}'",
"app_location_unavailable": "Questo URL non è disponibile o va in conflitto con la/le applicazione/i già installata/e:\n{apps:s}",
"app_upgrade_app_name": "Aggiornando l'applicazione {app}...",
"app_upgrade_some_app_failed": "Impossibile aggiornare alcune applicazioni",
"appslist_corrupted_json": "Caricamento della lista delle applicazioni non riuscita. Sembra che {filename:s} sia corrotto.",
"appslist_could_not_migrate": "Migrazione della lista delle applicazioni {appslist:s} non riuscita! Impossibile analizzare l'URL... La vecchia operazione pianificata è stata tenuta in {bkp_file:s}.",
"appslist_migrating": "Migrando la lista di applicazioni {appslist:s} ...",
"appslist_name_already_tracked": "C'è già una lista di applicazioni registrata con il nome {name:s}.",
"appslist_url_already_tracked": "C'è già una lista di applicazioni registrata con URL {url:s}.",
"ask_path": "Percorso",
"backup_abstract_method": "Questo metodo di backup non è ancora stato implementato",
"backup_applying_method_borg": "Inviando tutti i file da salvare nel backup nel deposito borg-backup...",
"backup_applying_method_copy": "Copiando tutti i files nel backup...",
"backup_applying_method_custom": "Chiamando il metodo di backup personalizzato '{method:s}'...",
"backup_applying_method_tar": "Creando l'archivio tar del backup...",
"backup_archive_mount_failed": "Montaggio dell'archivio del backup non riuscito",
"backup_archive_system_part_not_available": "La parte di sistema '{part:s}' non è disponibile in questo backup",
"backup_archive_writing_error": "Impossibile aggiungere i file al backup nell'archivio compresso",
"backup_ask_for_copying_if_needed": "Alcuni files non possono essere preparati al backup utilizzando il metodo che consente di evitare il consumo temporaneo di spazio nel sistema. Per eseguire il backup, {size:s}MB dovranno essere utilizzati temporaneamente. Sei d'accordo?",
"backup_borg_not_implemented": "Il metodo di backup Borg non è ancora stato implementato",
"backup_cant_mount_uncompress_archive": "Impossibile montare in modalità sola lettura la cartella di archivio non compressa",
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB per organizzare l'archivio",
"backup_couldnt_bind": "Impossibile legare {src:s} a {dest:s}.",
"backup_csv_addition_failed": "Impossibile aggiungere file del backup nel file CSV",
"backup_csv_creation_failed": "Impossibile creare il file CVS richiesto per le future operazioni di ripristino",
"backup_custom_backup_error": "Il metodo di backup personalizzato è fallito allo step 'backup'",
"backup_custom_mount_error": "Il metodo di backup personalizzato è fallito allo step 'mount'",
"backup_custom_need_mount_error": "Il metodo di backup personalizzato è fallito allo step 'need_mount'",
"backup_method_borg_finished": "Backup in borg terminato",
"backup_method_copy_finished": "Copia di backup terminata",
"backup_method_custom_finished": "Metodo di backup personalizzato '{method:s}' terminato",
"backup_method_tar_finished": "Archivio tar di backup creato",
"backup_no_uncompress_archive_dir": "La cartella di archivio non compressa non esiste",
"backup_php5_to_php7_migration_may_fail": "Conversione del tuo archivio per supportare php7 non riuscita, le tue app php potrebbero fallire in fase di ripristino (motivo: {error:s})",
"backup_system_part_failed": "Impossibile creare il backup della parte di sistema '{part:s}'",
"backup_unable_to_organize_files": "Impossibile organizzare i file nell'archivio con il metodo veloce",
"backup_with_no_backup_script_for_app": "L'app {app:s} non ha script di backup. Ignorata.",
"backup_with_no_restore_script_for_app": "L'app {app:s} non ha script di ripristino, non sarai in grado di ripristinarla automaticamente dal backup di questa app.",
"certmanager_acme_not_configured_for_domain": "Il certificato per il dominio {domain:s} non sembra essere correttamente installato. Per favore esegui cert-install per questo dominio prima.",
"certmanager_cannot_read_cert": "Qualcosa è andato storto nel tentativo di aprire il certificato attuale per il dominio {domain:s} (file: {file:s}), motivo: {reason:s}",
"certmanager_cert_install_success": "Certificato Let's Encrypt per il dominio {domain:s} installato con successo!"
}

View file

@ -71,7 +71,7 @@
"app_change_url_no_script": "Laplicacion {app_name:s} pren pas en compte lo cambiament dURL, benlèu que vos cal la metre a jorn.",
"app_make_default_location_already_used": "Impossible de configurar laplicacion « {app} »per defaut pel domeni {domain} perque es ja utilizat per laplicacion {other_app}",
"app_location_install_failed": "Impossible dinstallar laplicacion a aqueste emplaçament per causa de conflicte amb laplicacion {other_app} ques ja installada sus {other_path}",
"app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta",
"app_location_unavailable": "Aquesta URL es pas disponibla o en conflicte amb una aplicacion existenta:\n{apps:s}",
"appslist_corrupted_json": "Cargament impossible de la lista daplicacion. Sembla que {filename:s} siá gastat.",
"backup_delete_error": "Impossible de suprimir « {path:s} »",
"backup_deleted": "La salvagarda es estada suprimida",
@ -402,5 +402,69 @@
"backup_archive_writing_error": "Impossible dajustar los fichièrs a la salvagarda dins larchiu comprimit",
"backup_cant_mount_uncompress_archive": "Impossible de montar en lectura sola lo repertòri de larchiu descomprimit",
"backup_no_uncompress_archive_dir": "Lo repertòri de larchiu descomprimit existís pas",
"pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses"
"pattern_username": "Deu èsser compausat solament de caractèrs alfanumerics en letras minusculas e de tirets basses",
"experimental_feature": "Atencion: aquesta foncionalitat es experimentala e deu pas èsser considerada coma establa, deuriatz pas lutilizar levat que sapiatz çò que fasètz.",
"log_corrupted_md_file": "Lo fichièr yaml de metadonada amb los jornals daudit es corromput: « {md_file} »",
"log_category_404": "La categoria de jornals daudit « {category} » existís pas",
"log_link_to_log": "Jornal complèt daquesta operacion: <a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>",
"log_help_to_get_log": "Per veire lo jornal daquesta operacion « {desc} », utilizatz la comanda «yunohost log display {name} »",
"backup_php5_to_php7_migration_may_fail": "Impossible de convertir vòstre archiu per prendre en carga PHP 7, la restauracion de vòstras aplicacions PHP pòt reüssir pas (rason: {error:s})",
"log_link_to_failed_log": "Loperacion « {desc} » a pas capitat! Per obténer dajuda, mercés <a href=\"#/tools/logs/{name}\"> de fornir lo jornal complèt de loperacion</a>",
"log_help_to_get_failed_log": "Loperacion « {desc} » a pas reüssit! Per obténer dajuda, mercés de partejar lo jornal daudit complèt daquesta operacion en utilizant la comanda «yunohost log display {name} --share»",
"log_does_exists": "I a pas cap de jornal daudit per loperacion amb lo nom « {log} », utilizatz «yunohost log list» per veire totes los jornals doperacion disponibles",
"log_operation_unit_unclosed_properly": "Loperacion a pas acabat corrèctament",
"log_app_addaccess": "Ajustar laccès a « {} »",
"log_app_removeaccess": "Tirar laccès a « {} »",
"log_app_clearaccess": "Tirar totes los accèsses a « {} »",
"log_app_fetchlist": "Ajustar una lista daplicacions",
"log_app_removelist": "Levar una lista daplicacions",
"log_app_change_url": "Cambiar lURL de laplicacion « {} »",
"log_app_install": "Installar laplicacion « {} »",
"log_app_remove": "Levar laplicacion « {} »",
"log_app_upgrade": "Metre a jorn laplicacion « {} »",
"log_app_makedefault": "Far venir « {} » laplicacion per defaut",
"log_available_on_yunopaste": "Lo jornal es ara disponible via {url}",
"log_backup_restore_system": "Restaurar lo sistèma a partir duna salvagarda",
"log_backup_restore_app": "Restaurar « {} » a partir duna salvagarda",
"log_remove_on_failed_restore": "Levar « {} » aprèp un fracàs de restauracion a partir duna salvagarda",
"log_remove_on_failed_install": "Tirar « {} » aprèp una installacion pas reüssida",
"log_domain_add": "Ajustar lo domeni « {} » dins la configuracion sistèma",
"log_domain_remove": "Tirar lo domeni « {} » da la configuracion sistèma",
"log_dyndns_subscribe": "Sabonar al subdomeni YunoHost « {} »",
"log_dyndns_update": "Metre a jorn ladreça IP ligada a vòstre jos-domeni YunoHost « {} »",
"log_letsencrypt_cert_install": "Installar lo certificat Let's encrypt sul domeni « {} »",
"log_selfsigned_cert_install": "Installar lo certificat auto-signat sul domeni « {} »",
"log_letsencrypt_cert_renew": "Renovar lo certificat Let's encrypt de « {} »",
"log_service_enable": "Activar lo servici « {} »",
"log_service_regen_conf": "Regenerar la configuracion sistèma de « {} »",
"log_user_create": "Ajustar lutilizaire « {} »",
"log_user_delete": "Levar lutilizaire « {} »",
"log_user_update": "Metre a jorn las informacions a lutilizaire « {} »",
"log_tools_maindomain": "Far venir « {} » lo domeni màger",
"log_tools_migrations_migrate_forward": "Migrar",
"log_tools_migrations_migrate_backward": "Tornar en arrièr",
"log_tools_postinstall": "Realizar la post installacion del servidor YunoHost",
"log_tools_upgrade": "Mesa a jorn dels paquets Debian",
"log_tools_shutdown": "Atudar lo servidor",
"log_tools_reboot": "Reaviar lo servidor",
"mail_unavailable": "Aquesta adreça electronica es reservada e deu èsser automaticament atribuida al tot bèl just primièr utilizaire",
"migration_description_0004_php5_to_php7_pools": "Tornar configurar lo pools PHP per utilizar PHP 7 allòc del 5",
"migration_description_0005_postgresql_9p4_to_9p6": "Migracion de las basas de donadas de postgresql 9.4 cap a 9.6",
"migration_0005_postgresql_94_not_installed": "Postgresql es pas installat sul sistèma. Pas res de far!",
"migration_0005_postgresql_96_not_installed": "Avèm trobat que Postgresql 9.4 es installat, mas cap de version de Postgresql 9.6 pas trobada!? Quicòm destranh a degut arribar a vòstre sistèma :( …",
"migration_0005_not_enough_space": "I a pas pro despaci disponible sus {path} per lançar la migracion daquela passa :(.",
"recommend_to_add_first_user": "La post installacion es acabada, mas YunoHost fa besonh dalmens un utilizaire per foncionar coma cal. Vos cal najustar un en utilizant la comanda «yunohost user create» o ben linterfàcia dadministracion.",
"service_description_php7.0-fpm": "executa daplicacions escrichas en PHP amb nginx",
"users_available": "Lista dels utilizaires disponibles:",
"good_practices_about_admin_password": "Sètz per definir un nòu senhal per ladministracion. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far dutilizar un senhal mai long quaquò (ex. una passafrasa) e/o dutilizar mantun tipes de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
"good_practices_about_user_password": "Sètz per definir un nòu senhal dutilizaire. Lo senhal deu almens conténer 8 caractèrs - encara que siá de bon far dutilizar un senhal mai long quaquò (ex. una passafrasa) e/o dutilizar mantun tipes de caractèrs (majuscula, minuscula, nombre e caractèrs especials).",
"migration_description_0006_sync_admin_and_root_passwords": "Sincronizar los senhals admin e root",
"migration_0006_disclaimer": "Ara YunoHost sespèra que los senhals admin e root sián sincronizats. En lançant aquesta migracion, vòstre senhal root serà remplaçat pel senhal admin.",
"migration_0006_done": "Lo senhal root es estat remplaçat pel senhal admin.",
"password_listed": "Aqueste senhal fa part dels senhals mai utilizats del monde. Volgatz ben ne causir un mai unic.",
"password_too_simple_1": "Lo senhal deu conténer almens 8 caractèrs",
"password_too_simple_2": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas e de minusculas",
"password_too_simple_3": "Lo senhal deu conténer almens 8 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials",
"password_too_simple_4": "Lo senhal deu conténer almens 12 caractèrs amb de nombres, de majusculas, de minusculas e de caractèrs especials",
"root_password_desynchronized": "Lo senhal de ladministrator es estat cambiat, mas YunoHost a pas pogut lespandir al senhal root!"
}

View file

@ -192,5 +192,6 @@
"backup_ask_for_copying_if_needed": "Alguns arquivos não consiguiram ser preparados para backup utilizando o metodo que não gasta espaço de disco temporariamente. Para realizar o backup {size:s}MB precisam ser usados temporariamente. Você concorda?",
"backup_borg_not_implemented": "O método de backup Borg ainda não foi implementado.",
"backup_cant_mount_uncompress_archive": "Não foi possível montar em modo leitura o diretorio de arquivos não comprimido",
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo"
"backup_copying_to_organize_the_archive": "Copiando {size:s}MB para organizar o arquivo",
"app_change_url_identical_domains": "O antigo e o novo domínio / url_path são idênticos ('{domain:s}{path:s}'), nada para fazer."
}

View file

@ -30,15 +30,15 @@ import yaml
import time
import re
import urlparse
import errno
import subprocess
import glob
import pwd
import grp
from collections import OrderedDict
from datetime import datetime
from moulinette import msignals, m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_json
@ -80,6 +80,12 @@ def app_listlists():
# Get the list
appslist_list = _read_appslist_list()
# Convert 'lastUpdate' timestamp to datetime
for name, infos in appslist_list.items():
if infos["lastUpdate"] is None:
infos["lastUpdate"] = 0
infos["lastUpdate"] = datetime.utcfromtimestamp(infos["lastUpdate"])
return appslist_list
@ -118,14 +124,12 @@ def app_fetchlist(url=None, name=None):
appslists_to_be_fetched = [name]
operation_logger.success()
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('custom_appslist_name_required'))
raise YunohostError('custom_appslist_name_required')
# If a name is given, look for an appslist with that name and fetch it
elif name is not None:
if name not in appslists.keys():
raise MoulinetteError(errno.EINVAL,
m18n.n('appslist_unknown', appslist=name))
raise YunohostError('appslist_unknown', appslist=name)
else:
appslists_to_be_fetched = [name]
@ -168,7 +172,7 @@ def app_fetchlist(url=None, name=None):
appslist = appslist_request.text
try:
json.loads(appslist)
except ValueError, e:
except ValueError as e:
logger.error(m18n.n('appslist_retrieve_bad_format',
appslist=name))
continue
@ -179,9 +183,7 @@ def app_fetchlist(url=None, name=None):
with open(list_file, "w") as f:
f.write(appslist)
except Exception as e:
raise MoulinetteError(errno.EIO,
"Error while writing appslist %s: %s" %
(name, str(e)))
raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), raw_msg=True)
now = int(time.time())
appslists[name]["lastUpdate"] = now
@ -205,7 +207,7 @@ def app_removelist(operation_logger, name):
# Make sure we know this appslist
if name not in appslists.keys():
raise MoulinetteError(errno.ENOENT, m18n.n('appslist_unknown', appslist=name))
raise YunohostError('appslist_unknown', appslist=name)
operation_logger.start()
@ -336,8 +338,7 @@ def app_info(app, show_status=False, raw=False):
"""
if not _is_installed(app):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
app_setting_path = APPS_SETTING_PATH + app
@ -346,7 +347,8 @@ def app_info(app, show_status=False, raw=False):
ret['settings'] = _get_app_settings(app)
# Determine upgradability
local_update_time = ret['settings'].get('update_time', ret['settings']['install_time'])
# In case there is neither update_time nor install_time, we assume the app can/has to be upgraded
local_update_time = ret['settings'].get('update_time', ret['settings'].get('install_time', 0))
if 'lastUpdate' not in ret or 'git' not in ret:
upgradable = "url_required"
@ -394,8 +396,7 @@ def app_map(app=None, raw=False, user=None):
if app is not None:
if not _is_installed(app):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
apps = [app, ]
else:
apps = os.listdir(APPS_SETTING_PATH)
@ -447,11 +448,10 @@ def app_change_url(operation_logger, auth, app, domain, path):
installed = _is_installed(app)
if not installed:
raise MoulinetteError(errno.ENOPKG,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")):
raise MoulinetteError(errno.EINVAL, m18n.n("app_change_no_change_url_script", app_name=app))
raise YunohostError("app_change_no_change_url_script", app_name=app)
old_domain = app_setting(app, "domain")
old_path = app_setting(app, "path")
@ -463,7 +463,7 @@ def app_change_url(operation_logger, auth, app, domain, path):
path = normalize_url_path(path)
if (domain, path) == (old_domain, old_path):
raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_identical_domains", domain=domain, path=path))
raise YunohostError("app_change_url_identical_domains", domain=domain, path=path)
# WARNING / FIXME : checkurl will modify the settings
# (this is a non intuitive behavior that should be changed)
@ -539,7 +539,7 @@ def app_change_url(operation_logger, auth, app, domain, path):
stderr=subprocess.STDOUT,
shell=True).rstrip()
raise MoulinetteError(errno.EINVAL, m18n.n("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors))
raise YunohostError("app_change_url_failed_nginx_reload", nginx_errors=nginx_errors)
logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path))
@ -564,8 +564,8 @@ def app_upgrade(auth, app=[], url=None, file=None):
try:
app_list()
except MoulinetteError:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade'))
except YunohostError:
raise YunohostError('app_no_upgrade')
upgraded_apps = []
@ -585,8 +585,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name))
installed = _is_installed(app_instance_name)
if not installed:
raise MoulinetteError(errno.ENOPKG,
m18n.n('app_not_installed', app=app_instance_name))
raise YunohostError('app_not_installed', app=app_instance_name)
if app_instance_name in upgraded_apps:
continue
@ -676,7 +675,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
operation_logger.success()
if not upgraded_apps:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade'))
raise YunohostError('app_no_upgrade')
app_ssowatconf(auth)
@ -688,7 +687,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
@is_unit_operation()
def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False):
def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on_failure=False, force=False):
"""
Install apps
@ -697,12 +696,11 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
label -- Custom name for the app
args -- Serialize arguments for app installation
no_remove_on_failure -- Debug option to avoid removing the app on a failed installation
force -- Do not ask for confirmation when installing experimental / low-quality apps
"""
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.log import OperationLogger
# Fetch or extract sources
try:
os.listdir(INSTALL_TMP)
@ -717,17 +715,46 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
},
}
if app in app_list(raw=True) or ('@' in app) or ('http://' in app) or ('https://' in app):
def confirm_install(confirm):
# Ignore if there's nothing for confirm (good quality app), if --force is used
# or if request on the API (confirm already implemented on the API side)
if confirm is None or force or msettings.get('interface') == 'api':
return
answer = msignals.prompt(m18n.n('confirm_app_install_' + confirm,
answers='Y/N'))
if answer.upper() != "Y":
raise YunohostError("aborting")
raw_app_list = app_list(raw=True)
if app in raw_app_list or ('@' in app) or ('http://' in app) or ('https://' in app):
if app in raw_app_list:
state = raw_app_list[app].get("state", "notworking")
level = raw_app_list[app].get("level", None)
confirm = "danger"
if state in ["working", "validated"]:
if isinstance(level, int) and level >= 3:
confirm = None
elif isinstance(level, int) and level > 0:
confirm = "warning"
else:
confirm = "thirdparty"
confirm_install(confirm)
manifest, extracted_app_folder = _fetch_app_from_git(app)
elif os.path.exists(app):
confirm_install("thirdparty")
manifest, extracted_app_folder = _extract_app_from_file(app)
else:
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown'))
raise YunohostError('app_unknown')
status['remote'] = manifest.get('remote', {})
# Check ID
if 'id' not in manifest or '__' in manifest['id']:
raise MoulinetteError(errno.EINVAL, m18n.n('app_id_invalid'))
raise YunohostError('app_id_invalid')
app_id = manifest['id']
@ -738,8 +765,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
instance_number = _installed_instance_number(app_id, last=True) + 1
if instance_number > 1:
if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']):
raise MoulinetteError(errno.EEXIST,
m18n.n('app_already_installed', app=app_id))
raise YunohostError('app_already_installed', app=app_id)
# Change app_id to the forked app id
app_instance_name = app_id + '__' + str(instance_number)
@ -760,7 +786,7 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
# Start register change on system
operation_logger.extra.update({'env':env_dict})
operation_logger.extra.update({'env': env_dict})
operation_logger.related_to = [s for s in operation_logger.related_to if s[0] != "app"]
operation_logger.related_to.append(("app", app_id))
operation_logger.start()
@ -842,9 +868,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
if install_retcode == -1:
msg = m18n.n('operation_interrupted') + " " + error_msg
raise MoulinetteError(errno.EINTR, msg)
raise YunohostError(msg, raw_msg=True)
msg = error_msg
raise MoulinetteError(errno.EIO, msg)
raise YunohostError(msg, raw_msg=True)
# Clean hooks and add new ones
hook_remove(app_instance_name)
@ -880,8 +906,7 @@ def app_remove(operation_logger, auth, app):
"""
from yunohost.hook import hook_exec, hook_remove, hook_callback
if not _is_installed(app):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
operation_logger.start()
@ -947,7 +972,6 @@ def app_addaccess(auth, apps, users=[]):
for app in apps:
app_settings = _get_app_settings(app)
if not app_settings:
continue
@ -960,18 +984,18 @@ def app_addaccess(auth, apps, users=[]):
# Start register change on system
related_to = [('app', app)]
operation_logger= OperationLogger('app_addaccess', related_to)
operation_logger = OperationLogger('app_addaccess', related_to)
operation_logger.start()
allowed_users = set()
if 'allowed_users' in app_settings:
if 'allowed_users' in app_settings and app_settings['allowed_users']:
allowed_users = set(app_settings['allowed_users'].split(','))
for allowed_user in users:
if allowed_user not in allowed_users:
try:
user_info(auth, allowed_user)
except MoulinetteError:
except YunohostError:
logger.warning(m18n.n('user_unknown', user=allowed_user))
continue
allowed_users.add(allowed_user)
@ -1023,7 +1047,7 @@ def app_removeaccess(auth, apps, users=[]):
# Start register change on system
related_to = [('app', app)]
operation_logger= OperationLogger('app_removeaccess', related_to)
operation_logger = OperationLogger('app_removeaccess', related_to)
operation_logger.start()
if remove_all:
@ -1035,9 +1059,9 @@ def app_removeaccess(auth, apps, users=[]):
else:
for allowed_user in user_list(auth)['users'].keys():
if allowed_user not in users:
allowed_users.append(allowed_user)
allowed_users.add(allowed_user)
operation_logger.related_to += [ ('user', x) for x in allowed_users ]
operation_logger.related_to += [('user', x) for x in allowed_users]
operation_logger.flush()
new_users = ','.join(allowed_users)
app_setting(app, 'allowed_users', new_users)
@ -1072,7 +1096,7 @@ def app_clearaccess(auth, apps):
# Start register change on system
related_to = [('app', app)]
operation_logger= OperationLogger('app_clearaccess', related_to)
operation_logger = OperationLogger('app_clearaccess', related_to)
operation_logger.start()
if 'mode' in app_settings:
@ -1129,23 +1153,19 @@ def app_makedefault(operation_logger, auth, app, domain=None):
if domain is None:
domain = app_domain
operation_logger.related_to.append(('domain',domain))
operation_logger.related_to.append(('domain', domain))
elif domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
raise YunohostError('domain_unknown')
operation_logger.start()
if '/' in app_map(raw=True)[domain]:
raise MoulinetteError(errno.EEXIST,
m18n.n('app_make_default_location_already_used',
app=app, domain=app_domain,
other_app=app_map(raw=True)[domain]["/"]["id"]))
raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"])
try:
with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('ssowat_persistent_conf_read_error', error=e.strerror))
raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror)
except IOError:
ssowat_conf = {}
@ -1158,8 +1178,7 @@ def app_makedefault(operation_logger, auth, app, domain=None):
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e:
raise MoulinetteError(errno.EPERM,
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror)
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
@ -1211,8 +1230,7 @@ def app_checkport(port):
if tools_port_available(port):
logger.success(m18n.n('port_available', port=int(port)))
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('port_unavailable', port=int(port)))
raise YunohostError('port_unavailable', port=int(port))
def app_register_url(auth, app, domain, path):
@ -1227,7 +1245,7 @@ def app_register_url(auth, app, domain, path):
# This line can't be moved on top of file, otherwise it creates an infinite
# loop of import with tools.py...
from domain import _get_conflicting_apps, _normalize_domain_path
from .domain import _get_conflicting_apps, _normalize_domain_path
domain, path = _normalize_domain_path(domain, path)
@ -1239,8 +1257,7 @@ def app_register_url(auth, app, domain, path):
if installed:
settings = _get_app_settings(app)
if "path" in settings.keys() and "domain" in settings.keys():
raise MoulinetteError(errno.EINVAL,
m18n.n('app_already_installed_cant_change_url'))
raise YunohostError('app_already_installed_cant_change_url')
# Check the url is available
conflicts = _get_conflicting_apps(auth, domain, path)
@ -1254,7 +1271,7 @@ def app_register_url(auth, app, domain, path):
app_label=app_label,
))
raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps)))
raise YunohostError('app_location_unavailable', apps="\n".join(apps))
app_setting(app, 'domain', value=domain)
app_setting(app, 'path', value=path)
@ -1292,7 +1309,7 @@ def app_checkurl(auth, url, app=None):
apps_map = app_map(raw=True)
if domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
raise YunohostError('domain_unknown')
if domain in apps_map:
# Loop through apps
@ -1302,14 +1319,10 @@ def app_checkurl(auth, url, app=None):
installed = True
continue
if path == p:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_location_already_used',
app=a["id"], path=path))
raise YunohostError('app_location_already_used', app=a["id"], path=path)
# can't install "/a/b/" if "/a/" exists
elif path.startswith(p) or p.startswith(path):
raise MoulinetteError(errno.EPERM,
m18n.n('app_location_install_failed',
other_path=p, other_app=a['id']))
raise YunohostError('app_location_install_failed', other_path=p, other_app=a['id'])
if app is not None and not installed:
app_setting(app, 'domain', value=domain)
@ -1341,10 +1354,10 @@ def app_initdb(user, password=None, db=None, sql=None):
mysql_root_pwd = open('/etc/yunohost/mysql').read().rstrip()
mysql_command = 'mysql -u root -p%s -e "CREATE DATABASE %s ; GRANT ALL PRIVILEGES ON %s.* TO \'%s\'@localhost IDENTIFIED BY \'%s\';"' % (mysql_root_pwd, db, db, user, password)
if os.system(mysql_command) != 0:
raise MoulinetteError(errno.EIO, m18n.n('mysql_db_creation_failed'))
raise YunohostError('mysql_db_creation_failed')
if sql is not None:
if os.system('mysql -u %s -p%s %s < %s' % (user, password, db, sql)) != 0:
raise MoulinetteError(errno.EIO, m18n.n('mysql_db_init_failed'))
raise YunohostError('mysql_db_init_failed')
if return_pwd:
return password
@ -1450,8 +1463,7 @@ def app_ssowatconf(auth):
def app_change_label(auth, app, new_label):
installed = _is_installed(app)
if not installed:
raise MoulinetteError(errno.ENOPKG,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
app_setting(app, "label", value=new_label)
@ -1487,7 +1499,7 @@ def app_action_run(app, action, args=None):
actions = {x["id"]: x for x in actions}
if action not in actions:
raise MoulinetteError(errno.EINVAL, "action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys())))
raise YunohostError("action '%s' not available for app '%s', available actions are: %s" % (action, app, ", ".join(actions.keys())), raw_msg=True)
action_declaration = actions[action]
@ -1525,7 +1537,7 @@ def app_action_run(app, action, args=None):
)
if retcode not in action_declaration.get("accepted_return_codes", [0]):
raise MoulinetteError(retcode, "Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode))
raise YunohostError("Error while executing action '%s' of app '%s': return code %s" % (action, app, retcode), raw_msg=True)
os.remove(path)
@ -1632,8 +1644,7 @@ def app_config_apply(app, args):
installed = _is_installed(app)
if not installed:
raise MoulinetteError(errno.ENOPKG,
m18n.n('app_not_installed', app=app))
raise YunohostError('app_not_installed', app=app)
config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json')
config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config')
@ -1691,8 +1702,7 @@ def _get_app_settings(app_id):
"""
if not _is_installed(app_id):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_not_installed', app=app_id))
raise YunohostError('app_not_installed', app=app_id)
try:
with open(os.path.join(
APPS_SETTING_PATH, app_id, 'settings.yml')) as f:
@ -1730,7 +1740,7 @@ def _get_app_status(app_id, format_date=False):
"""
app_setting_path = APPS_SETTING_PATH + app_id
if not os.path.isdir(app_setting_path):
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown'))
raise YunohostError('app_unknown')
status = {}
try:
@ -1754,8 +1764,7 @@ def _get_app_status(app_id, format_date=False):
if not v:
status[f] = '-'
else:
status[f] = time.strftime(m18n.n('format_datetime_short'),
time.gmtime(v))
status[f] = datetime.utcfromtimestamp(v)
return status
@ -1796,7 +1805,7 @@ def _extract_app_from_file(path, remove=False):
extract_result = 1
if extract_result != 0:
raise MoulinetteError(errno.EINVAL, m18n.n('app_extraction_failed'))
raise YunohostError('app_extraction_failed')
try:
extracted_app_folder = APP_TMP_FOLDER
@ -1807,10 +1816,9 @@ def _extract_app_from_file(path, remove=False):
manifest = json.loads(str(json_manifest.read()))
manifest['lastUpdate'] = int(time.time())
except IOError:
raise MoulinetteError(errno.EIO, m18n.n('app_install_files_invalid'))
raise YunohostError('app_install_files_invalid')
except ValueError as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_manifest_invalid', error=e.strerror))
raise YunohostError('app_manifest_invalid', error=e.strerror)
logger.debug(m18n.n('done'))
@ -1878,8 +1886,7 @@ def _fetch_app_from_git(app):
'wget', '-qO', app_tmp_archive, tarball_url])
except subprocess.CalledProcessError:
logger.exception('unable to download %s', tarball_url)
raise MoulinetteError(errno.EIO,
m18n.n('app_sources_fetch_failed'))
raise YunohostError('app_sources_fetch_failed')
else:
manifest, extracted_app_folder = _extract_app_from_file(
app_tmp_archive, remove=True)
@ -1902,11 +1909,9 @@ def _fetch_app_from_git(app):
with open(extracted_app_folder + '/manifest.json') as f:
manifest = json.loads(str(f.read()))
except subprocess.CalledProcessError:
raise MoulinetteError(errno.EIO,
m18n.n('app_sources_fetch_failed'))
raise YunohostError('app_sources_fetch_failed')
except ValueError as e:
raise MoulinetteError(errno.EIO,
m18n.n('app_manifest_invalid', error=e.strerror))
raise YunohostError('app_manifest_invalid', error=e.strerror)
else:
logger.debug(m18n.n('done'))
@ -1926,11 +1931,10 @@ def _fetch_app_from_git(app):
app_info['manifest']['lastUpdate'] = app_info['lastUpdate']
manifest = app_info['manifest']
else:
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown'))
raise YunohostError('app_unknown')
if 'git' not in app_info:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_unsupported_remote_type'))
raise YunohostError('app_unsupported_remote_type')
url = app_info['git']['url']
if 'github.com' in url:
@ -1942,8 +1946,7 @@ def _fetch_app_from_git(app):
'wget', '-qO', app_tmp_archive, tarball_url])
except subprocess.CalledProcessError:
logger.exception('unable to download %s', tarball_url)
raise MoulinetteError(errno.EIO,
m18n.n('app_sources_fetch_failed'))
raise YunohostError('app_sources_fetch_failed')
else:
manifest, extracted_app_folder = _extract_app_from_file(
app_tmp_archive, remove=True)
@ -1959,11 +1962,9 @@ def _fetch_app_from_git(app):
with open(extracted_app_folder + '/manifest.json') as f:
manifest = json.loads(str(f.read()))
except subprocess.CalledProcessError:
raise MoulinetteError(errno.EIO,
m18n.n('app_sources_fetch_failed'))
raise YunohostError('app_sources_fetch_failed')
except ValueError as e:
raise MoulinetteError(errno.EIO,
m18n.n('app_manifest_invalid', error=e.strerror))
raise YunohostError('app_manifest_invalid', error=e.strerror)
else:
logger.debug(m18n.n('done'))
@ -2082,7 +2083,7 @@ def _check_manifest_requirements(manifest, app_instance_name):
yunohost_req = requirements.get('yunohost', None)
if (not yunohost_req or
not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'):
raise MoulinetteError(errno.EINVAL, '{0}{1}'.format(
raise YunohostError('{0}{1}'.format(
m18n.g('colon', m18n.n('app_incompatible'), app=app_instance_name),
m18n.n('app_package_need_update', app=app_instance_name)))
elif not requirements:
@ -2095,18 +2096,15 @@ def _check_manifest_requirements(manifest, app_instance_name):
versions = packages.get_installed_version(
*requirements.keys(), strict=True, as_dict=True)
except packages.PackageException as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_requirements_failed',
error=str(e), app=app_instance_name))
raise YunohostError('app_requirements_failed', error=str(e), app=app_instance_name)
# Iterate over requirements
for pkgname, spec in requirements.items():
version = versions[pkgname]
if version not in packages.SpecifierSet(spec):
raise MoulinetteError(
errno.EINVAL, m18n.n('app_requirements_unmeet',
raise YunohostError('app_requirements_unmeet',
pkgname=pkgname, version=version,
spec=spec, app=app_instance_name))
spec=spec, app=app_instance_name)
def _parse_args_from_manifest(manifest, action, args={}, auth=None):
@ -2206,11 +2204,14 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
for domain in domain_list(auth)['domains']:
msignals.display("- {}".format(domain))
if arg_type == 'user':
elif arg_type == 'user':
msignals.display(m18n.n('users_available'))
for user in user_list(auth)['users'].keys():
msignals.display("- {}".format(user))
elif arg_type == 'password':
msignals.display(m18n.n('good_practices_about_user_password'))
try:
input_string = msignals.prompt(ask_string, is_password)
except NotImplementedError:
@ -2226,36 +2227,27 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
# Validate argument value
if (arg_value is None or arg_value == '') \
and not arg.get('optional', False):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_required', name=arg_name))
raise YunohostError('app_argument_required', name=arg_name)
elif arg_value is None:
args_dict[arg_name] = ''
continue
# Validate argument choice
if arg_choices and arg_value not in arg_choices:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_choice_invalid',
name=arg_name, choices=', '.join(arg_choices)))
raise YunohostError('app_argument_choice_invalid', name=arg_name, choices=', '.join(arg_choices))
# Validate argument type
if arg_type == 'domain':
if arg_value not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_invalid',
name=arg_name, error=m18n.n('domain_unknown')))
raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown'))
elif arg_type == 'user':
try:
user_info(auth, arg_value)
except MoulinetteError as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_invalid',
name=arg_name, error=e.strerror))
except YunohostError as e:
raise YunohostError('app_argument_invalid', name=arg_name, error=e.strerror)
elif arg_type == 'app':
if not _is_installed(arg_value):
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_invalid',
name=arg_name, error=m18n.n('app_unknown')))
raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown'))
elif arg_type == 'boolean':
if isinstance(arg_value, bool):
arg_value = 1 if arg_value else 0
@ -2265,9 +2257,10 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
elif str(arg_value).lower() in ["0", "no", "n"]:
arg_value = 0
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('app_argument_choice_invalid',
name=arg_name, choices='yes, no, y, n, 1, 0'))
raise YunohostError('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')
elif arg_type == 'password':
from yunohost.utils.password import assert_password_is_strong_enough
assert_password_is_strong_enough('user', arg_value)
args_dict[arg_name] = arg_value
# END loop over action_args...
@ -2298,7 +2291,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
app_label=app_label,
))
raise MoulinetteError(errno.EINVAL, m18n.n('app_location_unavailable', apps="\n".join(apps)))
raise YunohostError('app_location_unavailable', apps="\n".join(apps))
# (We save this normalized path so that the install script have a
# standard path format to deal with no matter what the user inputted)
@ -2418,7 +2411,7 @@ def _install_appslist_fetch_cron():
with open(cron_job_file, "w") as f:
f.write('\n'.join(cron_job))
_set_permissions(cron_job_file, "root", "root", 0755)
_set_permissions(cron_job_file, "root", "root", 0o755)
# FIXME - Duplicate from certificate.py, should be moved into a common helper
@ -2448,8 +2441,7 @@ def _read_appslist_list():
try:
appslists = json.loads(appslists_json)
except ValueError:
raise MoulinetteError(errno.EBADR,
m18n.n('appslist_corrupted_json', filename=APPSLISTS_JSON))
raise YunohostError('appslist_corrupted_json', filename=APPSLISTS_JSON)
return appslists
@ -2464,9 +2456,8 @@ def _write_appslist_list(appslist_lists):
with open(APPSLISTS_JSON, "w") as f:
json.dump(appslist_lists, f)
except Exception as e:
raise MoulinetteError(errno.EIO,
"Error while writing list of appslist %s: %s" %
(APPSLISTS_JSON, str(e)))
raise YunohostError("Error while writing list of appslist %s: %s" %
(APPSLISTS_JSON, str(e)), raw_msg=True)
def _register_new_appslist(url, name):
@ -2479,15 +2470,13 @@ def _register_new_appslist(url, name):
# Check if name conflicts with an existing list
if name in appslist_list:
raise MoulinetteError(errno.EEXIST,
m18n.n('appslist_name_already_tracked', name=name))
raise YunohostError('appslist_name_already_tracked', name=name)
# Check if url conflicts with an existing list
known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()]
if url in known_appslist_urls:
raise MoulinetteError(errno.EEXIST,
m18n.n('appslist_url_already_tracked', url=url))
raise YunohostError('appslist_url_already_tracked', url=url)
logger.debug("Registering new appslist %s at %s" % (name, url))

View file

@ -26,18 +26,18 @@
import os
import re
import json
import errno
import time
import tarfile
import shutil
import subprocess
import csv
import tempfile
from datetime import datetime
from glob import glob
from collections import OrderedDict
from moulinette import msignals, m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils import filesystem
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
@ -52,6 +52,7 @@ from yunohost.monitor import binary_to_human
from yunohost.tools import tools_postinstall
from yunohost.service import service_regen_conf
from yunohost.log import OperationLogger
from functools import reduce
BACKUP_PATH = '/home/yunohost.backup'
ARCHIVES_PATH = '%s/archives' % BACKUP_PATH
@ -63,6 +64,7 @@ logger = getActionLogger('yunohost.backup')
class BackupRestoreTargetsManager(object):
"""
BackupRestoreTargetsManager manage the targets
in BackupManager and RestoreManager
@ -176,6 +178,7 @@ class BackupRestoreTargetsManager(object):
class BackupManager():
"""
This class collect files to backup in a list and apply one or several
backup method on it.
@ -267,9 +270,9 @@ class BackupManager():
self.work_dir = os.path.join(BACKUP_PATH, 'tmp', name)
self._init_work_dir()
###########################################################################
#
# Misc helpers #
###########################################################################
#
@property
def info(self):
@ -299,7 +302,7 @@ class BackupManager():
(string) A backup name created from current date 'YYMMDD-HHMMSS'
"""
# FIXME: case where this name already exist
return time.strftime('%Y%m%d-%H%M%S')
return time.strftime('%Y%m%d-%H%M%S', time.gmtime())
def _init_work_dir(self):
"""Initialize preparation directory
@ -307,31 +310,30 @@ class BackupManager():
Ensure the working directory exists and is empty
exception:
backup_output_directory_not_empty -- (MoulinetteError) Raised if the
backup_output_directory_not_empty -- (YunohostError) Raised if the
directory was given by the user and isn't empty
(TODO) backup_cant_clean_tmp_working_directory -- (MoulinetteError)
(TODO) backup_cant_clean_tmp_working_directory -- (YunohostError)
Raised if the working directory isn't empty, is temporary and can't
be automaticcaly cleaned
(TODO) backup_cant_create_working_directory -- (MoulinetteError) Raised
(TODO) backup_cant_create_working_directory -- (YunohostError) Raised
if iyunohost can't create the working directory
"""
# FIXME replace isdir by exists ? manage better the case where the path
# exists
if not os.path.isdir(self.work_dir):
filesystem.mkdir(self.work_dir, 0750, parents=True, uid='admin')
filesystem.mkdir(self.work_dir, 0o750, parents=True, uid='admin')
elif self.is_tmp_work_dir:
logger.debug("temporary directory for backup '%s' already exists",
self.work_dir)
# FIXME May be we should clean the workdir here
raise MoulinetteError(
errno.EIO, m18n.n('backup_output_directory_not_empty'))
raise YunohostError('backup_output_directory_not_empty')
###########################################################################
#
# Backup target management #
###########################################################################
#
def set_system_targets(self, system_parts=[]):
"""
@ -381,9 +383,9 @@ class BackupManager():
logger.warning(m18n.n('backup_with_no_restore_script_for_app', app=app))
self.targets.set_result("apps", app, "Warning")
###########################################################################
#
# Management of files to backup / "The CSV" #
###########################################################################
#
def _import_to_list_to_backup(self, tmp_csv):
"""
@ -466,9 +468,9 @@ class BackupManager():
logger.error(m18n.n('backup_csv_addition_failed'))
self.csv_file.close()
###########################################################################
#
# File collection from system parts and apps #
###########################################################################
#
def collect_files(self):
"""
@ -493,7 +495,7 @@ class BackupManager():
copied here
Exceptions:
"backup_nothings_done" -- (MoulinetteError) This exception is raised if
"backup_nothings_done" -- (YunohostError) This exception is raised if
nothing has been listed.
"""
@ -506,7 +508,7 @@ class BackupManager():
if not successfull_apps and not successfull_system:
filesystem.rm(self.work_dir, True, True)
raise MoulinetteError(errno.EINVAL, m18n.n('backup_nothings_done'))
raise YunohostError('backup_nothings_done')
# Add unlisted files from backup tmp dir
self._add_to_list_to_backup('backup.csv')
@ -603,7 +605,7 @@ class BackupManager():
restore_hooks_dir = os.path.join(self.work_dir, "hooks", "restore")
if not os.path.exists(restore_hooks_dir):
filesystem.mkdir(restore_hooks_dir, mode=0750,
filesystem.mkdir(restore_hooks_dir, mode=0o750,
parents=True, uid='admin')
restore_hooks = hook_list("restore")["hooks"]
@ -669,7 +671,7 @@ class BackupManager():
logger.debug(m18n.n('backup_running_app_script', app=app))
try:
# Prepare backup directory for the app
filesystem.mkdir(tmp_app_bkp_dir, 0750, True, uid='admin')
filesystem.mkdir(tmp_app_bkp_dir, 0o750, True, uid='admin')
# Copy the app settings to be able to call _common.sh
shutil.copytree(app_setting_path, settings_dir)
@ -703,9 +705,9 @@ class BackupManager():
filesystem.rm(tmp_script, force=True)
filesystem.rm(env_dict["YNH_BACKUP_CSV"], force=True)
###########################################################################
#
# Actual backup archive creation / method management #
###########################################################################
#
def add(self, method):
"""
@ -777,6 +779,7 @@ class BackupManager():
class RestoreManager():
"""
RestoreManager allow to restore a past backup archive
@ -825,9 +828,9 @@ class RestoreManager():
self.method = BackupMethod.create(method)
self.targets = BackupRestoreTargetsManager()
###########################################################################
#
# Misc helpers #
###########################################################################
#
@property
def success(self):
@ -856,10 +859,10 @@ class RestoreManager():
self.info["system"] = self.info["hooks"]
except IOError:
logger.debug("unable to load '%s'", info_file, exc_info=1)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
raise YunohostError('backup_invalid_archive')
else:
logger.debug("restoring from backup '%s' created on %s", self.name,
time.ctime(self.info['created_at']))
datetime.utcfromtimestamp(self.info['created_at']))
def _postinstall_if_needed(self):
"""
@ -879,8 +882,7 @@ class RestoreManager():
logger.debug("unable to retrieve current_host from the backup",
exc_info=1)
# FIXME include the current_host by default ?
raise MoulinetteError(errno.EIO,
m18n.n('backup_invalid_archive'))
raise YunohostError('backup_invalid_archive')
logger.debug("executing the post-install...")
tools_postinstall(domain, 'yunohost', True)
@ -904,9 +906,9 @@ class RestoreManager():
logger.warning(m18n.n('restore_cleaning_failed'))
filesystem.rm(self.work_dir, True, True)
###########################################################################
#
# Restore target manangement #
###########################################################################
#
def set_system_targets(self, system_parts=[]):
"""
@ -982,9 +984,9 @@ class RestoreManager():
self.info['apps'].keys(),
unknown_error)
###########################################################################
#
# Archive mounting #
###########################################################################
#
def mount(self):
"""
@ -1009,8 +1011,7 @@ class RestoreManager():
subprocess.call(['rmdir', self.work_dir])
logger.debug("Unmount dir: {}".format(self.work_dir))
else:
raise MoulinetteError(errno.EIO,
m18n.n('restore_removing_tmp_dir_failed'))
raise YunohostError('restore_removing_tmp_dir_failed')
elif os.path.isdir(self.work_dir):
logger.debug("temporary restore directory '%s' already exists",
self.work_dir)
@ -1018,8 +1019,7 @@ class RestoreManager():
if ret == 0:
logger.debug("Delete dir: {}".format(self.work_dir))
else:
raise MoulinetteError(errno.EIO,
m18n.n('restore_removing_tmp_dir_failed'))
raise YunohostError('restore_removing_tmp_dir_failed')
filesystem.mkdir(self.work_dir, parents=True)
@ -1027,9 +1027,9 @@ class RestoreManager():
self._read_info_files()
###########################################################################
#
# Space computation / checks #
###########################################################################
#
def _compute_needed_space(self):
"""
@ -1086,21 +1086,13 @@ class RestoreManager():
return True
elif free_space > needed_space:
# TODO Add --force options to avoid the error raising
raise MoulinetteError(errno.EIO,
m18n.n('restore_may_be_not_enough_disk_space',
free_space=free_space,
needed_space=needed_space,
margin=margin))
raise YunohostError('restore_may_be_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin)
else:
raise MoulinetteError(errno.EIO,
m18n.n('restore_not_enough_disk_space',
free_space=free_space,
needed_space=needed_space,
margin=margin))
raise YunohostError('restore_not_enough_disk_space', free_space=free_space, needed_space=needed_space, margin=margin)
###########################################################################
#
# "Actual restore" (reverse step of the backup collect part) #
###########################################################################
#
def restore(self):
"""
@ -1116,7 +1108,6 @@ class RestoreManager():
# Apply dirty patch to redirect php5 file on php7
self._patch_backup_csv_file()
self._restore_system()
self._restore_apps()
finally:
@ -1142,13 +1133,11 @@ class RestoreManager():
contains_php5 = True
row['source'] = row['source'].replace('/etc/php5', '/etc/php/7.0') \
.replace('/var/run/php5-fpm', '/var/run/php/php7.0-fpm') \
.replace('php5','php7')
.replace('php5', 'php7')
newlines.append(row)
except (IOError, OSError, csv.Error) as e:
raise MoulinetteError(errno.EIO,m18n.n('error_reading_file',
file=backup_csv,
error=str(e)))
raise YunohostError('error_reading_file', file=backup_csv, error=str(e))
if not contains_php5:
return
@ -1287,7 +1276,7 @@ class RestoreManager():
app_instance_name)
app_scripts_new_path = os.path.join(app_settings_new_path, 'scripts')
shutil.copytree(app_settings_in_archive, app_settings_new_path)
filesystem.chmod(app_settings_new_path, 0400, 0400, True)
filesystem.chmod(app_settings_new_path, 0o400, 0o400, True)
filesystem.chown(app_scripts_new_path, 'admin', None, True)
# Copy the app scripts to a writable temporary folder
@ -1295,7 +1284,7 @@ class RestoreManager():
# in the backup method ?
tmp_folder_for_app_restore = tempfile.mkdtemp(prefix='restore')
copytree(app_scripts_in_archive, tmp_folder_for_app_restore)
filesystem.chmod(tmp_folder_for_app_restore, 0550, 0550, True)
filesystem.chmod(tmp_folder_for_app_restore, 0o550, 0o550, True)
filesystem.chown(tmp_folder_for_app_restore, 'admin', None, True)
restore_script = os.path.join(tmp_folder_for_app_restore, 'restore')
@ -1312,7 +1301,7 @@ class RestoreManager():
raise_on_error=True,
env=env_dict)
except:
msg = m18n.n('restore_app_failed',app=app_instance_name)
msg = m18n.n('restore_app_failed', app=app_instance_name)
logger.exception(msg)
operation_logger.error(msg)
@ -1373,12 +1362,13 @@ class RestoreManager():
return env_var
###############################################################################
#
# Backup methods #
###############################################################################
#
class BackupMethod(object):
"""
BackupMethod is an abstract class that represents a way to backup and
restore a list of files.
@ -1441,7 +1431,7 @@ class BackupMethod(object):
@property
def method_name(self):
"""Return the string name of a BackupMethod (eg "tar" or "copy")"""
raise MoulinetteError(errno.EINVAL, m18n.n('backup_abstract_method'))
raise YunohostError('backup_abstract_method')
@property
def name(self):
@ -1523,8 +1513,7 @@ class BackupMethod(object):
"""
if self.need_mount():
if self._recursive_umount(self.work_dir) > 0:
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_cleaning_failed'))
raise YunohostError('backup_cleaning_failed')
if self.manager.is_tmp_work_dir:
filesystem.rm(self.work_dir, True, True)
@ -1566,8 +1555,7 @@ class BackupMethod(object):
if free_space < backup_size:
logger.debug('Not enough space at %s (free: %s / needed: %d)',
self.repo, free_space, backup_size)
raise MoulinetteError(errno.EIO, m18n.n(
'not_enough_disk_space', path=self.repo))
raise YunohostError('not_enough_disk_space', path=self.repo)
def _organize_files(self):
"""
@ -1655,12 +1643,10 @@ class BackupMethod(object):
i = msignals.prompt(m18n.n('backup_ask_for_copying_if_needed',
answers='y/N', size=str(size)))
except NotImplemented:
raise MoulinetteError(errno.EIO,
m18n.n('backup_unable_to_organize_files'))
raise YunohostError('backup_unable_to_organize_files')
else:
if i != 'y' and i != 'Y':
raise MoulinetteError(errno.EIO,
m18n.n('backup_unable_to_organize_files'))
raise YunohostError('backup_unable_to_organize_files')
# Copy unbinded path
logger.debug(m18n.n('backup_copying_to_organize_the_archive',
@ -1704,6 +1690,7 @@ class BackupMethod(object):
class CopyBackupMethod(BackupMethod):
"""
This class just do an uncompress copy of each file in a location, and
could be the inverse for restoring
@ -1730,7 +1717,7 @@ class CopyBackupMethod(BackupMethod):
dest_parent = os.path.dirname(dest)
if not os.path.exists(dest_parent):
filesystem.mkdir(dest_parent, 0750, True, uid='admin')
filesystem.mkdir(dest_parent, 0o750, True, uid='admin')
if os.path.isdir(source):
shutil.copytree(source, dest)
@ -1750,8 +1737,7 @@ class CopyBackupMethod(BackupMethod):
super(CopyBackupMethod, self).mount()
if not os.path.isdir(self.repo):
raise MoulinetteError(errno.EIO,
m18n.n('backup_no_uncompress_archive_dir'))
raise YunohostError('backup_no_uncompress_archive_dir')
filesystem.mkdir(self.work_dir, parent=True)
ret = subprocess.call(["mount", "-r", "--rbind", self.repo,
@ -1762,11 +1748,11 @@ class CopyBackupMethod(BackupMethod):
logger.warning(m18n.n("bind_mouting_disable"))
subprocess.call(["mountpoint", "-q", dest,
"&&", "umount", "-R", dest])
raise MoulinetteError(errno.EIO,
m18n.n('backup_cant_mount_uncompress_archive'))
raise YunohostError('backup_cant_mount_uncompress_archive')
class TarBackupMethod(BackupMethod):
"""
This class compress all files to backup in archive.
"""
@ -1797,7 +1783,7 @@ class TarBackupMethod(BackupMethod):
"""
if not os.path.exists(self.repo):
filesystem.mkdir(self.repo, 0750, parents=True, uid='admin')
filesystem.mkdir(self.repo, 0o750, parents=True, uid='admin')
# Check free space in output
self._check_is_enough_free_space()
@ -1808,8 +1794,7 @@ class TarBackupMethod(BackupMethod):
except:
logger.debug("unable to open '%s' for writing",
self._archive_file, exc_info=1)
raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_open_failed'))
raise YunohostError('backup_archive_open_failed')
# Add files to the archive
try:
@ -1820,8 +1805,7 @@ class TarBackupMethod(BackupMethod):
tar.close()
except IOError:
logger.error(m18n.n('backup_archive_writing_error'), exc_info=1)
raise MoulinetteError(errno.EIO,
m18n.n('backup_creation_failed'))
raise YunohostError('backup_creation_failed')
# Move info file
shutil.copy(os.path.join(self.work_dir, 'info.json'),
@ -1849,8 +1833,7 @@ class TarBackupMethod(BackupMethod):
except:
logger.debug("cannot open backup archive '%s'",
self._archive_file, exc_info=1)
raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_open_failed'))
raise YunohostError('backup_archive_open_failed')
tar.close()
# Mount the tarball
@ -1911,15 +1894,14 @@ class BorgBackupMethod(BackupMethod):
super(CopyBackupMethod, self).backup()
# TODO run borg create command
raise MoulinetteError(
errno.EIO, m18n.n('backup_borg_not_implemented'))
raise YunohostError('backup_borg_not_implemented')
def mount(self, mnt_path):
raise MoulinetteError(
errno.EIO, m18n.n('backup_borg_not_implemented'))
raise YunohostError('backup_borg_not_implemented')
class CustomBackupMethod(BackupMethod):
"""
This class use a bash script/hook "backup_method" to do the
backup/restore operations. A user can add his own hook inside
@ -1962,8 +1944,7 @@ class CustomBackupMethod(BackupMethod):
ret = hook_callback('backup_method', [self.method],
args=self._get_args('backup'))
if ret['failed']:
raise MoulinetteError(errno.EIO,
m18n.n('backup_custom_backup_error'))
raise YunohostError('backup_custom_backup_error')
def mount(self, restore_manager):
"""
@ -1976,8 +1957,7 @@ class CustomBackupMethod(BackupMethod):
ret = hook_callback('backup_method', [self.method],
args=self._get_args('mount'))
if ret['failed']:
raise MoulinetteError(errno.EIO,
m18n.n('backup_custom_mount_error'))
raise YunohostError('backup_custom_mount_error')
def _get_args(self, action):
"""Return the arguments to give to the custom script"""
@ -1985,9 +1965,9 @@ class CustomBackupMethod(BackupMethod):
self.manager.description]
###############################################################################
#
# "Front-end" #
###############################################################################
#
def backup_create(name=None, description=None, methods=[],
output_directory=None, no_compress=False,
@ -2007,14 +1987,13 @@ def backup_create(name=None, description=None, methods=[],
# TODO: Add a 'clean' argument to clean output directory
###########################################################################
#
# Validate / parse arguments #
###########################################################################
#
# Validate there is no archive with the same name
if name and name in backup_list()['archives']:
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_archive_name_exists'))
raise YunohostError('backup_archive_name_exists')
# Validate output_directory option
if output_directory:
@ -2024,17 +2003,14 @@ def backup_create(name=None, description=None, methods=[],
if output_directory.startswith(ARCHIVES_PATH) or \
re.match(r'^/(|(bin|boot|dev|etc|lib|root|run|sbin|sys|usr|var)(|/.*))$',
output_directory):
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_output_directory_forbidden'))
raise YunohostError('backup_output_directory_forbidden')
# Check that output directory is empty
if os.path.isdir(output_directory) and no_compress and \
os.listdir(output_directory):
raise MoulinetteError(errno.EIO,
m18n.n('backup_output_directory_not_empty'))
raise YunohostError('backup_output_directory_not_empty')
elif no_compress:
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_output_directory_required'))
raise YunohostError('backup_output_directory_required')
# Define methods (retro-compat)
if not methods:
@ -2048,9 +2024,9 @@ def backup_create(name=None, description=None, methods=[],
system = []
apps = []
###########################################################################
#
# Intialize #
###########################################################################
#
# Create yunohost archives directory if it does not exists
_create_archive_dir()
@ -2075,9 +2051,9 @@ def backup_create(name=None, description=None, methods=[],
backup_manager.set_system_targets(system)
backup_manager.set_apps_targets(apps)
###########################################################################
#
# Collect files and put them in the archive #
###########################################################################
#
# Collect files to be backup (by calling app backup script / system hooks)
backup_manager.collect_files()
@ -2105,9 +2081,9 @@ def backup_restore(auth, name, system=[], apps=[], force=False):
apps -- List of application names to restore
"""
###########################################################################
#
# Validate / parse arguments #
###########################################################################
#
# If no --system or --apps given, restore everything
if system is None and apps is None:
@ -2131,14 +2107,14 @@ def backup_restore(auth, name, system=[], apps=[], force=False):
if i == 'y' or i == 'Y':
force = True
if not force:
raise MoulinetteError(errno.EEXIST, m18n.n('restore_failed'))
raise YunohostError('restore_failed')
# TODO Partial app restore could not work if ldap is not restored before
# TODO repair mysql if broken and it's a complete restore
###########################################################################
#
# Initialize #
###########################################################################
#
restore_manager = RestoreManager(name)
@ -2147,9 +2123,9 @@ def backup_restore(auth, name, system=[], apps=[], force=False):
restore_manager.assert_enough_free_space()
###########################################################################
#
# Mount the archive then call the restore for each system part / app #
###########################################################################
#
restore_manager.mount()
restore_manager.restore()
@ -2158,7 +2134,7 @@ def backup_restore(auth, name, system=[], apps=[], force=False):
if restore_manager.success:
logger.success(m18n.n('restore_complete'))
else:
raise MoulinetteError(errno.EINVAL, m18n.n('restore_nothings_done'))
raise YunohostError('restore_nothings_done')
return restore_manager.targets.results
@ -2187,14 +2163,14 @@ def backup_list(with_info=False, human_readable=False):
except ValueError:
continue
result.append(name)
result.sort()
result.sort(key=lambda x: os.path.getctime(os.path.join(ARCHIVES_PATH, x + ".tar.gz")))
if result and with_info:
d = OrderedDict()
for a in result:
try:
d[a] = backup_info(a, human_readable=human_readable)
except MoulinetteError, e:
except YunohostError as e:
logger.warning('%s: %s' % (a, e.strerror))
result = d
@ -2216,8 +2192,7 @@ def backup_info(name, with_details=False, human_readable=False):
# Check file exist (even if it's a broken symlink)
if not os.path.lexists(archive_file):
raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_name_unknown', name=name))
raise YunohostError('backup_archive_name_unknown', name=name)
# If symlink, retrieve the real path
if os.path.islink(archive_file):
@ -2225,9 +2200,8 @@ def backup_info(name, with_details=False, human_readable=False):
# Raise exception if link is broken (e.g. on unmounted external storage)
if not os.path.exists(archive_file):
raise MoulinetteError(errno.EIO,
m18n.n('backup_archive_broken_link',
path=archive_file))
raise YunohostError('backup_archive_broken_link',
path=archive_file)
info_file = "%s/%s.info.json" % (ARCHIVES_PATH, name)
@ -2239,7 +2213,7 @@ def backup_info(name, with_details=False, human_readable=False):
except KeyError:
logger.debug("unable to retrieve '%s' inside the archive",
info_file, exc_info=1)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
raise YunohostError('backup_invalid_archive')
else:
shutil.move(os.path.join(info_dir, 'info.json'), info_file)
finally:
@ -2252,7 +2226,7 @@ def backup_info(name, with_details=False, human_readable=False):
info = json.load(f)
except:
logger.debug("unable to load '%s'", info_file, exc_info=1)
raise MoulinetteError(errno.EIO, m18n.n('backup_invalid_archive'))
raise YunohostError('backup_invalid_archive')
# Retrieve backup size
size = info.get('size', 0)
@ -2266,8 +2240,7 @@ def backup_info(name, with_details=False, human_readable=False):
result = {
'path': archive_file,
'created_at': time.strftime(m18n.n('format_datetime_short'),
time.gmtime(info['created_at'])),
'created_at': datetime.utcfromtimestamp(info['created_at']),
'description': info['description'],
'size': size,
}
@ -2292,8 +2265,8 @@ def backup_delete(name):
"""
if name not in backup_list()["archives"]:
raise MoulinetteError(errno.EIO, m18n.n('backup_archive_name_unknown',
name=name))
raise YunohostError('backup_archive_name_unknown',
name=name)
hook_callback('pre_backup_delete', args=[name])
@ -2311,20 +2284,18 @@ def backup_delete(name):
logger.success(m18n.n('backup_deleted'))
###############################################################################
#
# Misc helpers #
###############################################################################
#
def _create_archive_dir():
""" Create the YunoHost archives directory if doesn't exist """
if not os.path.isdir(ARCHIVES_PATH):
if os.path.lexists(ARCHIVES_PATH):
raise MoulinetteError(errno.EINVAL,
m18n.n('backup_output_symlink_dir_broken',
path=ARCHIVES_PATH))
raise YunohostError('backup_output_symlink_dir_broken', path=ARCHIVES_PATH)
os.mkdir(ARCHIVES_PATH, 0750)
os.mkdir(ARCHIVES_PATH, 0o750)
def _call_for_each_path(self, callback, csv_path=None):

View file

@ -24,7 +24,6 @@
import os
import sys
import errno
import shutil
import pwd
import grp
@ -37,7 +36,7 @@ from datetime import datetime
from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.utils.network import get_public_ip
@ -81,9 +80,9 @@ DNS_RESOLVERS = [
"80.67.188.188" # LDN
]
###############################################################################
#
# Front-end stuff #
###############################################################################
#
def certificate_status(auth, domain_list, full=False):
@ -106,8 +105,7 @@ def certificate_status(auth, domain_list, full=False):
for domain in domain_list:
# Is it in Yunohost domain list?
if domain not in yunohost_domains_list:
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_unknown', domain=domain))
raise YunohostError('certmanager_domain_unknown', domain=domain)
certificates = {}
@ -154,7 +152,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
args={'force': force})
# Paths of files and folder we'll need
date_tag = datetime.now().strftime("%Y%m%d.%H%M%S")
date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S")
new_cert_folder = "%s/%s-history/%s-selfsigned" % (
CERT_FOLDER, domain, date_tag)
@ -172,8 +170,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
status = _get_status(domain)
if status["summary"]["code"] in ('good', 'great'):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_attempt_to_replace_valid_cert', domain=domain))
raise YunohostError('certmanager_attempt_to_replace_valid_cert', domain=domain)
operation_logger.start()
@ -203,8 +200,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
if p.returncode != 0:
logger.warning(out)
raise MoulinetteError(
errno.EIO, m18n.n('domain_cert_gen_failed'))
raise YunohostError('domain_cert_gen_failed')
else:
logger.debug(out)
@ -219,10 +215,10 @@ def _certificate_install_selfsigned(domain_list, force=False):
crt_pem.write(ca_pem.read())
# Set appropriate permissions
_set_permissions(new_cert_folder, "root", "root", 0755)
_set_permissions(key_file, "root", "ssl-cert", 0640)
_set_permissions(crt_file, "root", "ssl-cert", 0640)
_set_permissions(conf_file, "root", "root", 0600)
_set_permissions(new_cert_folder, "root", "root", 0o755)
_set_permissions(key_file, "root", "ssl-cert", 0o640)
_set_permissions(crt_file, "root", "ssl-cert", 0o640)
_set_permissions(conf_file, "root", "root", 0o600)
# Actually enable the certificate we created
_enable_certificate(domain, new_cert_folder)
@ -262,14 +258,12 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F
for domain in domain_list:
yunohost_domains_list = yunohost.domain.domain_list(auth)['domains']
if domain not in yunohost_domains_list:
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_unknown', domain=domain))
raise YunohostError('certmanager_domain_unknown', domain=domain)
# Is it self-signed?
status = _get_status(domain)
if not force and status["CA_type"]["code"] != "self-signed":
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_cert_not_selfsigned', domain=domain))
raise YunohostError('certmanager_domain_cert_not_selfsigned', domain=domain)
if staging:
logger.warning(
@ -304,6 +298,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F
logger.error(msg)
operation_logger.error(msg)
def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False):
"""
Renew Let's Encrypt certificate for given domains (all by default)
@ -349,25 +344,21 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal
# Is it in Yunohost dmomain list?
if domain not in yunohost.domain.domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_unknown', domain=domain))
raise YunohostError('certmanager_domain_unknown', domain=domain)
status = _get_status(domain)
# Does it expire soon?
if status["validity"] > VALIDITY_LIMIT and not force:
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_attempt_to_renew_valid_cert', domain=domain))
raise YunohostError('certmanager_attempt_to_renew_valid_cert', domain=domain)
# Does it have a Let's Encrypt cert?
if status["CA_type"]["code"] != "lets-encrypt":
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_attempt_to_renew_nonLE_cert', domain=domain))
raise YunohostError('certmanager_attempt_to_renew_nonLE_cert', domain=domain)
# Check ACME challenge configured for given domain
if not _check_acme_challenge_configuration(domain):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_acme_not_configured_for_domain', domain=domain))
raise YunohostError('certmanager_acme_not_configured_for_domain', domain=domain)
if staging:
logger.warning(
@ -411,9 +402,10 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal
logger.error("Sending email with details to root ...")
_email_renewing_failed(domain, e, stack.getvalue())
###############################################################################
#
# Back-end stuff #
###############################################################################
#
def _install_cron():
cron_job_file = "/etc/cron.daily/yunohost-certificate-renew"
@ -422,7 +414,7 @@ def _install_cron():
f.write("#!/bin/bash\n")
f.write("yunohost domain cert-renew --email\n")
_set_permissions(cron_job_file, "root", "root", 0755)
_set_permissions(cron_job_file, "root", "root", 0o755)
def _email_renewing_failed(domain, exception_message, stack):
@ -466,7 +458,7 @@ def _configure_for_acme_challenge(auth, domain):
nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder
nginx_configuration = '''
location ^~ '/.well-known/acme-challenge'
location ^~ '/.well-known/acme-challenge/'
{
default_type "text/plain";
alias %s;
@ -484,8 +476,7 @@ location ^~ '/.well-known/acme-challenge'
contents = f.read()
if '/.well-known/acme-challenge' in contents:
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_conflicting_nginx_file', filepath=path))
raise YunohostError('certmanager_conflicting_nginx_file', filepath=path)
# Write the conf
if os.path.exists(nginx_conf_file):
@ -528,8 +519,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
if not os.path.exists(TMP_FOLDER):
os.makedirs(TMP_FOLDER)
_set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650)
_set_permissions(TMP_FOLDER, "root", "root", 0640)
_set_permissions(WEBROOT_FOLDER, "root", "www-data", 0o650)
_set_permissions(TMP_FOLDER, "root", "root", 0o640)
# Regen conf for dnsmasq if needed
_regen_dnsmasq_if_needed()
@ -540,7 +531,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain)
_generate_key(domain_key_file)
_set_permissions(domain_key_file, "root", "ssl-cert", 0640)
_set_permissions(domain_key_file, "root", "ssl-cert", 0o640)
_prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER)
@ -563,31 +554,28 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
CA=certification_authority)
except ValueError as e:
if "urn:acme:error:rateLimited" in str(e):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_hit_rate_limit', domain=domain))
raise YunohostError('certmanager_hit_rate_limit', domain=domain)
else:
logger.error(str(e))
_display_debug_information(domain)
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_cert_signing_failed'))
raise YunohostError('certmanager_cert_signing_failed')
except Exception as e:
logger.error(str(e))
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_cert_signing_failed'))
raise YunohostError('certmanager_cert_signing_failed')
import requests # lazy loading this module for performance reasons
try:
intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text
except requests.exceptions.Timeout as e:
raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_couldnt_fetch_intermediate_cert'))
raise YunohostError('certmanager_couldnt_fetch_intermediate_cert')
# Now save the key and signed certificate
logger.debug("Saving the key and signed certificate...")
# Create corresponding directory
date_tag = datetime.now().strftime("%Y%m%d.%H%M%S")
date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S")
if staging:
folder_flag = "staging"
@ -599,12 +587,12 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
os.makedirs(new_cert_folder)
_set_permissions(new_cert_folder, "root", "root", 0655)
_set_permissions(new_cert_folder, "root", "root", 0o655)
# Move the private key
domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem")
shutil.move(domain_key_file, domain_key_file_finaldest)
_set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0640)
_set_permissions(domain_key_file_finaldest, "root", "ssl-cert", 0o640)
# Write the cert
domain_cert_file = os.path.join(new_cert_folder, "crt.pem")
@ -613,7 +601,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
f.write(signed_certificate)
f.write(intermediate_certificate)
_set_permissions(domain_cert_file, "root", "ssl-cert", 0640)
_set_permissions(domain_cert_file, "root", "ssl-cert", 0o640)
if staging:
return
@ -624,8 +612,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
status_summary = _get_status(domain)["summary"]
if status_summary["code"] != "great":
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_certificate_fetching_or_enabling_failed', domain=domain))
raise YunohostError('certmanager_certificate_fetching_or_enabling_failed', domain=domain)
def _prepare_certificate_signing_request(domain, key_file, output_folder):
@ -658,8 +645,7 @@ def _get_status(domain):
cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem")
if not os.path.isfile(cert_file):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_no_cert_file', domain=domain, file=cert_file))
raise YunohostError('certmanager_no_cert_file', domain=domain, file=cert_file)
from OpenSSL import crypto # lazy loading this module for performance reasons
try:
@ -668,13 +654,12 @@ def _get_status(domain):
except Exception as exception:
import traceback
traceback.print_exc(file=sys.stdout)
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception))
raise YunohostError('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)
cert_subject = cert.get_subject().CN
cert_issuer = cert.get_issuer().CN
valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ")
days_remaining = (valid_up_to - datetime.now()).days
days_remaining = (valid_up_to - datetime.utcnow()).days
if cert_issuer == _name_self_CA():
CA_type = {
@ -752,15 +737,15 @@ def _get_status(domain):
"ACME_eligible": ACME_eligible
}
###############################################################################
#
# Misc small stuff ... #
###############################################################################
#
def _generate_account_key():
logger.debug("Generating account key ...")
_generate_key(ACCOUNT_KEY_FILE)
_set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400)
_set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0o400)
def _generate_key(destination_path):
@ -803,15 +788,23 @@ def _enable_certificate(domain, new_cert_folder):
for service in ("postfix", "dovecot", "metronome"):
_run_service_command("restart", service)
if os.path.isfile('/etc/yunohost/installed'):
# regen nginx conf to be sure it integrates OCSP Stapling
# (We don't do this yet if postinstall is not finished yet)
service_regen_conf(names=['nginx'])
_run_service_command("reload", "nginx")
from yunohost.hook import hook_callback
hook_callback('post_cert_update', args=[domain])
def _backup_current_cert(domain):
logger.debug("Backuping existing certificate for domain %s", domain)
cert_folder_domain = os.path.join(CERT_FOLDER, domain)
date_tag = datetime.now().strftime("%Y%m%d.%H%M%S")
date_tag = datetime.utcnow().strftime("%Y%m%d.%H%M%S")
backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag)
shutil.copytree(cert_folder_domain, backup_folder)
@ -822,13 +815,11 @@ def _check_domain_is_ready_for_ACME(domain):
# Check if IP from DNS matches public IP
if not _dns_ip_match_public_ip(public_ip, domain):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain))
raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)
# Check if domain seems to be accessible through HTTP?
if not _domain_is_accessible_through_HTTP(public_ip, domain):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_domain_http_not_working', domain=domain))
raise YunohostError('certmanager_domain_http_not_working', domain=domain)
def _get_dns_ip(domain):
@ -837,8 +828,7 @@ def _get_dns_ip(domain):
resolver.nameservers = DNS_RESOLVERS
answers = resolver.query(domain, "A")
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
raise MoulinetteError(errno.EINVAL, m18n.n(
'certmanager_error_no_A_record', domain=domain))
raise YunohostError('certmanager_error_no_A_record', domain=domain)
return str(answers[0])

View file

@ -3,7 +3,9 @@ import glob
from yunohost.tools import Migration
from moulinette.utils.filesystem import chown
class MyMigration(Migration):
"Change certificates group permissions from 'metronome' to 'ssl-cert'"
all_certificate_files = glob.glob("/etc/yunohost/certs/*/*.pem")

View file

@ -4,10 +4,9 @@ import requests
import base64
import time
import json
import errno
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
@ -17,6 +16,7 @@ logger = getActionLogger('yunohost.migration')
class MyMigration(Migration):
"Migrate Dyndns stuff from MD5 TSIG to SHA512 TSIG"
def backward(self):
@ -29,7 +29,7 @@ class MyMigration(Migration):
try:
(domain, private_key_path) = _guess_current_dyndns_domain(dyn_host)
assert "+157" in private_key_path
except (MoulinetteError, AssertionError):
except (YunohostError, AssertionError):
logger.info(m18n.n("migrate_tsig_not_needed"))
return
@ -52,7 +52,7 @@ class MyMigration(Migration):
'public_key_sha512': base64.b64encode(public_key_sha512),
}, timeout=30)
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
raise YunohostError('no_internet_connection')
if r.status_code != 201:
try:
@ -70,8 +70,8 @@ class MyMigration(Migration):
# Migration didn't succeed, so we rollback and raise an exception
os.system("mv /etc/yunohost/dyndns/*+165* /tmp")
raise MoulinetteError(m18n.n('migrate_tsig_failed', domain=domain,
error_code=str(r.status_code), error=error))
raise YunohostError('migrate_tsig_failed', domain=domain,
error_code=str(r.status_code), error=error)
# remove old certificates
os.system("mv /etc/yunohost/dyndns/*+157* /tmp")
@ -88,4 +88,3 @@ class MyMigration(Migration):
logger.info(m18n.n('migrate_tsig_end'))
return

View file

@ -3,7 +3,7 @@ import os
from shutil import copy2
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output, call_async_output
from moulinette.utils.filesystem import read_file
@ -24,13 +24,14 @@ YUNOHOST_PACKAGES = ["yunohost", "yunohost-admin", "moulinette", "ssowat"]
class MyMigration(Migration):
"Upgrade the system to Debian Stretch and Yunohost 3.0"
mode = "manual"
def backward(self):
raise MoulinetteError(m18n.n("migration_0003_backward_impossible"))
raise YunohostError("migration_0003_backward_impossible")
def migrate(self):
@ -57,7 +58,7 @@ class MyMigration(Migration):
self.apt_dist_upgrade(conf_flags=["old", "miss", "def"])
_run_service_command("start", "mysql")
if self.debian_major_version() == 8:
raise MoulinetteError(m18n.n("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile))
raise YunohostError("migration_0003_still_on_jessie_after_main_upgrade", log=self.logfile)
# Specific upgrade for fail2ban...
logger.info(m18n.n("migration_0003_fail2ban_upgrade"))
@ -107,11 +108,11 @@ class MyMigration(Migration):
# would still be in 2.x...
if not self.debian_major_version() == 8 \
and not self.yunohost_major_version() == 2:
raise MoulinetteError(m18n.n("migration_0003_not_jessie"))
raise YunohostError("migration_0003_not_jessie")
# Have > 1 Go free space on /var/ ?
if free_space_in_directory("/var/") / (1024**3) < 1.0:
raise MoulinetteError(m18n.n("migration_0003_not_enough_free_space"))
raise YunohostError("migration_0003_not_enough_free_space")
# Check system is up to date
# (but we don't if 'stretch' is already in the sources.list ...
@ -120,7 +121,7 @@ class MyMigration(Migration):
self.apt_update()
apt_list_upgradable = check_output("apt list --upgradable -a")
if "upgradable" in apt_list_upgradable:
raise MoulinetteError(m18n.n("migration_0003_system_not_fully_up_to_date"))
raise YunohostError("migration_0003_system_not_fully_up_to_date")
@property
def disclaimer(self):
@ -286,7 +287,7 @@ class MyMigration(Migration):
# Create tmp directory if it does not exists
tmp_dir = os.path.join("/tmp/", self.name)
if not os.path.exists(tmp_dir):
os.mkdir(tmp_dir, 0700)
os.mkdir(tmp_dir, 0o700)
for f in self.files_to_keep:
dest_file = f.strip('/').replace("/", "_")

View file

@ -19,6 +19,7 @@ MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {}
class MyMigration(Migration):
"Migrate php5-fpm 'pool' conf files to php7 stuff"
def migrate(self):

View file

@ -1,7 +1,7 @@
import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
@ -11,6 +11,7 @@ logger = getActionLogger('yunohost.migration')
class MyMigration(Migration):
"Migrate DBs from Postgresql 9.4 to 9.6 after migrating to Stretch"
def migrate(self):
@ -20,10 +21,10 @@ class MyMigration(Migration):
return
if not self.package_is_installed("postgresql-9.6"):
raise MoulinetteError(m18n.n("migration_0005_postgresql_96_not_installed"))
raise YunohostError("migration_0005_postgresql_96_not_installed")
if not space_used_by_directory("/var/lib/postgresql/9.4") > free_space_in_directory("/var/lib/postgresql"):
raise MoulinetteError(m18n.n("migration_0005_not_enough_space", path="/var/lib/postgresql/"))
raise YunohostError("migration_0005_not_enough_space", path="/var/lib/postgresql/")
subprocess.check_call("service postgresql stop", shell=True)
subprocess.check_call("pg_dropcluster --stop 9.6 main", shell=True)

View file

@ -0,0 +1,81 @@
import spwd
import crypt
import random
import string
import subprocess
from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import run_commands, check_output
from moulinette.utils.filesystem import append_to_file
from moulinette.authenticators.ldap import Authenticator
from yunohost.tools import Migration
logger = getActionLogger('yunohost.migration')
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"]
class MyMigration(Migration):
"Synchronize admin and root passwords"
def migrate(self):
new_hash = self._get_admin_hash()
self._replace_root_hash(new_hash)
logger.info(m18n.n("root_password_replaced_by_admin_password"))
def backward(self):
pass
@property
def mode(self):
# If the root password is still a "default" value,
# then this is an emergency and migration shall
# be applied automatically
#
# Otherwise, as playing with root password is touchy,
# we set this as a manual migration.
return "auto" if self._is_root_pwd_listed(SMALL_PWD_LIST) else "manual"
@property
def disclaimer(self):
if self._is_root_pwd_listed(SMALL_PWD_LIST):
return None
return m18n.n("migration_0006_disclaimer")
def _get_admin_hash(self):
"""
Fetch the admin hash from the LDAP db using slapcat
"""
admin_hash = check_output("slapcat \
| grep 'dn: cn=admin,dc=yunohost,dc=org' -A20 \
| grep userPassword -A2 \
| tr -d '\n ' \
| tr ':' ' ' \
| awk '{print $2}' \
| base64 -d \
| sed 's/{CRYPT}//g'")
return admin_hash
def _replace_root_hash(self, new_hash):
hash_root = spwd.getspnam("root").sp_pwd
with open('/etc/shadow', 'r') as before_file:
before = before_file.read()
with open('/etc/shadow', 'w') as after_file:
after_file.write(before.replace("root:" + hash_root,
"root:" + new_hash))
def _is_root_pwd_listed(self, pwd_list):
hash_root = spwd.getspnam("root").sp_pwd
for password in pwd_list:
if hash_root == crypt.crypt(password, hash_root):
return True
return False

View file

@ -0,0 +1,80 @@
import os
import re
from shutil import copyfile
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import mkdir, rm
from yunohost.tools import Migration
from yunohost.service import service_regen_conf, \
_get_conf_hashes, \
_calculate_hash, \
_run_service_command
from yunohost.settings import settings_set
from yunohost.utils.error import YunohostError
logger = getActionLogger('yunohost.migration')
SSHD_CONF = '/etc/ssh/sshd_config'
class MyMigration(Migration):
"""
This is the first step of a couple of migrations that ensure SSH conf is
managed by YunoHost (even if the "from_script" flag is present, which was
previously preventing it from being managed by YunoHost)
The goal of this first (automatic) migration is to make sure that the
sshd_config is managed by the regen-conf mechanism.
If the from_script flag exists, then we keep the current SSH conf such that it
will appear as "manually modified" to the regenconf.
In step 2 (manual), the admin will be able to choose wether or not to actually
use the recommended configuration, with an appropriate disclaimer.
"""
def migrate(self):
# Check if deprecated DSA Host Key is in config
dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$'
dsa = False
for line in open(SSHD_CONF):
if re.match(dsa_rgx, line) is not None:
dsa = True
break
if dsa:
settings_set("service.ssh.allow_deprecated_dsa_hostkey", True)
# Create sshd_config.d dir
if not os.path.exists(SSHD_CONF + '.d'):
mkdir(SSHD_CONF + '.d', 0o755, uid='root', gid='root')
# Here, we make it so that /etc/ssh/sshd_config is managed
# by the regen conf (in particular in the case where the
# from_script flag is present - in which case it was *not*
# managed by the regenconf)
# But because we can't be sure the user wants to use the
# recommended conf, we backup then restore the /etc/ssh/sshd_config
# right after the regenconf, such that it will appear as
# "manually modified".
if os.path.exists('/etc/yunohost/from_script'):
rm('/etc/yunohost/from_script')
copyfile(SSHD_CONF, '/etc/ssh/sshd_config.bkp')
service_regen_conf(names=['ssh'], force=True)
copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF)
# Restart ssh and backward if it fail
if not _run_service_command('restart', 'ssh'):
self.backward()
raise YunohostError("migration_0007_cancel")
def backward(self):
# We don't backward completely but it should be enough
copyfile('/etc/ssh/sshd_config.bkp', SSHD_CONF)
if not _run_service_command('restart', 'ssh'):
raise YunohostError("migration_0007_cannot_restart")

View file

@ -0,0 +1,99 @@
import re
from moulinette import m18n
from moulinette.utils.log import getActionLogger
from yunohost.tools import Migration
from yunohost.service import service_regen_conf, \
_get_conf_hashes, \
_calculate_hash
from yunohost.settings import settings_set, settings_get
from yunohost.utils.error import YunohostError
logger = getActionLogger('yunohost.migration')
SSHD_CONF = '/etc/ssh/sshd_config'
class MyMigration(Migration):
"""
In this second step, the admin is asked if it's okay to use
the recommended SSH configuration - which also implies
disabling deprecated DSA key.
This has important implications in the way the user may connect
to its server (key change, and a spooky warning might be given
by SSH later)
A disclaimer explaining the various things to be aware of is
shown - and the user may also choose to skip this migration.
"""
def migrate(self):
settings_set("service.ssh.allow_deprecated_dsa_hostkey", False)
service_regen_conf(names=['ssh'], force=True)
def backward(self):
raise YunohostError("migration_0008_backward_impossible")
@property
def mode(self):
# If the conf is already up to date
# and no DSA key is used, then we're good to go
# and the migration can be done automatically
# (basically nothing shall change)
ynh_hash = _get_conf_hashes('ssh').get(SSHD_CONF, None)
current_hash = _calculate_hash(SSHD_CONF)
dsa = settings_get("service.ssh.allow_deprecated_dsa_hostkey")
if ynh_hash == current_hash and not dsa:
return "auto"
return "manual"
@property
def disclaimer(self):
if self.mode == "auto":
return None
# Detect key things to be aware of before enabling the
# recommended configuration
dsa_key_enabled = False
ports = []
root_login = []
port_rgx = r'^[ \t]*Port[ \t]+(\d+)[ \t]*(?:#.*)?$'
root_rgx = r'^[ \t]*PermitRootLogin[ \t]([^# \t]*)[ \t]*(?:#.*)?$'
dsa_rgx = r'^[ \t]*HostKey[ \t]+/etc/ssh/ssh_host_dsa_key[ \t]*(?:#.*)?$'
for line in open(SSHD_CONF):
ports = ports + re.findall(port_rgx, line)
root_login = root_login + re.findall(root_rgx, line)
if not dsa_key_enabled and re.match(dsa_rgx, line) is not None:
dsa_key_enabled = True
custom_port = ports != ['22'] and ports != []
root_login_enabled = root_login and root_login[-1] != 'no'
# Build message
message = m18n.n("migration_0008_general_disclaimer")
if custom_port:
message += "\n\n" + m18n.n("migration_0008_port")
if root_login_enabled:
message += "\n\n" + m18n.n("migration_0008_root")
if dsa_key_enabled:
message += "\n\n" + m18n.n("migration_0008_dsa")
if custom_port or root_login_enabled or dsa_key_enabled:
message += "\n\n" + m18n.n("migration_0008_warning")
else:
message += "\n\n" + m18n.n("migration_0008_no_warning")
return message

View file

@ -25,12 +25,11 @@
"""
import os
import re
import json
import yaml
import errno
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
import yunohost.certificate
@ -78,7 +77,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False):
try:
auth.validate_uniqueness({'virtualdomain': domain})
except MoulinetteError:
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists'))
raise YunohostError('domain_exists')
operation_logger.start()
@ -87,16 +86,14 @@ def domain_add(operation_logger, auth, domain, dyndns=False):
# Do not allow to subscribe to multiple dyndns domains...
if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM,
m18n.n('domain_dyndns_already_subscribed'))
raise YunohostError('domain_dyndns_already_subscribed')
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
# Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if not _dyndns_provides("dyndns.yunohost.org", domain):
raise MoulinetteError(errno.EINVAL,
m18n.n('domain_dyndns_root_unknown'))
raise YunohostError('domain_dyndns_root_unknown')
# Actually subscribe
dyndns_subscribe(domain=domain)
@ -110,23 +107,20 @@ def domain_add(operation_logger, auth, domain, dyndns=False):
}
if not auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict):
raise MoulinetteError(errno.EIO, m18n.n('domain_creation_failed'))
raise YunohostError('domain_creation_failed')
# Don't regen these conf if we're still in postinstall
if os.path.exists('/etc/yunohost/installed'):
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix', 'rspamd'])
app_ssowatconf(auth)
except Exception, e:
from sys import exc_info;
t, v, tb = exc_info()
except Exception:
# Force domain removal silently
try:
domain_remove(auth, domain, True)
except:
pass
raise t, v, tb
raise
hook_callback('post_domain_add', args=[domain])
@ -147,11 +141,11 @@ def domain_remove(operation_logger, auth, domain, force=False):
from yunohost.app import app_ssowatconf
if not force and domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
raise YunohostError('domain_unknown')
# Check domain is not the main domain
if domain == _get_maindomain():
raise MoulinetteError(errno.EINVAL, m18n.n('domain_cannot_remove_main'))
raise YunohostError('domain_cannot_remove_main')
# Check if apps are installed on the domain
for app in os.listdir('/etc/yunohost/apps/'):
@ -162,14 +156,13 @@ def domain_remove(operation_logger, auth, domain, force=False):
continue
else:
if app_domain == domain:
raise MoulinetteError(errno.EPERM,
m18n.n('domain_uninstall_app_first'))
raise YunohostError('domain_uninstall_app_first')
operation_logger.start()
if auth.remove('virtualdomain=' + domain + ',ou=domains') or force:
os.system('rm -rf /etc/yunohost/certs/%s' % domain)
else:
raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed'))
raise YunohostError('domain_deletion_failed')
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
app_ssowatconf(auth)
@ -209,6 +202,11 @@ def domain_dns_conf(domain, ttl=None):
for record in dns_conf["mail"]:
result += "\n{name} {ttl} IN {type} {value}".format(**record)
result += "\n\n"
result += "; Extra"
for record in dns_conf["extra"]:
result += "\n{name} {ttl} IN {type} {value}".format(**record)
is_cli = True if msettings.get('interface') == 'cli' else False
if is_cli:
logger.info(m18n.n("domain_dns_conf_is_just_a_recommendation"))
@ -241,7 +239,7 @@ def _get_conflicting_apps(auth, domain, path):
# Abort if domain is unknown
if domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
raise YunohostError('domain_unknown')
# This import cannot be put on top of file because it would create a
# recursive import...
@ -334,6 +332,9 @@ def _build_dns_conf(domain, ttl=3600):
{"type": "TXT", "name": "mail._domainkey", "value": "\"v=DKIM1; k=rsa; p=some-super-long-key\"", "ttl": 3600},
{"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600}
],
"extra": [
{"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600},
],
}
"""
@ -387,10 +388,16 @@ def _build_dns_conf(domain, ttl=3600):
["_dmarc", ttl, "TXT", '"v=DMARC1; p=none"'],
]
# Extra
extra = [
["@", ttl, "CAA", '128 issue "letsencrypt.org"']
]
return {
"basic": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in basic],
"xmpp": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in xmpp],
"mail": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in mail],
"extra": [{"name": name, "ttl": ttl, "type": type_, "value": value} for name, ttl, type_, value in extra],
}

View file

@ -27,25 +27,23 @@ import os
import re
import json
import glob
import time
import base64
import errno
import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, write_to_file, rm
from moulinette.utils.filesystem import write_to_file
from moulinette.utils.network import download_json
from moulinette.utils.process import check_output
from yunohost.utils.error import YunohostError
from yunohost.domain import _get_maindomain, _build_dns_conf
from yunohost.utils.network import get_public_ip
from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.dyndns')
OLD_IPV4_FILE = '/etc/yunohost/dyndns/old_ip'
OLD_IPV6_FILE = '/etc/yunohost/dyndns/old_ipv6'
DYNDNS_ZONE = '/etc/yunohost/dyndns/zone'
RE_DYNDNS_PRIVATE_KEY_MD5 = re.compile(
@ -77,9 +75,7 @@ def _dyndns_provides(provider, domain):
dyndomains = download_json('https://%s/domains' % provider, timeout=30)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_provide',
domain=domain, provider=provider))
raise YunohostError('dyndns_could_not_check_provide', domain=domain, provider=provider)
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
dyndomain = '.'.join(domain.split('.')[1:])
@ -96,7 +92,7 @@ def _dyndns_available(provider, domain):
domain -- The full domain that you'd like.. e.g. "foo.nohost.me"
Returns:
True if the domain is avaible, False otherwise.
True if the domain is available, False otherwise.
"""
logger.debug("Checking if domain %s is available on %s ..."
% (domain, provider))
@ -106,9 +102,8 @@ def _dyndns_available(provider, domain):
expected_status_code=None)
except MoulinetteError as e:
logger.error(str(e))
raise MoulinetteError(errno.EIO,
m18n.n('dyndns_could_not_check_available',
domain=domain, provider=provider))
raise YunohostError('dyndns_could_not_check_available',
domain=domain, provider=provider)
return r == u"Domain %s is available" % domain
@ -130,14 +125,11 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom
# Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT,
m18n.n('dyndns_domain_not_provided',
domain=domain, provider=subscribe_host))
raise YunohostError('dyndns_domain_not_provided', domain=domain, provider=subscribe_host)
# Verify if domain is available
if not _dyndns_available(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT,
m18n.n('dyndns_unavailable', domain=domain))
raise YunohostError('dyndns_unavailable', domain=domain)
operation_logger.start()
@ -161,14 +153,13 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom
try:
r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30)
except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection'))
raise YunohostError('no_internet_connection')
if r.status_code != 201:
try:
error = json.loads(r.text)['error']
except:
error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text)
raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_registration_failed', error=error))
raise YunohostError('dyndns_registration_failed', error=error)
logger.success(m18n.n('dyndns_registered'))
@ -193,32 +184,6 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
old_ipv4, old_ipv6 = (None, None) # (default values)
if os.path.isfile(OLD_IPV4_FILE):
old_ipv4 = read_file(OLD_IPV4_FILE).rstrip()
if os.path.isfile(OLD_IPV6_FILE):
old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
# Get current IPv4 and IPv6
ipv4_ = get_public_ip()
ipv6_ = get_public_ip(6)
if ipv4 is None:
ipv4 = ipv4_
if ipv6 is None:
ipv6 = ipv6_
logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
# no need to update
if old_ipv4 == ipv4 and old_ipv6 == ipv6:
logger.info("No updated needed.")
return
else:
logger.info("Updated needed, going on...")
# If domain is not given, try to guess it from keys available...
if domain is None:
(domain, key) = _guess_current_dyndns_domain(dyn_host)
@ -228,13 +193,10 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain))
if not keys:
raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found'))
raise YunohostError('dyndns_key_not_found')
key = keys[0]
operation_logger.related_to.append(('domain', domain))
operation_logger.start()
# This mean that hmac-md5 is used
# (Re?)Trigger the migration to sha256 and return immediately.
# The actual update will be done in next run.
@ -262,7 +224,33 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
'zone %s' % host,
]
old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() or None
old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() or None
# Get current IPv4 and IPv6
ipv4_ = get_public_ip()
ipv6_ = get_public_ip(6)
if ipv4 is None:
ipv4 = ipv4_
if ipv6 is None:
ipv6 = ipv6_
logger.debug("Old IPv4/v6 are (%s, %s)" % (old_ipv4, old_ipv6))
logger.debug("Requested IPv4/v6 are (%s, %s)" % (ipv4, ipv6))
# no need to update
if old_ipv4 == ipv4 and old_ipv6 == ipv6:
logger.info("No updated needed.")
return
else:
operation_logger.related_to.append(('domain', domain))
operation_logger.start()
logger.info("Updated needed, going on...")
dns_conf = _build_dns_conf(domain)
del dns_conf["extra"] # Ignore records from the 'extra' category
# Delete the old records for all domain/subdomains
@ -283,7 +271,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
# should be muc.the.domain.tld. or the.domain.tld
if record["value"] == "@":
record["value"] = domain
record["value"] = record["value"].replace(";","\;")
record["value"] = record["value"].replace(";", "\;")
action = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record)
action = action.replace(" @.", " ")
@ -304,18 +292,10 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
subprocess.check_call(command)
except subprocess.CalledProcessError:
rm(OLD_IPV4_FILE, force=True) # Remove file (ignore if non-existent)
rm(OLD_IPV6_FILE, force=True) # Remove file (ignore if non-existent)
raise MoulinetteError(errno.EPERM,
m18n.n('dyndns_ip_update_failed'))
raise YunohostError('dyndns_ip_update_failed')
logger.success(m18n.n('dyndns_ip_updated'))
if ipv4 is not None:
write_to_file(OLD_IPV4_FILE, ipv4)
if ipv6 is not None:
write_to_file(OLD_IPV6_FILE, ipv6)
def dyndns_installcron():
"""
@ -338,7 +318,7 @@ def dyndns_removecron():
try:
os.remove("/etc/cron.d/yunohost-dyndns")
except:
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed'))
raise YunohostError('dyndns_cron_remove_failed')
logger.success(m18n.n('dyndns_cron_removed'))
@ -368,5 +348,4 @@ def _guess_current_dyndns_domain(dyn_host):
else:
return (_domain, path)
raise MoulinetteError(errno.EINVAL,
m18n.n('dyndns_no_domain_registered'))
raise YunohostError('dyndns_no_domain_registered')

View file

@ -26,7 +26,6 @@
import os
import sys
import yaml
import errno
try:
import miniupnpc
except ImportError:
@ -34,7 +33,7 @@ except ImportError:
sys.exit(1)
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils import process
from moulinette.utils.log import getActionLogger
from moulinette.utils.text import prependlines
@ -268,7 +267,7 @@ def firewall_reload(skip_upnp=False):
reloaded = True
if not reloaded:
raise MoulinetteError(errno.ESRCH, m18n.n('firewall_reload_failed'))
raise YunohostError('firewall_reload_failed')
hook_callback('post_iptable_rules',
args=[upnp, os.path.exists("/proc/net/if_inet6")])
@ -338,12 +337,13 @@ def firewall_upnp(action='status', no_refresh=False):
if action == 'status':
no_refresh = True
else:
raise MoulinetteError(errno.EINVAL, m18n.n('action_invalid', action=action))
raise YunohostError('action_invalid', action=action)
# Refresh port mapping using UPnP
if not no_refresh:
upnpc = miniupnpc.UPnP()
upnpc.discoverdelay = 3000
upnpc.discoverdelay = 62000
upnpc.localport = 1900
# Discover UPnP device(s)
logger.debug('discovering UPnP devices...')
@ -406,7 +406,7 @@ def firewall_upnp(action='status', no_refresh=False):
firewall_reload(skip_upnp=True)
if action == 'enable' and not enabled:
raise MoulinetteError(errno.ENXIO, m18n.n('upnp_port_open_failed'))
raise YunohostError('upnp_port_open_failed')
return {'enabled': enabled}
@ -418,7 +418,7 @@ def firewall_stop():
"""
if os.system("iptables -w -P INPUT ACCEPT") != 0:
raise MoulinetteError(errno.ESRCH, m18n.n('iptables_unavailable'))
raise YunohostError('iptables_unavailable')
os.system("iptables -w -F")
os.system("iptables -w -X")

View file

@ -25,12 +25,11 @@
"""
import os
import re
import errno
import tempfile
from glob import iglob
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils import log
HOOK_FOLDER = '/usr/share/yunohost/hooks/'
@ -112,7 +111,7 @@ def hook_info(action, name):
})
if not hooks:
raise MoulinetteError(errno.EINVAL, m18n.n('hook_name_unknown', name=name))
raise YunohostError('hook_name_unknown', name=name)
return {
'action': action,
'name': name,
@ -174,7 +173,7 @@ def hook_list(action, list_by='name', show_info=False):
# Add only the name
d.add(name)
else:
raise MoulinetteError(errno.EINVAL, m18n.n('hook_list_by_invalid'))
raise YunohostError('hook_list_by_invalid')
def _append_folder(d, folder):
# Iterate over and add hook from a folder
@ -255,8 +254,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
try:
hl = hooks_names[n]
except KeyError:
raise MoulinetteError(errno.EINVAL,
m18n.n('hook_name_unknown', n))
raise YunohostError('hook_name_unknown', n)
# Iterate over hooks with this name
for h in hl:
# Update hooks dict
@ -282,7 +280,7 @@ def hook_callback(action, hooks=[], args=None, no_trace=False, chdir=None,
path=path, args=args)
hook_exec(path, args=hook_args, chdir=chdir, env=env,
no_trace=no_trace, raise_on_error=True)
except MoulinetteError as e:
except YunohostError as e:
state = 'failed'
logger.error(e.strerror, exc_info=1)
post_callback(name=name, priority=priority, path=path,
@ -319,7 +317,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
if path[0] != '/':
path = os.path.realpath(path)
if not os.path.isfile(path):
raise MoulinetteError(errno.EIO, m18n.g('file_not_exist', path=path))
raise YunohostError('file_not_exist', path=path)
# Construct command variables
cmd_args = ''
@ -371,7 +369,7 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
)
if stdinfo:
callbacks = ( callbacks[0], callbacks[1],
callbacks = (callbacks[0], callbacks[1],
lambda l: logger.info(l.rstrip()))
logger.debug("About to run the command '%s'" % command)
@ -384,14 +382,12 @@ def hook_exec(path, args=None, raise_on_error=False, no_trace=False,
# Check and return process' return code
if returncode is None:
if raise_on_error:
raise MoulinetteError(
errno.EIO, m18n.n('hook_exec_not_terminated', path=path))
raise YunohostError('hook_exec_not_terminated', path=path)
else:
logger.error(m18n.n('hook_exec_not_terminated', path=path))
return 1
elif raise_on_error and returncode != 0:
raise MoulinetteError(
errno.EIO, m18n.n('hook_exec_failed', path=path))
raise YunohostError('hook_exec_failed', path=path)
return returncode

View file

@ -26,15 +26,13 @@
import os
import yaml
import errno
import collections
from datetime import datetime
from logging import FileHandler, getLogger, Formatter
from sys import exc_info
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
@ -148,8 +146,7 @@ def log_display(path, number=50, share=False):
log_path = base_path + LOG_FILE_EXT
if not os.path.exists(md_path) and not os.path.exists(log_path):
raise MoulinetteError(errno.EINVAL,
m18n.n('log_does_exists', log=path))
raise YunohostError('log_does_exists', log=path)
infos = {}
@ -189,7 +186,7 @@ def log_display(path, number=50, share=False):
if os.path.exists(log_path):
logger.warning(error)
else:
raise MoulinetteError(errno.EINVAL, error)
raise YunohostError(error)
# Display logs if exist
if os.path.exists(log_path):
@ -285,6 +282,7 @@ def is_unit_operation(entities=['app', 'domain', 'service', 'user'],
class OperationLogger(object):
"""
Instances of this class represents unit operation done on the ynh instance.
@ -316,7 +314,7 @@ class OperationLogger(object):
"""
if self.started_at is None:
self.started_at = datetime.now()
self.started_at = datetime.utcnow()
self.flush()
self._register_log()
@ -408,7 +406,7 @@ class OperationLogger(object):
return
if error is not None and not isinstance(error, basestring):
error = str(error)
self.ended_at = datetime.now()
self.ended_at = datetime.utcnow()
self._error = error
self._success = error is None
if self.logger is not None:

View file

@ -31,14 +31,13 @@ import calendar
import subprocess
import xmlrpclib
import os.path
import errno
import os
import dns.resolver
import cPickle as pickle
from datetime import datetime
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.utils.network import get_public_ip
@ -83,7 +82,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False):
result_dname = dn
if len(devices) == 0:
if mountpoint is not None:
raise MoulinetteError(errno.ENODEV, m18n.n('mountpoint_unknown'))
raise YunohostError('mountpoint_unknown')
return result
# Retrieve monitoring for unit(s)
@ -141,7 +140,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False):
for dname in devices_names:
_set(dname, 'not-available')
else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u))
raise YunohostError('unit_unknown', unit=u)
if result_dname is not None:
return result[result_dname]
@ -237,7 +236,7 @@ def monitor_network(units=None, human_readable=False):
'gateway': gateway,
}
else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u))
raise YunohostError('unit_unknown', unit=u)
if len(units) == 1:
return result[units[0]]
@ -287,9 +286,9 @@ def monitor_system(units=None, human_readable=False):
elif u == 'infos':
result[u] = json.loads(glances.getSystem())
else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u))
raise YunohostError('unit_unknown', unit=u)
if len(units) == 1 and type(result[units[0]]) is not str:
if len(units) == 1 and not isinstance(result[units[0]], str):
return result[units[0]]
return result
@ -303,7 +302,7 @@ def monitor_update_stats(period):
"""
if period not in ['day', 'week', 'month']:
raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid'))
raise YunohostError('monitor_period_invalid')
stats = _retrieve_stats(period)
if not stats:
@ -321,7 +320,7 @@ def monitor_update_stats(period):
else:
monitor = _monitor_all(p, 0)
if not monitor:
raise MoulinetteError(errno.ENODATA, m18n.n('monitor_stats_no_update'))
raise YunohostError('monitor_stats_no_update')
stats['timestamp'].append(time.time())
@ -386,15 +385,13 @@ def monitor_show_stats(period, date=None):
"""
if period not in ['day', 'week', 'month']:
raise MoulinetteError(errno.EINVAL, m18n.n('monitor_period_invalid'))
raise YunohostError('monitor_period_invalid')
result = _retrieve_stats(period, date)
if result is False:
raise MoulinetteError(errno.ENOENT,
m18n.n('monitor_stats_file_not_found'))
raise YunohostError('monitor_stats_file_not_found')
elif result is None:
raise MoulinetteError(errno.EINVAL,
m18n.n('monitor_stats_period_unavailable'))
raise YunohostError('monitor_stats_period_unavailable')
return result
@ -442,7 +439,7 @@ def monitor_disable():
if glances['loaded'] != 'disabled':
try:
service_disable('glances')
except MoulinetteError as e:
except YunohostError as e:
logger.warning(e.strerror)
# Remove crontab
@ -470,8 +467,8 @@ def _get_glances_api():
from yunohost.service import service_status
if service_status('glances')['status'] != 'running':
raise MoulinetteError(errno.EPERM, m18n.n('monitor_not_enabled'))
raise MoulinetteError(errno.EIO, m18n.n('monitor_glances_con_failed'))
raise YunohostError('monitor_not_enabled')
raise YunohostError('monitor_glances_con_failed')
def _extract_inet(string, skip_netmask=False, skip_loopback=True):

View file

@ -28,7 +28,6 @@ import time
import yaml
import json
import subprocess
import errno
import shutil
import hashlib
@ -36,11 +35,11 @@ from difflib import unified_diff
from datetime import datetime
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils import log, filesystem
from yunohost.hook import hook_callback
from yunohost.log import is_unit_operation
from yunohost.hook import hook_callback, hook_list
BASE_CONF_PATH = '/home/yunohost.conf'
BACKUP_CONF_DIR = os.path.join(BASE_CONF_PATH, 'backup')
@ -85,7 +84,7 @@ def service_add(name, status=None, log=None, runlevel=None, need_lock=False, des
_save_services(services)
except:
# we'll get a logger.warning with more details in _save_services
raise MoulinetteError(errno.EIO, m18n.n('service_add_failed', service=name))
raise YunohostError('service_add_failed', service=name)
logger.success(m18n.n('service_added', service=name))
@ -103,13 +102,13 @@ def service_remove(name):
try:
del services[name]
except KeyError:
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name))
raise YunohostError('service_unknown', service=name)
try:
_save_services(services)
except:
# we'll get a logger.warning with more details in _save_services
raise MoulinetteError(errno.EIO, m18n.n('service_remove_failed', service=name))
raise YunohostError('service_remove_failed', service=name)
logger.success(m18n.n('service_removed', service=name))
@ -130,10 +129,7 @@ def service_start(names):
logger.success(m18n.n('service_started', service=name))
else:
if service_status(name)['status'] != 'running':
raise MoulinetteError(errno.EPERM,
m18n.n('service_start_failed',
service=name,
logs=_get_journalctl_logs(name)))
raise YunohostError('service_start_failed', service=name, logs=_get_journalctl_logs(name))
logger.debug(m18n.n('service_already_started', service=name))
@ -152,12 +148,10 @@ def service_stop(names):
logger.success(m18n.n('service_stopped', service=name))
else:
if service_status(name)['status'] != 'inactive':
raise MoulinetteError(errno.EPERM,
m18n.n('service_stop_failed',
service=name,
logs=_get_journalctl_logs(name)))
raise YunohostError('service_stop_failed', service=name, logs=_get_journalctl_logs(name))
logger.debug(m18n.n('service_already_stopped', service=name))
@is_unit_operation()
def service_enable(operation_logger, names):
"""
@ -174,10 +168,7 @@ def service_enable(operation_logger, names):
if _run_service_command('enable', name):
logger.success(m18n.n('service_enabled', service=name))
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_enable_failed',
service=name,
logs=_get_journalctl_logs(name)))
raise YunohostError('service_enable_failed', service=name, logs=_get_journalctl_logs(name))
def service_disable(names):
@ -194,10 +185,7 @@ def service_disable(names):
if _run_service_command('disable', name):
logger.success(m18n.n('service_disabled', service=name))
else:
raise MoulinetteError(errno.EPERM,
m18n.n('service_disable_failed',
service=name,
logs=_get_journalctl_logs(name)))
raise YunohostError('service_disable_failed', service=name, logs=_get_journalctl_logs(name))
def service_status(names=[]):
@ -220,8 +208,7 @@ def service_status(names=[]):
for name in names:
if check_names and name not in services.keys():
raise MoulinetteError(errno.EINVAL,
m18n.n('service_unknown', service=name))
raise YunohostError('service_unknown', service=name)
# this "service" isn't a service actually so we skip it
#
@ -248,10 +235,7 @@ def service_status(names=[]):
'status': "unknown",
'loaded': "unknown",
'active': "unknown",
'active_at': {
"timestamp": "unknown",
"human": "unknown",
},
'active_at': "unknown",
'description': "Error: failed to get information for this service, it doesn't exists for systemd",
'service_file_path': "unknown",
}
@ -273,13 +257,13 @@ def service_status(names=[]):
'status': str(status.get("SubState", "unknown")),
'loaded': "enabled" if str(status.get("LoadState", "unknown")) == "loaded" else str(status.get("LoadState", "unknown")),
'active': str(status.get("ActiveState", "unknown")),
'active_at': {
"timestamp": str(status.get("ActiveEnterTimestamp", "unknown")),
"human": datetime.fromtimestamp(status["ActiveEnterTimestamp"] / 1000000).strftime("%F %X") if "ActiveEnterTimestamp" in status else "unknown",
},
'description': description,
'service_file_path': str(status.get("FragmentPath", "unknown")),
}
if "ActiveEnterTimestamp" in status:
result[name]['active_at'] = datetime.utcfromtimestamp(status["ActiveEnterTimestamp"] / 1000000)
else:
result[name]['active_at'] = "unknown"
if len(names) == 1:
return result[names[0]]
@ -293,7 +277,7 @@ def _get_service_information_from_systemd(service):
d = dbus.SystemBus()
systemd = d.get_object('org.freedesktop.systemd1','/org/freedesktop/systemd1')
systemd = d.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1')
manager = dbus.Interface(systemd, 'org.freedesktop.systemd1.Manager')
try:
@ -323,10 +307,10 @@ def service_log(name, number=50):
services = _get_services()
if name not in services.keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=name))
raise YunohostError('service_unknown', service=name)
if 'log' not in services[name]:
raise MoulinetteError(errno.EPERM, m18n.n('service_no_log', service=name))
raise YunohostError('service_no_log', service=name)
log_list = services[name]['log']
@ -394,7 +378,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False,
if not names:
operation_logger.name_parameter_override = 'all'
elif len(names) != 1:
operation_logger.name_parameter_override = str(len(operation_logger.related_to))+'_services'
operation_logger.name_parameter_override = str(len(operation_logger.related_to)) + '_services'
operation_logger.start()
# Clean pending conf directory
@ -406,7 +390,7 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False,
shutil.rmtree(os.path.join(PENDING_CONF_DIR, name),
ignore_errors=True)
else:
filesystem.mkdir(PENDING_CONF_DIR, 0755, True)
filesystem.mkdir(PENDING_CONF_DIR, 0o755, True)
# Format common hooks arguments
common_args = [1 if force else 0, 1 if dry_run else 0]
@ -417,20 +401,25 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False,
def _pre_call(name, priority, path, args):
# create the pending conf directory for the service
service_pending_path = os.path.join(PENDING_CONF_DIR, name)
filesystem.mkdir(service_pending_path, 0755, True, uid='root')
filesystem.mkdir(service_pending_path, 0o755, True, uid='root')
# return the arguments to pass to the script
return pre_args + [service_pending_path, ]
# Don't regen SSH if not specifically specified
if not names:
names = hook_list('conf_regen', list_by='name',
show_info=False)['hooks']
names.remove('ssh')
pre_result = hook_callback('conf_regen', names, pre_callback=_pre_call)
# Update the services name
names = pre_result['succeed'].keys()
if not names:
raise MoulinetteError(errno.EIO,
m18n.n('service_regenconf_failed',
services=', '.join(pre_result['failed'])))
raise YunohostError('service_regenconf_failed',
services=', '.join(pre_result['failed']))
# Set the processing method
_regen = _process_regen_conf if not dry_run else lambda *a, **k: True
@ -502,8 +491,8 @@ def service_regen_conf(operation_logger, names=[], with_diff=False, force=False,
# we assume that it is safe to regen it, since the file is backuped
# anyway (by default in _regen), as long as we warn the user
# appropriately.
logger.info(m18n.n('service_conf_new_managed_file',
conf=system_path, service=service))
logger.info(m18n.n('service_conf_now_managed_by_yunohost',
conf=system_path))
regenerated = _regen(system_path, pending_path)
conf_status = 'new'
elif force:
@ -606,7 +595,7 @@ def _run_service_command(action, service):
"""
services = _get_services()
if service not in services.keys():
raise MoulinetteError(errno.EINVAL, m18n.n('service_unknown', service=service))
raise YunohostError('service_unknown', service=service)
possible_actions = ['start', 'stop', 'restart', 'reload', 'enable', 'disable']
if action not in possible_actions:
@ -649,10 +638,10 @@ def _give_lock(action, service, p):
else:
systemctl_PID_name = "ControlPID"
cmd_get_son_PID ="systemctl show %s -p %s" % (service, systemctl_PID_name)
cmd_get_son_PID = "systemctl show %s -p %s" % (service, systemctl_PID_name)
son_PID = 0
# As long as we did not found the PID and that the command is still running
while son_PID == 0 and p.poll() == None:
while son_PID == 0 and p.poll() is None:
# Call systemctl to get the PID
# Output of the command is e.g. ControlPID=1234
son_PID = subprocess.check_output(cmd_get_son_PID.split()) \
@ -669,11 +658,12 @@ def _give_lock(action, service, p):
return son_PID
def _remove_lock(PID_to_remove):
# FIXME ironically not concurrency safe because it's not atomic...
PIDs = filesystem.read_file(MOULINETTE_LOCK).split("\n")
PIDs_to_keep = [ PID for PID in PIDs if int(PID) != PID_to_remove ]
PIDs_to_keep = [PID for PID in PIDs if int(PID) != PID_to_remove]
filesystem.write_to_file(MOULINETTE_LOCK, '\n'.join(PIDs_to_keep))
@ -787,6 +777,7 @@ def _find_previous_log_file(file):
return None
def _get_files_diff(orig_file, new_file, as_string=False, skip_header=True):
"""Compare two files and return the differences
@ -927,11 +918,11 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
"""
if save:
backup_path = os.path.join(BACKUP_CONF_DIR, '{0}-{1}'.format(
system_conf.lstrip('/'), time.strftime("%Y%m%d.%H%M%S")))
system_conf.lstrip('/'), datetime.utcnow().strftime("%Y%m%d.%H%M%S")))
backup_dir = os.path.dirname(backup_path)
if not os.path.isdir(backup_dir):
filesystem.mkdir(backup_dir, 0755, True)
filesystem.mkdir(backup_dir, 0o755, True)
shutil.copy2(system_conf, backup_path)
logger.debug(m18n.n('service_conf_file_backed_up',
@ -946,7 +937,7 @@ def _process_regen_conf(system_conf, new_conf=None, save=True):
system_dir = os.path.dirname(system_conf)
if not os.path.isdir(system_dir):
filesystem.mkdir(system_dir, 0755, True)
filesystem.mkdir(system_dir, 0o755, True)
shutil.copyfile(new_conf, system_conf)
logger.debug(m18n.n('service_conf_file_updated',

View file

@ -1,12 +1,11 @@
import os
import json
import errno
from datetime import datetime
from collections import OrderedDict
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
logger = getActionLogger('yunohost.settings')
@ -29,12 +28,17 @@ SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"
# * string
# * enum (in form a python list)
# we don't store the value in default options
DEFAULTS = OrderedDict([
("example.bool", {"type": "bool", "default": True}),
("example.int", {"type": "int", "default": 42}),
("example.string", {"type": "string", "default": "yolo swag"}),
("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),
# Password Validation
# -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
("security.password.admin.strength", {"type": "int", "default": 1}),
("security.password.user.strength", {"type": "int", "default": 1}),
("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
])
@ -49,8 +53,7 @@ def settings_get(key, full=False):
settings = _get_settings()
if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_key_doesnt_exists', settings_key=key))
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
if full:
return settings[key]
@ -78,36 +81,39 @@ def settings_set(key, value):
settings = _get_settings()
if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_key_doesnt_exists', settings_key=key))
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
key_type = settings[key]["type"]
if key_type == "bool":
if not isinstance(value, bool):
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type))
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
elif key_type == "int":
if not isinstance(value, int) or isinstance(value, bool):
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type))
if isinstance(value, str):
try:
value = int(value)
except:
raise YunohostError('global_settings_bad_type_for_setting',
setting=key,
received_type=type(value).__name__,
expected_type=key_type)
else:
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
elif key_type == "string":
if not isinstance(value, basestring):
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type))
raise YunohostError('global_settings_bad_type_for_setting', setting=key,
received_type=type(value).__name__, expected_type=key_type)
elif key_type == "enum":
if value not in settings[key]["choices"]:
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_bad_choice_for_enum', setting=key,
raise YunohostError('global_settings_bad_choice_for_enum', setting=key,
received_type=type(value).__name__,
expected_type=", ".join(settings[key]["choices"])))
expected_type=", ".join(settings[key]["choices"]))
else:
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_unknown_type', setting=key,
unknown_type=key_type))
raise YunohostError('global_settings_unknown_type', setting=key,
unknown_type=key_type)
settings[key]["value"] = value
@ -125,8 +131,7 @@ def settings_reset(key):
settings = _get_settings()
if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n(
'global_settings_key_doesnt_exists', settings_key=key))
raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
settings[key]["value"] = settings[key]["default"]
_save_settings(settings)
@ -147,7 +152,7 @@ def settings_reset_all():
# addition but we'll see if this is a common need.
# Another solution would be to use etckeeper and integrate those
# modification inside of it and take advantage of its git history
old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.now().strftime("%F_%X")
old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
_save_settings(settings, location=old_settings_backup_path)
for value in settings.values():
@ -202,8 +207,7 @@ def _get_settings():
setting_key=key))
unknown_settings[key] = value
except Exception as e:
raise MoulinetteError(errno.EIO, m18n.n('global_settings_cant_open_settings', reason=e),
exc_info=1)
raise YunohostError('global_settings_cant_open_settings', reason=e)
if unknown_settings:
try:
@ -224,14 +228,10 @@ def _save_settings(settings, location=SETTINGS_PATH):
try:
result = json.dumps(settings_without_description, indent=4)
except Exception as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('global_settings_cant_serialize_settings', reason=e),
exc_info=1)
raise YunohostError('global_settings_cant_serialize_settings', reason=e)
try:
with open(location, "w") as settings_fd:
settings_fd.write(result)
except Exception as e:
raise MoulinetteError(errno.EIO,
m18n.n('global_settings_cant_write_settings', reason=e),
exc_info=1)
raise YunohostError('global_settings_cant_write_settings', reason=e)

View file

@ -2,12 +2,10 @@
import re
import os
import errno
import pwd
import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config"
@ -23,7 +21,7 @@ def user_ssh_allow(auth, username):
# TODO it would be good to support different kind of shells
if not _get_user_for_ssh(auth, username):
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
raise YunohostError('user_unknown', user=username)
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'})
@ -42,7 +40,7 @@ def user_ssh_disallow(auth, username):
# TODO it would be good to support different kind of shells
if not _get_user_for_ssh(auth, username):
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
raise YunohostError('user_unknown', user=username)
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'})
@ -99,7 +97,7 @@ def user_ssh_add_key(auth, username, key, comment):
# create empty file to set good permissions
write_to_file(authorized_keys_file, "")
chown(authorized_keys_file, uid=user["uid"][0])
chmod(authorized_keys_file, 0600)
chmod(authorized_keys_file, 0o600)
authorized_keys_content = read_file(authorized_keys_file)

View file

@ -7,12 +7,14 @@ sys.path.append("..")
def pytest_addoption(parser):
parser.addoption("--yunodebug", action="store_true", default=False)
###############################################################################
#
# Tweak translator to raise exceptions if string keys are not defined #
###############################################################################
#
old_translate = moulinette.core.Translator.translate
def new_translate(self, key, *args, **kwargs):
if key not in self._translations[self.default_locale].keys():
@ -21,14 +23,15 @@ def new_translate(self, key, *args, **kwargs):
return old_translate(self, key, *args, **kwargs)
moulinette.core.Translator.translate = new_translate
def new_m18nn(self, key, *args, **kwargs):
return self._namespaces[self._current_namespace].translate(key, *args, **kwargs)
moulinette.core.Moulinette18n.n = new_m18nn
###############################################################################
#
# Init the moulinette to have the cli loggers stuff #
###############################################################################
#
def pytest_cmdline_main(config):

View file

@ -5,7 +5,7 @@ import requests_mock
import glob
import time
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from yunohost.app import app_fetchlist, app_removelist, app_listlists, _using_legacy_appslist_system, _migrate_appslist_system, _register_new_appslist
@ -17,7 +17,7 @@ APPSLISTS_JSON = '/etc/yunohost/appslists.json'
def setup_function(function):
# Clear all appslist
files = glob.glob(REPO_PATH+"/*")
files = glob.glob(REPO_PATH + "/*")
for f in files:
os.remove(f)
@ -42,9 +42,9 @@ def cron_job_is_there():
return r == 0
###############################################################################
#
# Test listing of appslists and registering of appslists #
###############################################################################
#
def test_appslist_list_empty():
@ -79,7 +79,7 @@ def test_appslist_list_register_conflict_name():
"""
_register_new_appslist("https://lol.com/appslist.json", "dummy")
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
_register_new_appslist("https://lol.com/appslist2.json", "dummy")
appslist_dict = app_listlists()
@ -94,7 +94,7 @@ def test_appslist_list_register_conflict_url():
"""
_register_new_appslist("https://lol.com/appslist.json", "dummy")
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
_register_new_appslist("https://lol.com/appslist.json", "plopette")
appslist_dict = app_listlists()
@ -103,9 +103,9 @@ def test_appslist_list_register_conflict_url():
assert "plopette" not in appslist_dict.keys()
###############################################################################
#
# Test fetching of appslists #
###############################################################################
#
def test_appslist_fetch():
@ -161,7 +161,7 @@ def test_appslist_fetch_unknownlist():
assert app_listlists() == {}
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_fetchlist(name="swag")
@ -170,7 +170,7 @@ def test_appslist_fetch_url_but_no_name():
Do a fetchlist with url given, but no name given
"""
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_fetchlist(url=URL_OFFICIAL_APP_LIST)
@ -244,9 +244,9 @@ def test_appslist_fetch_timeout():
app_fetchlist()
###############################################################################
#
# Test remove of appslist #
###############################################################################
#
def test_appslist_remove():
@ -270,13 +270,13 @@ def test_appslist_remove_unknown():
Attempt to remove an unknown list
"""
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_removelist("dummy")
###############################################################################
#
# Test migration from legacy appslist system #
###############################################################################
#
def add_legacy_cron(name, url):

View file

@ -1,7 +1,7 @@
import pytest
from moulinette.core import MoulinetteError, init_authenticator
from moulinette.core import init_authenticator
from yunohost.utils.error import YunohostError
from yunohost.app import app_install, app_remove
from yunohost.domain import _get_maindomain, domain_url_available, _normalize_domain_path
@ -22,6 +22,7 @@ def setup_function(function):
except:
pass
def teardown_function(function):
try:
@ -43,7 +44,7 @@ def test_urlavailable():
assert domain_url_available(auth, maindomain, "/macnuggets")
# We don't know the domain yolo.swag
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
assert domain_url_available(auth, "yolo.swag", "/macnuggets")
@ -55,13 +56,13 @@ def test_registerurl():
assert not domain_url_available(auth, maindomain, "/urlregisterapp")
# Try installing at same location
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_install(auth, "./tests/apps/register_url_app_ynh",
args="domain=%s&path=%s" % (maindomain, "/urlregisterapp"))
def test_registerurl_baddomain():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_install(auth, "./tests/apps/register_url_app_ynh",
args="domain=%s&path=%s" % ("yolo.swag", "/urlregisterapp"))

View file

@ -12,18 +12,22 @@ from yunohost.app import app_install, app_remove, app_ssowatconf
from yunohost.app import _is_installed
from yunohost.backup import backup_create, backup_restore, backup_list, backup_info, backup_delete
from yunohost.domain import _get_maindomain
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
# Get main domain
maindomain = _get_maindomain()
maindomain = ""
# Instantiate LDAP Authenticator
AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous')
AUTH_PARAMETERS = {'uri': 'ldap://localhost:389', 'base_dn': 'dc=yunohost,dc=org'}
auth = None
def setup_function(function):
global maindomain
maindomain = _get_maindomain()
print ""
global auth
@ -84,9 +88,9 @@ def teardown_function(function):
shutil.rmtree("/opt/test_backup_output_directory")
###############################################################################
#
# Helpers #
###############################################################################
#
def app_is_installed(app):
@ -108,6 +112,7 @@ def backup_test_dependencies_are_met():
return True
def tmp_backup_directory_is_empty():
if not os.path.exists("/home/yunohost.backup/tmp/"):
@ -115,6 +120,7 @@ def tmp_backup_directory_is_empty():
else:
return len(os.listdir('/home/yunohost.backup/tmp/')) == 0
def clean_tmp_backup_directory():
if tmp_backup_directory_is_empty():
@ -122,10 +128,10 @@ def clean_tmp_backup_directory():
mount_lines = subprocess.check_output("mount").split("\n")
points_to_umount = [ line.split(" ")[2]
points_to_umount = [line.split(" ")[2]
for line in mount_lines
if len(line) >= 3
and line.split(" ")[2].startswith("/home/yunohost.backup/tmp") ]
and line.split(" ")[2].startswith("/home/yunohost.backup/tmp")]
for point in reversed(points_to_umount):
os.system("umount %s" % point)
@ -135,6 +141,7 @@ def clean_tmp_backup_directory():
shutil.rmtree("/home/yunohost.backup/tmp/")
def reset_ssowat_conf():
# Make sure we have a ssowat
@ -188,14 +195,15 @@ def add_archive_system_from_2p4():
os.system("cp ./tests/apps/backup_system_from_2p4/backup.tar.gz \
/home/yunohost.backup/archives/backup_system_from_2p4.tar.gz")
###############################################################################
#
# System backup #
###############################################################################
#
def test_backup_only_ldap():
# Create the backup
backup_create(ignore_system=False, ignore_apps=True, system=["conf_ldap"])
backup_create(system=["conf_ldap"], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
@ -211,20 +219,21 @@ def test_backup_system_part_that_does_not_exists(mocker):
mocker.spy(m18n, "n")
# Create the backup
with pytest.raises(MoulinetteError):
backup_create(ignore_system=False, ignore_apps=True, system=["yolol"])
with pytest.raises(YunohostError):
backup_create(system=["yolol"], apps=None)
m18n.n.assert_any_call('backup_hook_unknown', hook="yolol")
m18n.n.assert_any_call('backup_nothings_done')
###############################################################################
#
# System backup and restore #
###############################################################################
#
def test_backup_and_restore_all_sys():
# Create the backup
backup_create(ignore_system=False, ignore_apps=True)
backup_create(system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 1
@ -241,40 +250,41 @@ def test_backup_and_restore_all_sys():
# Restore the backup
backup_restore(auth, name=archives[0], force=True,
ignore_system=False, ignore_apps=True)
system=[], apps=None)
# Check ssowat conf is back
assert os.path.exists("/etc/ssowat/conf.json")
###############################################################################
#
# System restore from 2.4 #
###############################################################################
#
@pytest.mark.with_system_archive_from_2p4
def test_restore_system_from_Ynh2p4(monkeypatch, mocker):
# Backup current system
backup_create(ignore_system=False, ignore_apps=True)
backup_create(system=[], apps=None)
archives = backup_list()["archives"]
assert len(archives) == 2
# Restore system archive from 2.4
try:
backup_restore(auth, name=backup_list()["archives"][1],
ignore_system=False,
ignore_apps=True,
system=[],
apps=None,
force=True)
finally:
# Restore system as it was
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=False,
ignore_apps=True,
system=[],
apps=None,
force=True)
###############################################################################
#
# App backup #
###############################################################################
#
@pytest.mark.with_backup_recommended_app_installed
def test_backup_script_failure_handling(monkeypatch, mocker):
@ -292,11 +302,12 @@ def test_backup_script_failure_handling(monkeypatch, mocker):
monkeypatch.setattr("yunohost.backup.hook_exec", custom_hook_exec)
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"])
with pytest.raises(YunohostError):
backup_create(system=None, apps=["backup_recommended_app"])
m18n.n.assert_any_call('backup_app_failed', app='backup_recommended_app')
@pytest.mark.with_backup_recommended_app_installed
def test_backup_not_enough_free_space(monkeypatch, mocker):
@ -312,8 +323,8 @@ def test_backup_not_enough_free_space(monkeypatch, mocker):
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"])
with pytest.raises(YunohostError):
backup_create(system=None, apps=["backup_recommended_app"])
m18n.n.assert_any_call('not_enough_disk_space', path=ANY)
@ -324,8 +335,8 @@ def test_backup_app_not_installed(mocker):
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_create(ignore_system=True, ignore_apps=False, apps=["wordpress"])
with pytest.raises(YunohostError):
backup_create(system=None, apps=["wordpress"])
m18n.n.assert_any_call("unbackup_app", app="wordpress")
m18n.n.assert_any_call('backup_nothings_done')
@ -340,8 +351,8 @@ def test_backup_app_with_no_backup_script(mocker):
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"])
with pytest.raises(YunohostError):
backup_create(system=None, apps=["backup_recommended_app"])
m18n.n.assert_any_call("backup_with_no_backup_script_for_app", app="backup_recommended_app")
m18n.n.assert_any_call('backup_nothings_done')
@ -359,7 +370,7 @@ def test_backup_app_with_no_restore_script(mocker):
# Backuping an app with no restore script will only display a warning to the
# user...
backup_create(ignore_system=True, ignore_apps=False, apps=["backup_recommended_app"])
backup_create(system=None, apps=["backup_recommended_app"])
m18n.n.assert_any_call("backup_with_no_restore_script_for_app", app="backup_recommended_app")
@ -368,7 +379,7 @@ def test_backup_app_with_no_restore_script(mocker):
def test_backup_with_different_output_directory():
# Create the backup
backup_create(ignore_system=False, ignore_apps=True, system=["conf_ssh"],
backup_create(system=["conf_ssh"], apps=None,
output_directory="/opt/test_backup_output_directory",
name="backup")
@ -382,10 +393,11 @@ def test_backup_with_different_output_directory():
assert len(archives_info["system"].keys()) == 1
assert "conf_ssh" in archives_info["system"].keys()
@pytest.mark.clean_opt_dir
def test_backup_with_no_compress():
# Create the backup
backup_create(ignore_system=False, ignore_apps=True, system=["conf_nginx"],
backup_create(system=["conf_nginx"], apps=None,
output_directory="/opt/test_backup_output_directory",
no_compress=True,
name="backup")
@ -393,16 +405,14 @@ def test_backup_with_no_compress():
assert os.path.exists("/opt/test_backup_output_directory/info.json")
###############################################################################
#
# App restore #
###############################################################################
#
@pytest.mark.with_wordpress_archive_from_2p4
def test_restore_app_wordpress_from_Ynh2p4():
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
@ -419,10 +429,8 @@ def test_restore_app_script_failure_handling(monkeypatch, mocker):
assert not _is_installed("wordpress")
with pytest.raises(MoulinetteError):
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
with pytest.raises(YunohostError):
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
m18n.n.assert_any_call('restore_app_failed', app='wordpress')
@ -442,10 +450,8 @@ def test_restore_app_not_enough_free_space(monkeypatch, mocker):
assert not _is_installed("wordpress")
with pytest.raises(MoulinetteError):
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
with pytest.raises(YunohostError):
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
m18n.n.assert_any_call('restore_not_enough_disk_space',
@ -463,10 +469,8 @@ def test_restore_app_not_in_backup(mocker):
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
with pytest.raises(YunohostError):
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["yoloswag"])
m18n.n.assert_any_call('backup_archive_app_not_found', app="yoloswag")
@ -479,18 +483,14 @@ def test_restore_app_already_installed(mocker):
assert not _is_installed("wordpress")
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
assert _is_installed("wordpress")
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_restore(auth, name=backup_list()["archives"][0],
ignore_system=True,
ignore_apps=False,
with pytest.raises(YunohostError):
backup_restore(auth, system=None, name=backup_list()["archives"][0],
apps=["wordpress"])
m18n.n.assert_any_call('restore_already_installed_app', app="wordpress")
@ -520,7 +520,7 @@ def test_backup_and_restore_with_ynh_restore():
def _test_backup_and_restore_app(app):
# Create a backup of this app
backup_create(ignore_system=True, ignore_apps=False, apps=[app])
backup_create(system=None, apps=[app])
archives = backup_list()["archives"]
assert len(archives) == 1
@ -535,14 +535,15 @@ def _test_backup_and_restore_app(app):
assert not app_is_installed(app)
# Restore the app
backup_restore(auth, name=archives[0], ignore_system=True,
ignore_apps=False, apps=[app])
backup_restore(auth, system=None, name=archives[0],
apps=[app])
assert app_is_installed(app)
###############################################################################
#
# Some edge cases #
###############################################################################
#
def test_restore_archive_with_no_json(mocker):
@ -553,9 +554,8 @@ def test_restore_archive_with_no_json(mocker):
assert "badbackup" in backup_list()["archives"]
mocker.spy(m18n, "n")
with pytest.raises(MoulinetteError):
backup_restore(auth, name="badbackup", force=True,
ignore_system=False, ignore_apps=False)
with pytest.raises(YunohostError):
backup_restore(auth, name="badbackup", force=True)
m18n.n.assert_any_call('backup_invalid_archive')
@ -567,7 +567,7 @@ def test_backup_binds_are_readonly(monkeypatch):
confssh = os.path.join(self.work_dir, "conf/ssh")
output = subprocess.check_output("touch %s/test 2>&1 || true" % confssh,
shell=True)
shell=True, env={'LANG': 'en_US.UTF-8'})
assert "Read-only file system" in output
@ -580,4 +580,4 @@ def test_backup_binds_are_readonly(monkeypatch):
custom_mount_and_backup)
# Create the backup
backup_create(ignore_system=False, ignore_apps=True)
backup_create(system=[])

View file

@ -6,7 +6,7 @@ from moulinette.core import init_authenticator
from yunohost.app import app_install, app_change_url, app_remove, app_map
from yunohost.domain import _get_maindomain
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
# Instantiate LDAP Authenticator
AUTH_IDENTIFIER = ('ldap', 'ldap-anonymous')
@ -38,7 +38,7 @@ def check_changeurl_app(path):
assert appmap[maindomain][path + "/"]["id"] == "change_url_app"
r = requests.get("https://%s%s/" % (maindomain, path), verify=False)
r = requests.get("https://127.0.0.1%s/" % path, headers={"domain": maindomain}, verify=False)
assert r.status_code == 200
@ -53,9 +53,10 @@ def test_appchangeurl():
check_changeurl_app("/newchangeurl")
def test_appchangeurl_sameurl():
install_changeurl_app("/changeurl")
check_changeurl_app("/changeurl")
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
app_change_url(auth, "change_url_app", maindomain, "changeurl")

View file

@ -2,7 +2,7 @@ import os
import json
import pytest
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from yunohost.settings import settings_get, settings_list, _get_settings, \
settings_set, settings_reset, settings_reset_all, \
@ -18,7 +18,8 @@ def teardown_function(function):
def test_settings_get_bool():
assert settings_get("example.bool") == True
assert settings_get("example.bool")
def test_settings_get_full_bool():
assert settings_get("example.bool", True) == {"type": "bool", "value": True, "default": True, "description": "Example boolean option"}
@ -27,6 +28,7 @@ def test_settings_get_full_bool():
def test_settings_get_int():
assert settings_get("example.int") == 42
def test_settings_get_full_int():
assert settings_get("example.int", True) == {"type": "int", "value": 42, "default": 42, "description": "Example int option"}
@ -34,6 +36,7 @@ def test_settings_get_full_int():
def test_settings_get_string():
assert settings_get("example.string") == "yolo swag"
def test_settings_get_full_string():
assert settings_get("example.string", True) == {"type": "string", "value": "yolo swag", "default": "yolo swag", "description": "Example string option"}
@ -41,12 +44,13 @@ def test_settings_get_full_string():
def test_settings_get_enum():
assert settings_get("example.enum") == "a"
def test_settings_get_full_enum():
assert settings_get("example.enum", True) == {"type": "enum", "value": "a", "default": "a", "description": "Example enum option", "choices": ["a", "b", "c"]}
def test_settings_get_doesnt_exists():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_get("doesnt.exists")
@ -70,39 +74,39 @@ def test_settings_set_enum():
def test_settings_set_doesexit():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("doesnt.exist", True)
def test_settings_set_bad_type_bool():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.bool", 42)
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.bool", "pouet")
def test_settings_set_bad_type_int():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.int", True)
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.int", "pouet")
def test_settings_set_bad_type_string():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.string", True)
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.string", 42)
def test_settings_set_bad_value_enum():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.enum", True)
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.enum", "e")
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.enum", 42)
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_set("example.enum", "pouet")
@ -119,7 +123,7 @@ def test_reset():
def test_settings_reset_doesexit():
with pytest.raises(MoulinetteError):
with pytest.raises(YunohostError):
settings_reset("doesnt.exist")
@ -152,7 +156,6 @@ def test_reset_all_backup():
assert settings_after_modification == json.load(open(old_settings_backup_path, "r"))
def test_unknown_keys():
unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown"
unknown_setting = {

View file

@ -27,8 +27,6 @@ import re
import os
import yaml
import json
import errno
import logging
import subprocess
import pwd
import socket
@ -40,7 +38,8 @@ import apt
import apt.progress
from moulinette import msettings, msignals, m18n
from moulinette.core import MoulinetteError, init_authenticator
from moulinette.core import init_authenticator
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_json, write_to_json
@ -112,13 +111,13 @@ def tools_ldapinit():
pwd.getpwnam("admin")
except KeyError:
logger.error(m18n.n('ldap_init_failed_to_create_admin'))
raise MoulinetteError(errno.EINVAL, m18n.n('installation_failed'))
raise YunohostError('installation_failed')
logger.success(m18n.n('ldap_initialized'))
return auth
def tools_adminpw(auth, new_password):
def tools_adminpw(auth, new_password, check_strength=True):
"""
Change admin password
@ -127,15 +126,35 @@ def tools_adminpw(auth, new_password):
"""
from yunohost.user import _hash_user_password
from yunohost.utils.password import assert_password_is_strong_enough
import spwd
if check_strength:
assert_password_is_strong_enough("admin", new_password)
new_hash = _hash_user_password(new_password)
try:
auth.update("cn=admin", {
"userPassword": _hash_user_password(new_password),
})
auth.update("cn=admin", {"userPassword": new_hash, })
except:
logger.exception('unable to change admin password')
raise MoulinetteError(errno.EPERM,
m18n.n('admin_password_change_failed'))
raise YunohostError('admin_password_change_failed')
else:
# Write as root password
try:
hash_root = spwd.getspnam("root").sp_pwd
with open('/etc/shadow', 'r') as before_file:
before = before_file.read()
with open('/etc/shadow', 'w') as after_file:
after_file.write(before.replace("root:" + hash_root,
"root:" + new_hash.replace('{CRYPT}', '')))
except IOError:
logger.warning(m18n.n('root_password_desynchronized'))
return
logger.info(m18n.n("root_password_replaced_by_admin_password"))
logger.success(m18n.n('admin_password_changed'))
@ -155,7 +174,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
# Check domain exists
if new_domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown'))
raise YunohostError('domain_unknown')
operation_logger.related_to.append(('domain', new_domain))
operation_logger.start()
@ -178,7 +197,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
_set_maindomain(new_domain)
except Exception as e:
logger.warning("%s" % e, exc_info=1)
raise MoulinetteError(errno.EPERM, m18n.n('maindomain_change_failed'))
raise YunohostError('maindomain_change_failed')
_set_hostname(new_domain)
@ -187,7 +206,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
# Regen configurations
try:
with open('/etc/yunohost/installed', 'r') as f:
with open('/etc/yunohost/installed', 'r'):
service_regen_conf()
except IOError:
pass
@ -227,7 +246,7 @@ def _set_hostname(hostname, pretty_hostname=None):
if p.returncode != 0:
logger.warning(command)
logger.warning(out)
raise MoulinetteError(errno.EIO, m18n.n('domain_hostname_failed'))
raise YunohostError('domain_hostname_failed')
else:
logger.debug(out)
@ -245,12 +264,13 @@ def _is_inside_container():
stderr=subprocess.STDOUT)
out, _ = p.communicate()
container = ['lxc','lxd','docker']
container = ['lxc', 'lxd', 'docker']
return out.split()[0] in container
@is_unit_operation()
def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
force_password=False):
"""
YunoHost post-install
@ -261,12 +281,17 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
password -- YunoHost admin password
"""
from yunohost.utils.password import assert_password_is_strong_enough
dyndns_provider = "dyndns.yunohost.org"
# Do some checks at first
if os.path.isfile('/etc/yunohost/installed'):
raise MoulinetteError(errno.EPERM,
m18n.n('yunohost_already_installed'))
raise YunohostError('yunohost_already_installed')
# Check password
if not force_password:
assert_password_is_strong_enough("admin", password)
if not ignore_dyndns:
# Check if yunohost dyndns can handle the given domain
@ -291,9 +316,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
dyndns = True
# If not, abort the postinstall
else:
raise MoulinetteError(errno.EEXIST,
m18n.n('dyndns_unavailable',
domain=domain))
raise YunohostError('dyndns_unavailable', domain=domain)
else:
dyndns = False
else:
@ -335,8 +358,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('ssowat_persistent_conf_read_error', error=str(e)))
raise YunohostError('ssowat_persistent_conf_read_error', error=str(e))
except IOError:
ssowat_conf = {}
@ -349,16 +371,16 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e:
raise MoulinetteError(errno.EPERM,
m18n.n('ssowat_persistent_conf_write_error', error=str(e)))
raise YunohostError('ssowat_persistent_conf_write_error', error=str(e))
os.system('chmod 644 /etc/ssowat/conf.json.persistent')
# Create SSL CA
service_regen_conf(['ssl'], force=True)
ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA'
# (Update the serial so that it's specific to this very instance)
os.system("openssl rand -hex 19 > %s/serial" % ssl_dir)
commands = [
'echo "01" > %s/serial' % ssl_dir,
'rm %s/index.txt' % ssl_dir,
'touch %s/index.txt' % ssl_dir,
'cp %s/openssl.cnf %s/openssl.ca.cnf' % (ssl_dir, ssl_dir),
@ -376,8 +398,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
if p.returncode != 0:
logger.warning(out)
raise MoulinetteError(errno.EPERM,
m18n.n('yunohost_ca_creation_failed'))
raise YunohostError('yunohost_ca_creation_failed')
else:
logger.debug(out)
@ -389,7 +410,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
tools_maindomain(auth, domain)
# Change LDAP admin password
tools_adminpw(auth, password)
tools_adminpw(auth, password, check_strength=not force_password)
# Enable UPnP silently and reload firewall
firewall_upnp('enable', no_refresh=True)
@ -404,7 +425,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
_install_appslist_fetch_cron()
# Init migrations (skip them, no need to run them on a fresh system)
tools_migrations_migrate(skip=True, auto=True)
_skip_all_migrations()
os.system('touch /etc/yunohost/installed')
@ -413,6 +434,24 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False):
service_start("yunohost-firewall")
service_regen_conf(force=True)
# Restore original ssh conf, as chosen by the
# admin during the initial install
#
# c.f. the install script and in particular
# https://github.com/YunoHost/install_script/pull/50
# The user can now choose during the install to keep
# the initial, existing sshd configuration
# instead of YunoHost's recommended conf
#
original_sshd_conf = '/etc/ssh/sshd_config.before_yunohost'
if os.path.exists(original_sshd_conf):
os.rename(original_sshd_conf, '/etc/ssh/sshd_config')
else:
# We need to explicitly ask the regen conf to regen ssh
# (by default, i.e. first argument = None, it won't because it's too touchy)
service_regen_conf(names=["ssh"], force=True)
logger.success(m18n.n('yunohost_configured'))
logger.warning(m18n.n('recommend_to_add_first_user'))
@ -435,7 +474,7 @@ def tools_update(ignore_apps=False, ignore_packages=False):
# Update APT cache
logger.debug(m18n.n('updating_apt_cache'))
if not cache.update():
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed'))
raise YunohostError('update_cache_failed')
cache.open(None)
cache.upgrade(True)
@ -454,7 +493,7 @@ def tools_update(ignore_apps=False, ignore_packages=False):
if not ignore_apps:
try:
app_fetchlist()
except MoulinetteError:
except YunohostError:
# FIXME : silent exception !?
pass
@ -534,7 +573,6 @@ def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=Fal
else:
logger.info(m18n.n('packages_no_upgrade'))
if not ignore_apps:
try:
app_upgrade(auth)
@ -585,7 +623,7 @@ def tools_diagnosis(auth, private=False):
diagnosis['system'] = OrderedDict()
try:
disks = monitor_disk(units=['filesystem'], human_readable=True)
except (MoulinetteError, Fault) as e:
except (YunohostError, Fault) as e:
logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
else:
diagnosis['system']['disks'] = {}
@ -601,7 +639,7 @@ def tools_diagnosis(auth, private=False):
try:
system = monitor_system(units=['cpu', 'memory'], human_readable=True)
except MoulinetteError as e:
except YunohostError as e:
logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1)
else:
diagnosis['system']['memory'] = {
@ -627,7 +665,7 @@ def tools_diagnosis(auth, private=False):
# YNH Applications
try:
applications = app_list()['apps']
except MoulinetteError as e:
except YunohostError as e:
diagnosis['applications'] = m18n.n('diagnosis_no_apps')
else:
diagnosis['applications'] = {}
@ -759,7 +797,7 @@ def tools_migrations_list(pending=False, done=False):
# Check for option conflict
if pending and done:
raise MoulinetteError(errno.EINVAL, m18n.n("migrations_list_conflict_pending_done"))
raise YunohostError("migrations_list_conflict_pending_done")
# Get all migrations
migrations = _get_migrations_list()
@ -774,12 +812,12 @@ def tools_migrations_list(pending=False, done=False):
migrations = [m for m in migrations if m.number > last_migration]
# Reduce to dictionnaries
migrations = [{ "id": migration.id,
migrations = [{"id": migration.id,
"number": migration.number,
"name": migration.name,
"mode": migration.mode,
"description": migration.description,
"disclaimer": migration.disclaimer } for migration in migrations ]
"disclaimer": migration.disclaimer} for migration in migrations]
return {"migrations": migrations}
@ -816,7 +854,7 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
# validate input, target must be "0" or a valid number
elif target != 0 and target not in all_migration_numbers:
raise MoulinetteError(errno.EINVAL, m18n.n('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers))))
raise YunohostError('migrations_bad_value_for_target', ", ".join(map(str, all_migration_numbers)))
logger.debug(m18n.n('migrations_current_target', target))
@ -844,38 +882,41 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
else: # can't happen, this case is handle before
raise Exception()
# If we are migrating in "automatic mode" (i.e. from debian
# configure during an upgrade of the package) but we are asked to run
# migrations is to be ran manually by the user
manual_migrations = [m for m in migrations if m.mode == "manual"]
if not skip and auto and manual_migrations:
for m in manual_migrations:
logger.warn(m18n.n('migrations_to_be_ran_manually',
number=m.number,
name=m.name))
return
# If some migrations have disclaimers, require the --accept-disclaimer
# option
migrations_with_disclaimer = [m for m in migrations if m.disclaimer]
if not skip and not accept_disclaimer and migrations_with_disclaimer:
for m in migrations_with_disclaimer:
logger.warn(m18n.n('migrations_need_to_accept_disclaimer',
number=m.number,
name=m.name,
disclaimer=m.disclaimer))
return
# effectively run selected migrations
for migration in migrations:
if not skip:
# If we are migrating in "automatic mode" (i.e. from debian configure
# during an upgrade of the package) but we are asked to run migrations
# to be ran manually by the user, stop there and ask the user to
# run the migration manually.
if auto and migration.mode == "manual":
logger.warn(m18n.n('migrations_to_be_ran_manually',
number=migration.number,
name=migration.name))
break
# If some migrations have disclaimers,
if migration.disclaimer:
# require the --accept-disclaimer option. Otherwise, stop everything
# here and display the disclaimer
if not accept_disclaimer:
logger.warn(m18n.n('migrations_need_to_accept_disclaimer',
number=migration.number,
name=migration.name,
disclaimer=migration.disclaimer))
break
# --accept-disclaimer will only work for the first migration
else:
accept_disclaimer = False
# Start register change on system
operation_logger= OperationLogger('tools_migrations_migrate_' + mode)
operation_logger = OperationLogger('tools_migrations_migrate_' + mode)
operation_logger.start()
if not skip:
logger.warn(m18n.n('migrations_show_currently_running_migration',
logger.info(m18n.n('migrations_show_currently_running_migration',
number=migration.number, name=migration.name))
try:
@ -896,6 +937,9 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
logger.error(msg, exc_info=1)
operation_logger.error(msg)
break
else:
logger.success(m18n.n('migrations_success',
number=migration.number, name=migration.name))
else: # if skip
logger.warn(m18n.n('migrations_skip_migration',
@ -910,6 +954,10 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
operation_logger.success()
# Skip migrations one at a time
if skip:
break
# special case where we want to go back from the start
if target == 0:
state["last_run_migration"] = None
@ -959,7 +1007,7 @@ def _get_migrations_list():
migrations = []
try:
import data_migrations
from . import data_migrations
except ImportError:
# not data migrations present, return empty list
return migrations
@ -982,7 +1030,7 @@ def _get_migration_by_name(migration_name):
"""
try:
import data_migrations
from . import data_migrations
except ImportError:
raise AssertionError("Unable to find migration with name %s" % migration_name)
@ -1013,8 +1061,28 @@ def _load_migration(migration_file):
import traceback
traceback.print_exc()
raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration',
number=number, name=name))
raise YunohostError('migrations_error_failed_to_load_migration',
number=number, name=name)
def _skip_all_migrations():
"""
Skip all pending migrations.
This is meant to be used during postinstall to
initialize the migration system.
"""
state = tools_migrations_state()
# load all migrations
migrations = _get_migrations_list()
migrations = sorted(migrations, key=lambda x: x.number)
last_migration = migrations[-1]
state["last_run_migration"] = {
"number": last_migration.number,
"name": last_migration.name
}
write_to_json(MIGRATIONS_STATE_PATH, state)
class Migration(object):

View file

@ -27,20 +27,20 @@ import os
import re
import pwd
import json
import errno
import crypt
import random
import string
import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from yunohost.service import service_status
from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user')
def user_list(auth, fields=None):
"""
List users
@ -71,8 +71,7 @@ def user_list(auth, fields=None):
if attr in keys:
attrs.append(attr)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('field_invalid', attr))
raise YunohostError('field_invalid', attr)
else:
attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell']
@ -116,6 +115,10 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
from yunohost.domain import domain_list, _get_maindomain
from yunohost.hook import hook_callback
from yunohost.app import app_ssowatconf
from yunohost.utils.password import assert_password_is_strong_enough
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", password)
# Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({
@ -126,13 +129,22 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
# Validate uniqueness of username in system users
all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
if username in all_existing_usernames:
raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists'))
raise YunohostError('system_username_exists')
main_domain = _get_maindomain()
aliases = [
'root@' + main_domain,
'admin@' + main_domain,
'webmaster@' + main_domain,
'postmaster@' + main_domain,
]
if mail in aliases:
raise YunohostError('mail_unavailable')
# Check that the mail domain exists
if mail.split("@")[1] not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
domain=mail.split("@")[1]))
raise YunohostError('mail_domain_unknown', domain=mail.split("@")[1])
operation_logger.start()
@ -166,13 +178,6 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
# If it is the first user, add some aliases
if not auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=*'):
main_domain = _get_maindomain()
aliases = [
'root@' + main_domain,
'admin@' + main_domain,
'webmaster@' + main_domain,
'postmaster@' + main_domain,
]
attr_dict['mail'] = [attr_dict['mail']] + aliases
# If exists, remove the redirection from the SSO
@ -180,8 +185,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e:
raise MoulinetteError(errno.EINVAL,
m18n.n('ssowat_persistent_conf_read_error', error=e.strerror))
raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror)
except IOError:
ssowat_conf = {}
@ -191,8 +195,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e:
raise MoulinetteError(errno.EPERM,
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror)
if auth.add('uid=%s,ou=users' % username, attr_dict):
# Invalidate passwd to take user creation into account
@ -218,7 +221,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
return {'fullname': fullname, 'username': username, 'mail': mail}
raise MoulinetteError(169, m18n.n('user_creation_failed'))
raise YunohostError('user_creation_failed')
@is_unit_operation([('username', 'user')])
@ -248,8 +251,9 @@ def user_delete(operation_logger, auth, username, purge=False):
if auth.update('cn=sftpusers,ou=groups', {'memberUid': memberlist}):
if purge:
subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)])
else:
raise MoulinetteError(169, m18n.n('user_deletion_failed'))
raise YunohostError('user_deletion_failed')
app_ssowatconf(auth)
@ -277,8 +281,9 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
remove_mailalias -- Mail aliases to remove
"""
from yunohost.domain import domain_list
from yunohost.domain import domain_list, _get_maindomain
from yunohost.app import app_ssowatconf
from yunohost.utils.password import assert_password_is_strong_enough
attrs_to_fetch = ['givenName', 'sn', 'mail', 'maildrop']
new_attr_dict = {}
@ -287,7 +292,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
# Populate user informations
result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch)
if not result:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
raise YunohostError('user_unknown', user=username)
user = result[0]
# Get modifications from arguments
@ -303,14 +308,25 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
new_attr_dict['cn'] = new_attr_dict['displayName'] = firstname + ' ' + lastname
if change_password:
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", change_password)
new_attr_dict['userPassword'] = _hash_user_password(change_password)
if mail:
main_domain = _get_maindomain()
aliases = [
'root@' + main_domain,
'admin@' + main_domain,
'webmaster@' + main_domain,
'postmaster@' + main_domain,
]
auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
domain=mail[mail.find('@') + 1:]))
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
if mail in aliases:
raise YunohostError('mail_unavailable')
del user['mail'][0]
new_attr_dict['mail'] = [mail] + user['mail']
@ -320,9 +336,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
for mail in add_mailalias:
auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_domain_unknown',
domain=mail[mail.find('@') + 1:]))
raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
user['mail'].append(mail)
new_attr_dict['mail'] = user['mail']
@ -333,8 +347,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
if len(user['mail']) > 1 and mail in user['mail'][1:]:
user['mail'].remove(mail)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_alias_remove_failed', mail=mail))
raise YunohostError('mail_alias_remove_failed', mail=mail)
new_attr_dict['mail'] = user['mail']
if add_mailforward:
@ -353,8 +366,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
user['maildrop'].remove(mail)
else:
raise MoulinetteError(errno.EINVAL,
m18n.n('mail_forward_remove_failed', mail=mail))
raise YunohostError('mail_forward_remove_failed', mail=mail)
new_attr_dict['maildrop'] = user['maildrop']
if mailbox_quota is not None:
@ -367,7 +379,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
app_ssowatconf(auth)
return user_info(auth, username)
else:
raise MoulinetteError(169, m18n.n('user_update_failed'))
raise YunohostError('user_update_failed')
def user_info(auth, username):
@ -392,7 +404,7 @@ def user_info(auth, username):
if result:
user = result[0]
else:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
raise YunohostError('user_unknown', user=username)
result_dict = {
'username': user['uid'][0],
@ -448,7 +460,7 @@ def user_info(auth, username):
if result:
return result_dict
else:
raise MoulinetteError(167, m18n.n('user_info_failed'))
raise YunohostError('user_info_failed')
#
# SSH subcategory
@ -456,18 +468,23 @@ def user_info(auth, username):
#
import yunohost.ssh
def user_ssh_allow(auth, username):
return yunohost.ssh.user_ssh_allow(auth, username)
def user_ssh_disallow(auth, username):
return yunohost.ssh.user_ssh_disallow(auth, username)
def user_ssh_list_keys(auth, username):
return yunohost.ssh.user_ssh_list_keys(auth, username)
def user_ssh_add_key(auth, username, key, comment):
return yunohost.ssh.user_ssh_add_key(auth, username, key, comment)
def user_ssh_remove_key(auth, username, key):
return yunohost.ssh.user_ssh_remove_key(auth, username, key)
@ -475,6 +492,7 @@ def user_ssh_remove_key(auth, username, key):
# End SSH subcategory
#
def _convertSize(num, suffix=''):
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
@ -510,6 +528,3 @@ def _hash_user_password(password):
salt = '$6$' + salt + '$'
return '{CRYPT}' + crypt.crypt(str(password), salt)

View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2018 YUNOHOST.ORG
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
from moulinette.core import MoulinetteError
from moulinette import m18n
class YunohostError(MoulinetteError):
"""
Yunohost base exception
The (only?) main difference with MoulinetteError being that keys
are translated via m18n.n (namespace) instead of m18n.g (global?)
"""
def __init__(self, key, __raw_msg__=False, *args, **kwargs):
if __raw_msg__:
msg = key
else:
msg = m18n.n(key, *args, **kwargs)
super(YunohostError, self).__init__(msg, __raw_msg__=True)

View file

@ -20,10 +20,12 @@
"""
import os
def free_space_in_directory(dirpath):
stat = os.statvfs(dirpath)
return stat.f_frsize * stat.f_bavail
def space_used_by_directory(dirpath):
stat = os.statvfs(dirpath)
return stat.f_frsize * stat.f_blocks

View file

@ -71,7 +71,7 @@ def get_gateway():
return addr.popitem()[1] if len(addr) == 1 else None
###############################################################################
#
def _extract_inet(string, skip_netmask=False, skip_loopback=True):

View file

@ -33,6 +33,7 @@ logger = logging.getLogger('yunohost.utils.packages')
# Exceptions -----------------------------------------------------------------
class PackageException(Exception):
"""Base exception related to a package
Represent an exception related to the package named `pkgname`. If no
@ -50,16 +51,19 @@ class PackageException(Exception):
class UnknownPackage(PackageException):
"""The package is not found in the cache."""
message_key = 'package_unknown'
class UninstalledPackage(PackageException):
"""The package is not installed."""
message_key = 'package_not_installed'
class InvalidSpecifier(ValueError):
"""An invalid specifier was found."""
@ -68,6 +72,7 @@ class InvalidSpecifier(ValueError):
# See: https://github.com/pypa/packaging
class Specifier(object):
"""Unique package version specifier
Restrict a package version according to the `spec`. It must be a string
@ -257,6 +262,7 @@ class Specifier(object):
class SpecifierSet(object):
"""A set of package version specifiers
Combine several Specifier separated by a comma. It allows to restrict

View file

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2018 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
import sys
import os
import json
import string
import subprocess
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin",
"root", "test", "rpi"]
MOST_USED_PASSWORDS = '/usr/share/yunohost/other/password/100000-most-used.txt'
# Length, digits, lowers, uppers, others
STRENGTH_LEVELS = [
(8, 0, 0, 0, 0),
(8, 1, 1, 1, 0),
(8, 1, 1, 1, 1),
(12, 1, 1, 1, 1),
]
def assert_password_is_strong_enough(profile, password):
PasswordValidator(profile).validate(password)
class PasswordValidator(object):
def __init__(self, profile):
"""
Initialize a password validator.
The profile shall be either "user" or "admin"
and will correspond to a validation strength
defined via the setting "security.password.<profile>.strength"
"""
self.profile = profile
try:
# We do this "manually" instead of using settings_get()
# from settings.py because this file is also meant to be
# use as a script by ssowat.
# (or at least that's my understanding -- Alex)
settings = json.load(open('/etc/yunohost/settings.json', "r"))
setting_key = "security.password." + profile + ".strength"
self.validation_strength = int(settings[setting_key]["value"])
except Exception as e:
# Fallback to default value if we can't fetch settings for some reason
self.validation_strength = 1
def validate(self, password):
"""
Check the validation_summary and trigger an exception
if the password does not pass tests.
This method is meant to be used from inside YunoHost's code
(compared to validation_summary which is meant to be called
by ssowat)
"""
if self.validation_strength == -1:
return
# Note that those imports are made here and can't be put
# on top (at least not the moulinette ones)
# because the moulinette needs to be correctly initialized
# as well as modules available in python's path.
import logging
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
logger = logging.getLogger('yunohost.utils.password')
status, msg = self.validation_summary(password)
if status == "error":
raise YunohostError(msg)
def validation_summary(self, password):
"""
Check if a password is listed in the list of most used password
and if the overall strength is good enough compared to the
validation_strength defined in the constructor.
Produces a summary-tuple comprised of a level (succes or error)
and a message key describing the issues found.
"""
if self.validation_strength < 0:
return ("success", "")
listed = password in SMALL_PWD_LIST or self.is_in_most_used_list(password)
strength_level = self.strength_level(password)
if listed:
return ("error", "password_listed")
if strength_level < self.validation_strength:
return ("error", "password_too_simple_%s" % self.validation_strength)
return ("success", "")
def strength(self, password):
"""
Returns the strength of a password, defined as a tuple
containing the length of the password, the number of digits,
lowercase letters, uppercase letters, and other characters.
For instance, "PikachuDu67" is (11, 2, 7, 2, 0)
"""
length = len(password)
digits = 0
uppers = 0
lowers = 0
others = 0
for character in password:
if character in string.digits:
digits = digits + 1
elif character in string.ascii_uppercase:
uppers = uppers + 1
elif character in string.ascii_lowercase:
lowers = lowers + 1
else:
others = others + 1
return (length, digits, lowers, uppers, others)
def strength_level(self, password):
"""
Computes the strength of a password and compares
it to the STRENGTH_LEVELS.
Returns an int corresponding to the highest STRENGTH_LEVEL
satisfied by the password.
"""
strength = self.strength(password)
strength_level = 0
# Iterate over each level and its criterias
for level, level_criterias in enumerate(STRENGTH_LEVELS):
# Iterate simulatenously over the level criterias (e.g. [8, 1, 1, 1, 0])
# and the strength of the password (e.g. [11, 2, 7, 2, 0])
# and compare the values 1-by-1.
# If one False is found, the password does not satisfy the level
if False in [s >= c for s, c in zip(strength, level_criterias)]:
break
# Otherwise, the strength of the password is at least of the current level.
strength_level = level + 1
return strength_level
def is_in_most_used_list(self, password):
# Decompress file if compressed
if os.path.exists("%s.gz" % MOST_USED_PASSWORDS):
os.system("gzip -fd %s.gz" % MOST_USED_PASSWORDS)
# Grep the password in the file
# We use '-f -' to feed the pattern (= the password) through
# stdin to avoid it being shown in ps -ef --forest...
command = "grep -q -f - %s" % MOST_USED_PASSWORDS
p = subprocess.Popen(command.split(), stdin=subprocess.PIPE)
p.communicate(input=password)
return not bool(p.returncode)
# This file is also meant to be used as an executable by
# SSOwat to validate password from the portal when an user
# change its password.
if __name__ == '__main__':
if len(sys.argv) < 2:
import getpass
pwd = getpass.getpass("")
# print("usage: password.py PASSWORD")
else:
pwd = sys.argv[1]
status, msg = PasswordValidator('user').validation_summary(pwd)
print(msg)
sys.exit(0)

View file

@ -2,9 +2,9 @@
import requests
import json
import errno
from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
def yunopaste(data):
@ -13,17 +13,14 @@ def yunopaste(data):
try:
r = requests.post("%s/documents" % paste_server, data=data, timeout=30)
except Exception as e:
raise MoulinetteError(errno.EIO,
"Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e))
raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s" % str(e), raw_msg=True)
if r.status_code != 200:
raise MoulinetteError(errno.EIO,
"Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text))
raise YunohostError("Something wrong happened while trying to paste data on paste.yunohost.org : %s, %s" % (r.status_code, r.text), raw_msg=True)
try:
url = json.loads(r.text)["key"]
except:
raise MoulinetteError(errno.EIO,
"Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text)
raise YunohostError("Uhoh, couldn't parse the answer from paste.yunohost.org : %s" % r.text, raw_msg=True)
return "%s/raw/%s" % (paste_server, url)