Merge branch 'stretch-unstable' of github.com:eauchat/yunohost into stretch-unstable

This commit is contained in:
chateau 2018-12-16 22:29:43 +01:00
commit 06522b1fa8
43 changed files with 1192 additions and 984 deletions

3
.gitignore vendored
View file

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

View file

@ -5,7 +5,7 @@ ip=$(hostname --all-ip-address)
# Fetch SSH fingerprints # Fetch SSH fingerprints
i=0 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) output=$(ssh-keygen -l -f $key)
fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)" fingerprint[$i]=" - $(echo $output | cut -d' ' -f2) $(echo $output| cut -d' ' -f4)"
i=$(($i + 1)) i=$(($i + 1))

View file

@ -558,6 +558,10 @@ app:
full: --no-remove-on-failure full: --no-remove-on-failure
help: Debug option to avoid removing the app on a failed installation help: Debug option to avoid removing the app on a failed installation
action: store_true 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 ### app_remove() TODO: Write help
remove: remove:

View file

@ -10,12 +10,13 @@ CAN_BIND=${CAN_BIND:-1}
# #
# If DEST is ended by a slash it complete this path with the basename of SRC. # If DEST is ended by a slash it complete this path with the basename of SRC.
# #
# usage: ynh_backup src [dest [is_big [arg]]] # usage: ynh_backup src [dest [is_big [not_mandatory [arg]]]]
# | arg: src - file or directory to bind or symlink or copy. it shouldn't be in # | arg: src - file or directory to bind or symlink or copy. it shouldn't be in
# the backup dir. # the backup dir.
# | arg: dest - destination file or directory inside the # | arg: dest - destination file or directory inside the
# backup dir # backup dir
# | arg: is_big - 1 to indicate data are big (mail, video, image ...) # | arg: is_big - 1 to indicate data are big (mail, video, image ...)
# | arg: not_mandatory - 1 to indicate that if the file is missing, the backup can ignore it.
# | arg: arg - Deprecated arg # | arg: arg - Deprecated arg
# #
# example: # example:
@ -46,6 +47,7 @@ ynh_backup() {
local SRC_PATH="$1" local SRC_PATH="$1"
local DEST_PATH="${2:-}" local DEST_PATH="${2:-}"
local IS_BIG="${3:-0}" local IS_BIG="${3:-0}"
local NOT_MANDATORY="${4:-0}"
BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0} BACKUP_CORE_ONLY=${BACKUP_CORE_ONLY:-0}
# If backing up core only (used by ynh_backup_before_upgrade), # If backing up core only (used by ynh_backup_before_upgrade),
@ -60,15 +62,18 @@ ynh_backup() {
# ============================================================================== # ==============================================================================
# Be sure the source path is not empty # Be sure the source path is not empty
[[ -e "${SRC_PATH}" ]] || { [[ -e "${SRC_PATH}" ]] || {
echo "!!! Source path '${SRC_PATH}' does not exist !!!" >&2 if [ "$NOT_MANDATORY" == "0" ]
# This is a temporary fix for fail2ban config files missing after the migration to stretch.
if echo "${SRC_PATH}" | grep --quiet "/etc/fail2ban"
then then
touch "${SRC_PATH}" # This is a temporary fix for fail2ban config files missing after the migration to stretch.
echo "The missing file will be replaced by a dummy one for the backup !!!" >&2 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 else
return 1 return 0
fi fi
} }
@ -176,12 +181,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 # Use the registered path in backup_list by ynh_backup to restore the file at
# the good place. # the good place.
# #
# usage: ynh_restore_file ORIGIN_PATH [ DEST_PATH ] # usage: ynh_restore_file ORIGIN_PATH [ DEST_PATH [NOT_MANDATORY]]
# | arg: ORIGIN_PATH - Path where was located the file or the directory before # | arg: ORIGIN_PATH - Path where was located the file or the directory before
# to be backuped or relative path to $YNH_CWD where it is located in the backup archive # to be backuped or relative path to $YNH_CWD where it is located in the backup archive
# | arg: DEST_PATH - Path where restore the file or the dir, if unspecified, # | arg: DEST_PATH - Path where restore the file or the dir, if unspecified,
# the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in # the destination will be ORIGIN_PATH or if the ORIGIN_PATH doesn't exist in
# the archive, the destination will be searched into backup.csv # the archive, the destination will be searched into backup.csv
# | arg: NOT_MANDATORY - 1 to indicate that if the file is missing, the restore process can ignore it.
# #
# If DEST_PATH already exists and is lighter than 500 Mo, a backup will be made in # 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. # /home/yunohost.conf/backup/. Otherwise, the existing file is removed.
@ -201,10 +207,16 @@ ynh_restore_file () {
local ARCHIVE_PATH="$YNH_CWD${ORIGIN_PATH}" local ARCHIVE_PATH="$YNH_CWD${ORIGIN_PATH}"
# Default value for DEST_PATH = /$ORIGIN_PATH # Default value for DEST_PATH = /$ORIGIN_PATH
local DEST_PATH="${2:-$ORIGIN_PATH}" local DEST_PATH="${2:-$ORIGIN_PATH}"
local NOT_MANDATORY="${3:-0}"
# If ARCHIVE_PATH doesn't exist, search for a corresponding path in CSV # If ARCHIVE_PATH doesn't exist, search for a corresponding path in CSV
if [ ! -d "$ARCHIVE_PATH" ] && [ ! -f "$ARCHIVE_PATH" ] && [ ! -L "$ARCHIVE_PATH" ]; then if [ ! -d "$ARCHIVE_PATH" ] && [ ! -f "$ARCHIVE_PATH" ] && [ ! -L "$ARCHIVE_PATH" ]; then
ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")" if [ "$NOT_MANDATORY" == "0" ]
then
ARCHIVE_PATH="$YNH_BACKUP_DIR/$(_get_archive_path \"$ORIGIN_PATH\")"
else
return 0
fi
fi fi
# Move the old directory if it already exists # Move the old directory if it already exists

View file

@ -272,6 +272,7 @@ ynh_local_curl () {
ynh_render_template() { ynh_render_template() {
local template_path=$1 local template_path=$1
local output_path=$2 local output_path=$2
mkdir -p "$(dirname $output_path)"
# Taken from https://stackoverflow.com/a/35009576 # Taken from https://stackoverflow.com/a/35009576
python2.7 -c 'import os, sys, jinja2; sys.stdout.write( python2.7 -c 'import os, sys, jinja2; sys.stdout.write(
jinja2.Template(sys.stdin.read() jinja2.Template(sys.stdin.read()

View file

@ -2,28 +2,53 @@
set -e set -e
. /usr/share/yunohost/helpers.d/utils
do_pre_regen() { do_pre_regen() {
pending_dir=$1 pending_dir=$1
cd /usr/share/yunohost/templates/ssh # If the (legacy) 'from_script' flag is here,
# we won't touch anything in the ssh config.
[[ ! -f /etc/yunohost/from_script ]] || return 0
# only overwrite SSH configuration on an ISO installation cd /usr/share/yunohost/templates/ssh
if [[ ! -f /etc/yunohost/from_script ]]; then
# do not listen to IPv6 if unavailable # do not listen to IPv6 if unavailable
[[ -f /proc/net/if_inet6 ]] \ [[ -f /proc/net/if_inet6 ]] && ipv6_enabled=true || ipv6_enabled=false
|| sed -i "s/ListenAddress ::/#ListenAddress ::/g" sshd_config
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)
fi 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() { do_post_regen() {
regen_conf_files=$1 regen_conf_files=$1
if [[ ! -f /etc/yunohost/from_script ]]; then # If the (legacy) 'from_script' flag is here,
[[ -z "$regen_conf_files" ]] \ # we won't touch anything in the ssh config.
|| sudo service ssh restart [[ ! -f /etc/yunohost/from_script ]] || return 0
fi
# 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} FORCE=${2:-0}

View file

@ -1,96 +1,78 @@
# Package generated configuration file # This configuration has been automatically generated
# See the sshd_config(5) manpage for details # 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 Protocol 2
# HostKeys for protocol version 2 Port 22
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_dsa_key
#Privilege Separation is turned on for security
UsePrivilegeSeparation yes
# Lifetime and size of ephemeral version 1 server key {% if ipv6_enabled == "true" %}ListenAddress ::{% endif %}
KeyRegenerationInterval 3600 ListenAddress 0.0.0.0
ServerKeyBits 768
# 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 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 LoginGraceTime 120
PermitRootLogin no PermitRootLogin no
StrictModes yes StrictModes yes
RSAAuthentication yes
PubkeyAuthentication 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 PermitEmptyPasswords no
# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
ChallengeResponseAuthentication no 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 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 Match User sftpusers
ForceCommand internal-sftp ForceCommand internal-sftp
ChrootDirectory /home/%u ChrootDirectory /home/%u
AllowTcpForwarding no AllowTcpForwarding no
GatewayPorts no GatewayPorts no
X11Forwarding 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

18
debian/changelog vendored
View file

@ -1,17 +1,19 @@
yunohost (3.3.3) stable; urgency=low
* [fix] ynh_wait_dpkg_free displaying a warning despite everything being okay (#593)
* [fix] Quotes for recommended CAA DNS record (#596)
* [fix] Manual migration and disclaimer behaviors (#594)
* [fix] Explicit root password change each time admin password is changed
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 09 Dec 2018 20:58:00 +0000
yunohost (3.3.2) stable; urgency=low yunohost (3.3.2) stable; urgency=low
* [fix] Regen nginx conf to be sure it integrates OCSP Stapling (#588) * [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] Broken new settings and options to control passwords checks / constrains (#589)
* [fix] Log dyndns update only if we really update something (#591) * [fix] Log dyndns update only if we really update something (#591)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 02 Dev 2018 17:23:00 +0000 -- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 02 Dec 2018 17:23:00 +0000
yunohost (3.3.2) stable; urgency=low
* [fix] Log dyndns update only if we really update something (#591)
* [fix] Broken new settings and options to control passwords checks / constrains (#589)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sun, 02 Dev 2018 17:17:00 +0000
yunohost (3.3.1) stable; urgency=low yunohost (3.3.1) stable; urgency=low

View file

@ -1,4 +1,5 @@
{ {
"aborting": "Aborting.",
"action_invalid": "Invalid action '{action:s}'", "action_invalid": "Invalid action '{action:s}'",
"admin_password": "Administration password", "admin_password": "Administration password",
"admin_password_change_failed": "Unable to change 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_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_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})", "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_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", "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}", "diagnosis_debian_version_error": "Can't retrieve the Debian version: {error}",
@ -194,7 +198,10 @@
"global_settings_setting_example_enum": "Example enum option", "global_settings_setting_example_enum": "Example enum option",
"global_settings_setting_example_int": "Example int option", "global_settings_setting_example_int": "Example int option",
"global_settings_setting_example_string": "Example string option", "global_settings_setting_example_string": "Example string option",
"global_settings_setting_security_password_admin_strength": "Admin password strength",
"global_settings_setting_security_password_user_strength": "User password strength",
"global_settings_unknown_setting_from_settings_file": "Unknown key in settings: '{setting_key:s}', discarding it and save it in /etc/yunohost/unkown_settings.json", "global_settings_unknown_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.", "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_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).", "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).",
@ -211,7 +218,7 @@
"log_category_404": "The log category '{category}' does not exist", "log_category_404": "The log category '{category}' does not exist",
"log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'", "log_link_to_log": "Full log of this operation: '<a href=\"#/tools/logs/{name}\" style=\"text-decoration:underline\">{desc}</a>'",
"log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'", "log_help_to_get_log": "To view the log of the operation '{desc}', use the command 'yunohost log display {name}'",
"log_link_to_failed_log": "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_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_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'", "log_does_exists": "There is not operation log with the name '{log}', use 'yunohost log list to see all available operation logs'",
@ -274,6 +281,8 @@
"migration_description_0004_php5_to_php7_pools": "Reconfigure the PHP pools to use PHP 7 instead of 5", "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_0005_postgresql_9p4_to_9p6": "Migrate databases from postgresql 9.4 to 9.6",
"migration_description_0006_sync_admin_and_root_passwords": "Synchronize admin and root passwords", "migration_description_0006_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_backward_impossible": "The stretch migration cannot be reverted.",
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.", "migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
"migration_0003_patching_sources_list": "Patching the sources.lists ...", "migration_0003_patching_sources_list": "Patching the sources.lists ...",
@ -291,7 +300,14 @@
"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_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_0005_not_enough_space": "Not enough space is available in {path} to run the migration right now :(.",
"migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.", "migration_0006_disclaimer": "Yunohost now expects admin and root passwords to be synchronized. By running this migration, your root password is going to be replaced by the admin password.",
"migration_0006_done": "Your root password have been replaced by your admin password.", "migration_0007_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_backward": "Migrating backward.",
"migrations_bad_value_for_target": "Invalid number for target argument, available migrations numbers are 0 or {}", "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", "migrations_cant_reach_migration_file": "Can't access migrations files at path %s",
@ -305,6 +321,7 @@
"migrations_show_currently_running_migration": "Running migration {number} {name}...", "migrations_show_currently_running_migration": "Running migration {number} {name}...",
"migrations_show_last_migration": "Last ran migration is {}", "migrations_show_last_migration": "Last ran migration is {}",
"migrations_skip_migration": "Skipping migration {number} {name}...", "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_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.", "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", "monitor_disabled": "The server monitoring has been disabled",
@ -375,6 +392,7 @@
"restore_running_hooks": "Running restoration hooks...", "restore_running_hooks": "Running restoration hooks...",
"restore_system_part_failed": "Unable to restore the '{part:s}' system part", "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_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": "The server will shutdown",
"server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]", "server_shutdown_confirm": "The server will shutdown immediatly, are you sure? [{answers:s}]",
"server_reboot": "The server will reboot", "server_reboot": "The server will reboot",
@ -392,7 +410,7 @@
"service_conf_file_remove_failed": "Unable to remove the configuration file '{conf}'", "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_removed": "The configuration file '{conf}' has been removed",
"service_conf_file_updated": "The configuration file '{conf}' has been updated", "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_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_updated": "The configuration has been updated for service '{service}'",
"service_conf_would_be_updated": "The configuration would have been updated for service '{service}'", "service_conf_would_be_updated": "The configuration would have been updated for service '{service}'",

View file

@ -30,15 +30,15 @@ import yaml
import time import time
import re import re
import urlparse import urlparse
import errno
import subprocess import subprocess
import glob import glob
import pwd import pwd
import grp import grp
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime
from moulinette import msignals, m18n, msettings 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.log import getActionLogger
from moulinette.utils.filesystem import read_json from moulinette.utils.filesystem import read_json
@ -80,6 +80,12 @@ def app_listlists():
# Get the list # Get the list
appslist_list = _read_appslist_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 return appslist_list
@ -118,14 +124,12 @@ def app_fetchlist(url=None, name=None):
appslists_to_be_fetched = [name] appslists_to_be_fetched = [name]
operation_logger.success() operation_logger.success()
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('custom_appslist_name_required')
m18n.n('custom_appslist_name_required'))
# If a name is given, look for an appslist with that name and fetch it # If a name is given, look for an appslist with that name and fetch it
elif name is not None: elif name is not None:
if name not in appslists.keys(): if name not in appslists.keys():
raise MoulinetteError(errno.EINVAL, raise YunohostError('appslist_unknown', appslist=name)
m18n.n('appslist_unknown', appslist=name))
else: else:
appslists_to_be_fetched = [name] appslists_to_be_fetched = [name]
@ -133,7 +137,7 @@ def app_fetchlist(url=None, name=None):
else: else:
appslists_to_be_fetched = appslists.keys() appslists_to_be_fetched = appslists.keys()
import requests # lazy loading this module for performance reasons import requests # lazy loading this module for performance reasons
# Fetch all appslists to be fetched # Fetch all appslists to be fetched
for name in appslists_to_be_fetched: for name in appslists_to_be_fetched:
@ -168,7 +172,7 @@ def app_fetchlist(url=None, name=None):
appslist = appslist_request.text appslist = appslist_request.text
try: try:
json.loads(appslist) json.loads(appslist)
except ValueError, e: except ValueError as e:
logger.error(m18n.n('appslist_retrieve_bad_format', logger.error(m18n.n('appslist_retrieve_bad_format',
appslist=name)) appslist=name))
continue continue
@ -179,9 +183,7 @@ def app_fetchlist(url=None, name=None):
with open(list_file, "w") as f: with open(list_file, "w") as f:
f.write(appslist) f.write(appslist)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise YunohostError("Error while writing appslist %s: %s" % (name, str(e)), raw_msg=True)
"Error while writing appslist %s: %s" %
(name, str(e)))
now = int(time.time()) now = int(time.time())
appslists[name]["lastUpdate"] = now appslists[name]["lastUpdate"] = now
@ -205,7 +207,7 @@ def app_removelist(operation_logger, name):
# Make sure we know this appslist # Make sure we know this appslist
if name not in appslists.keys(): 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() operation_logger.start()
@ -336,8 +338,7 @@ def app_info(app, show_status=False, raw=False):
""" """
if not _is_installed(app): if not _is_installed(app):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
app_setting_path = APPS_SETTING_PATH + app app_setting_path = APPS_SETTING_PATH + app
@ -395,8 +396,7 @@ def app_map(app=None, raw=False, user=None):
if app is not None: if app is not None:
if not _is_installed(app): if not _is_installed(app):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
apps = [app, ] apps = [app, ]
else: else:
apps = os.listdir(APPS_SETTING_PATH) apps = os.listdir(APPS_SETTING_PATH)
@ -448,11 +448,10 @@ def app_change_url(operation_logger, auth, app, domain, path):
installed = _is_installed(app) installed = _is_installed(app)
if not installed: if not installed:
raise MoulinetteError(errno.ENOPKG, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")): if not os.path.exists(os.path.join(APPS_SETTING_PATH, app, "scripts", "change_url")):
raise 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_domain = app_setting(app, "domain")
old_path = app_setting(app, "path") old_path = app_setting(app, "path")
@ -464,7 +463,7 @@ def app_change_url(operation_logger, auth, app, domain, path):
path = normalize_url_path(path) path = normalize_url_path(path)
if (domain, path) == (old_domain, old_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 # WARNING / FIXME : checkurl will modify the settings
# (this is a non intuitive behavior that should be changed) # (this is a non intuitive behavior that should be changed)
@ -540,10 +539,10 @@ def app_change_url(operation_logger, auth, app, domain, path):
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
shell=True).rstrip() 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", logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path)) app=app, domain=domain, path=path))
hook_callback('post_app_change_url', args=args_list, env=env_dict) hook_callback('post_app_change_url', args=args_list, env=env_dict)
@ -565,8 +564,8 @@ def app_upgrade(auth, app=[], url=None, file=None):
try: try:
app_list() app_list()
except MoulinetteError: except YunohostError:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) raise YunohostError('app_no_upgrade')
upgraded_apps = [] upgraded_apps = []
@ -586,8 +585,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name)) logger.info(m18n.n('app_upgrade_app_name', app=app_instance_name))
installed = _is_installed(app_instance_name) installed = _is_installed(app_instance_name)
if not installed: if not installed:
raise MoulinetteError(errno.ENOPKG, raise YunohostError('app_not_installed', app=app_instance_name)
m18n.n('app_not_installed', app=app_instance_name))
if app_instance_name in upgraded_apps: if app_instance_name in upgraded_apps:
continue continue
@ -677,7 +675,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
operation_logger.success() operation_logger.success()
if not upgraded_apps: if not upgraded_apps:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) raise YunohostError('app_no_upgrade')
app_ssowatconf(auth) app_ssowatconf(auth)
@ -689,7 +687,7 @@ def app_upgrade(auth, app=[], url=None, file=None):
@is_unit_operation() @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 Install apps
@ -698,12 +696,11 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
label -- Custom name for the app label -- Custom name for the app
args -- Serialize arguments for app installation args -- Serialize arguments for app installation
no_remove_on_failure -- Debug option to avoid removing the app on a failed 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.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.log import OperationLogger from yunohost.log import OperationLogger
# Fetch or extract sources # Fetch or extract sources
try: try:
os.listdir(INSTALL_TMP) os.listdir(INSTALL_TMP)
@ -718,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) manifest, extracted_app_folder = _fetch_app_from_git(app)
elif os.path.exists(app): elif os.path.exists(app):
confirm_install("thirdparty")
manifest, extracted_app_folder = _extract_app_from_file(app) manifest, extracted_app_folder = _extract_app_from_file(app)
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) raise YunohostError('app_unknown')
status['remote'] = manifest.get('remote', {}) status['remote'] = manifest.get('remote', {})
# Check ID # Check ID
if 'id' not in manifest or '__' in manifest['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'] app_id = manifest['id']
@ -739,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 instance_number = _installed_instance_number(app_id, last=True) + 1
if instance_number > 1: if instance_number > 1:
if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']): if 'multi_instance' not in manifest or not is_true(manifest['multi_instance']):
raise MoulinetteError(errno.EEXIST, raise YunohostError('app_already_installed', app=app_id)
m18n.n('app_already_installed', app=app_id))
# Change app_id to the forked app id # Change app_id to the forked app id
app_instance_name = app_id + '__' + str(instance_number) app_instance_name = app_id + '__' + str(instance_number)
@ -761,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) env_dict["YNH_APP_INSTANCE_NUMBER"] = str(instance_number)
# Start register change on system # 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 = [s for s in operation_logger.related_to if s[0] != "app"]
operation_logger.related_to.append(("app", app_id)) operation_logger.related_to.append(("app", app_id))
operation_logger.start() operation_logger.start()
@ -819,8 +844,8 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
# Execute remove script # Execute remove script
operation_logger_remove = OperationLogger('remove_on_failed_install', operation_logger_remove = OperationLogger('remove_on_failed_install',
[('app', app_instance_name)], [('app', app_instance_name)],
env=env_dict_remove) env=env_dict_remove)
operation_logger_remove.start() operation_logger_remove.start()
remove_retcode = hook_exec( remove_retcode = hook_exec(
@ -843,9 +868,9 @@ def app_install(operation_logger, auth, app, label=None, args=None, no_remove_on
if install_retcode == -1: if install_retcode == -1:
msg = m18n.n('operation_interrupted') + " " + error_msg msg = m18n.n('operation_interrupted') + " " + error_msg
raise MoulinetteError(errno.EINTR, msg) raise YunohostError(msg, raw_msg=True)
msg = error_msg msg = error_msg
raise MoulinetteError(errno.EIO, msg) raise YunohostError(msg, raw_msg=True)
# Clean hooks and add new ones # Clean hooks and add new ones
hook_remove(app_instance_name) hook_remove(app_instance_name)
@ -881,8 +906,7 @@ def app_remove(operation_logger, auth, app):
""" """
from yunohost.hook import hook_exec, hook_remove, hook_callback from yunohost.hook import hook_exec, hook_remove, hook_callback
if not _is_installed(app): if not _is_installed(app):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
operation_logger.start() operation_logger.start()
@ -948,7 +972,6 @@ def app_addaccess(auth, apps, users=[]):
for app in apps: for app in apps:
app_settings = _get_app_settings(app) app_settings = _get_app_settings(app)
if not app_settings: if not app_settings:
continue continue
@ -961,7 +984,7 @@ def app_addaccess(auth, apps, users=[]):
# Start register change on system # Start register change on system
related_to = [('app', app)] related_to = [('app', app)]
operation_logger= OperationLogger('app_addaccess', related_to) operation_logger = OperationLogger('app_addaccess', related_to)
operation_logger.start() operation_logger.start()
allowed_users = set() allowed_users = set()
@ -972,7 +995,7 @@ def app_addaccess(auth, apps, users=[]):
if allowed_user not in allowed_users: if allowed_user not in allowed_users:
try: try:
user_info(auth, allowed_user) user_info(auth, allowed_user)
except MoulinetteError: except YunohostError:
logger.warning(m18n.n('user_unknown', user=allowed_user)) logger.warning(m18n.n('user_unknown', user=allowed_user))
continue continue
allowed_users.add(allowed_user) allowed_users.add(allowed_user)
@ -1024,7 +1047,7 @@ def app_removeaccess(auth, apps, users=[]):
# Start register change on system # Start register change on system
related_to = [('app', app)] related_to = [('app', app)]
operation_logger= OperationLogger('app_removeaccess', related_to) operation_logger = OperationLogger('app_removeaccess', related_to)
operation_logger.start() operation_logger.start()
if remove_all: if remove_all:
@ -1038,7 +1061,7 @@ def app_removeaccess(auth, apps, users=[]):
if allowed_user not in users: if allowed_user not in users:
allowed_users.add(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() operation_logger.flush()
new_users = ','.join(allowed_users) new_users = ','.join(allowed_users)
app_setting(app, 'allowed_users', new_users) app_setting(app, 'allowed_users', new_users)
@ -1073,7 +1096,7 @@ def app_clearaccess(auth, apps):
# Start register change on system # Start register change on system
related_to = [('app', app)] related_to = [('app', app)]
operation_logger= OperationLogger('app_clearaccess', related_to) operation_logger = OperationLogger('app_clearaccess', related_to)
operation_logger.start() operation_logger.start()
if 'mode' in app_settings: if 'mode' in app_settings:
@ -1130,23 +1153,19 @@ def app_makedefault(operation_logger, auth, app, domain=None):
if domain is None: if domain is None:
domain = app_domain 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']: elif domain not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n('domain_unknown')) raise YunohostError('domain_unknown')
operation_logger.start() operation_logger.start()
if '/' in app_map(raw=True)[domain]: if '/' in app_map(raw=True)[domain]:
raise MoulinetteError(errno.EEXIST, raise YunohostError('app_make_default_location_already_used', app=app, domain=app_domain, other_app=app_map(raw=True)[domain]["/"]["id"])
m18n.n('app_make_default_location_already_used',
app=app, domain=app_domain,
other_app=app_map(raw=True)[domain]["/"]["id"]))
try: try:
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror)
m18n.n('ssowat_persistent_conf_read_error', error=e.strerror))
except IOError: except IOError:
ssowat_conf = {} ssowat_conf = {}
@ -1159,8 +1178,7 @@ def app_makedefault(operation_logger, auth, app, domain=None):
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EPERM, raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror)
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
os.system('chmod 644 /etc/ssowat/conf.json.persistent') os.system('chmod 644 /etc/ssowat/conf.json.persistent')
@ -1212,8 +1230,7 @@ def app_checkport(port):
if tools_port_available(port): if tools_port_available(port):
logger.success(m18n.n('port_available', port=int(port))) logger.success(m18n.n('port_available', port=int(port)))
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('port_unavailable', port=int(port))
m18n.n('port_unavailable', port=int(port)))
def app_register_url(auth, app, domain, path): def app_register_url(auth, app, domain, path):
@ -1228,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 # This line can't be moved on top of file, otherwise it creates an infinite
# loop of import with tools.py... # 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) domain, path = _normalize_domain_path(domain, path)
@ -1240,8 +1257,7 @@ def app_register_url(auth, app, domain, path):
if installed: if installed:
settings = _get_app_settings(app) settings = _get_app_settings(app)
if "path" in settings.keys() and "domain" in settings.keys(): if "path" in settings.keys() and "domain" in settings.keys():
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_already_installed_cant_change_url')
m18n.n('app_already_installed_cant_change_url'))
# Check the url is available # Check the url is available
conflicts = _get_conflicting_apps(auth, domain, path) conflicts = _get_conflicting_apps(auth, domain, path)
@ -1255,7 +1271,7 @@ def app_register_url(auth, app, domain, path):
app_label=app_label, 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, 'domain', value=domain)
app_setting(app, 'path', value=path) app_setting(app, 'path', value=path)
@ -1293,7 +1309,7 @@ def app_checkurl(auth, url, app=None):
apps_map = app_map(raw=True) apps_map = app_map(raw=True)
if domain not in domain_list(auth)['domains']: 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: if domain in apps_map:
# Loop through apps # Loop through apps
@ -1303,14 +1319,10 @@ def app_checkurl(auth, url, app=None):
installed = True installed = True
continue continue
if path == p: if path == p:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_location_already_used', app=a["id"], path=path)
m18n.n('app_location_already_used',
app=a["id"], path=path))
# can't install "/a/b/" if "/a/" exists # can't install "/a/b/" if "/a/" exists
elif path.startswith(p) or p.startswith(path): elif path.startswith(p) or p.startswith(path):
raise MoulinetteError(errno.EPERM, raise YunohostError('app_location_install_failed', other_path=p, other_app=a['id'])
m18n.n('app_location_install_failed',
other_path=p, other_app=a['id']))
if app is not None and not installed: if app is not None and not installed:
app_setting(app, 'domain', value=domain) app_setting(app, 'domain', value=domain)
@ -1342,10 +1354,10 @@ def app_initdb(user, password=None, db=None, sql=None):
mysql_root_pwd = open('/etc/yunohost/mysql').read().rstrip() 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) 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: 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 sql is not None:
if os.system('mysql -u %s -p%s %s < %s' % (user, password, db, sql)) != 0: 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: if return_pwd:
return password return password
@ -1451,8 +1463,7 @@ def app_ssowatconf(auth):
def app_change_label(auth, app, new_label): def app_change_label(auth, app, new_label):
installed = _is_installed(app) installed = _is_installed(app)
if not installed: if not installed:
raise MoulinetteError(errno.ENOPKG, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
app_setting(app, "label", value=new_label) app_setting(app, "label", value=new_label)
@ -1488,7 +1499,7 @@ def app_action_run(app, action, args=None):
actions = {x["id"]: x for x in actions} actions = {x["id"]: x for x in actions}
if action not 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] action_declaration = actions[action]
@ -1526,7 +1537,7 @@ def app_action_run(app, action, args=None):
) )
if retcode not in action_declaration.get("accepted_return_codes", [0]): 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) os.remove(path)
@ -1585,10 +1596,10 @@ def app_config_show_panel(app):
parsed_values[key] = value parsed_values[key] = value
return_code = hook_exec(config_script, return_code = hook_exec(config_script,
args=["show"], args=["show"],
env=env, env=env,
stdout_callback=parse_stdout, stdout_callback=parse_stdout,
) )
if return_code != 0: if return_code != 0:
raise Exception("script/config show return value code: %s (considered as an error)", return_code) raise Exception("script/config show return value code: %s (considered as an error)", return_code)
@ -1633,8 +1644,7 @@ def app_config_apply(app, args):
installed = _is_installed(app) installed = _is_installed(app)
if not installed: if not installed:
raise MoulinetteError(errno.ENOPKG, raise YunohostError('app_not_installed', app=app)
m18n.n('app_not_installed', app=app))
config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json') config_panel = os.path.join(APPS_SETTING_PATH, app, 'config_panel.json')
config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config') config_script = os.path.join(APPS_SETTING_PATH, app, 'scripts', 'config')
@ -1673,9 +1683,9 @@ def app_config_apply(app, args):
logger.warning("Ignore key '%s' from arguments because it is not in the config", key) logger.warning("Ignore key '%s' from arguments because it is not in the config", key)
return_code = hook_exec(config_script, return_code = hook_exec(config_script,
args=["apply"], args=["apply"],
env=env, env=env,
) )
if return_code != 0: if return_code != 0:
raise Exception("'script/config apply' return value code: %s (considered as an error)", return_code) raise Exception("'script/config apply' return value code: %s (considered as an error)", return_code)
@ -1692,8 +1702,7 @@ def _get_app_settings(app_id):
""" """
if not _is_installed(app_id): if not _is_installed(app_id):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_not_installed', app=app_id)
m18n.n('app_not_installed', app=app_id))
try: try:
with open(os.path.join( with open(os.path.join(
APPS_SETTING_PATH, app_id, 'settings.yml')) as f: APPS_SETTING_PATH, app_id, 'settings.yml')) as f:
@ -1731,7 +1740,7 @@ def _get_app_status(app_id, format_date=False):
""" """
app_setting_path = APPS_SETTING_PATH + app_id app_setting_path = APPS_SETTING_PATH + app_id
if not os.path.isdir(app_setting_path): if not os.path.isdir(app_setting_path):
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) raise YunohostError('app_unknown')
status = {} status = {}
try: try:
@ -1755,8 +1764,7 @@ def _get_app_status(app_id, format_date=False):
if not v: if not v:
status[f] = '-' status[f] = '-'
else: else:
status[f] = time.strftime(m18n.n('format_datetime_short'), status[f] = datetime.utcfromtimestamp(v)
time.gmtime(v))
return status return status
@ -1797,7 +1805,7 @@ def _extract_app_from_file(path, remove=False):
extract_result = 1 extract_result = 1
if extract_result != 0: if extract_result != 0:
raise MoulinetteError(errno.EINVAL, m18n.n('app_extraction_failed')) raise YunohostError('app_extraction_failed')
try: try:
extracted_app_folder = APP_TMP_FOLDER extracted_app_folder = APP_TMP_FOLDER
@ -1808,10 +1816,9 @@ def _extract_app_from_file(path, remove=False):
manifest = json.loads(str(json_manifest.read())) manifest = json.loads(str(json_manifest.read()))
manifest['lastUpdate'] = int(time.time()) manifest['lastUpdate'] = int(time.time())
except IOError: except IOError:
raise MoulinetteError(errno.EIO, m18n.n('app_install_files_invalid')) raise YunohostError('app_install_files_invalid')
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_manifest_invalid', error=e.strerror)
m18n.n('app_manifest_invalid', error=e.strerror))
logger.debug(m18n.n('done')) logger.debug(m18n.n('done'))
@ -1879,8 +1886,7 @@ def _fetch_app_from_git(app):
'wget', '-qO', app_tmp_archive, tarball_url]) 'wget', '-qO', app_tmp_archive, tarball_url])
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logger.exception('unable to download %s', tarball_url) logger.exception('unable to download %s', tarball_url)
raise MoulinetteError(errno.EIO, raise YunohostError('app_sources_fetch_failed')
m18n.n('app_sources_fetch_failed'))
else: else:
manifest, extracted_app_folder = _extract_app_from_file( manifest, extracted_app_folder = _extract_app_from_file(
app_tmp_archive, remove=True) app_tmp_archive, remove=True)
@ -1903,11 +1909,9 @@ def _fetch_app_from_git(app):
with open(extracted_app_folder + '/manifest.json') as f: with open(extracted_app_folder + '/manifest.json') as f:
manifest = json.loads(str(f.read())) manifest = json.loads(str(f.read()))
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise MoulinetteError(errno.EIO, raise YunohostError('app_sources_fetch_failed')
m18n.n('app_sources_fetch_failed'))
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EIO, raise YunohostError('app_manifest_invalid', error=e.strerror)
m18n.n('app_manifest_invalid', error=e.strerror))
else: else:
logger.debug(m18n.n('done')) logger.debug(m18n.n('done'))
@ -1927,11 +1931,10 @@ def _fetch_app_from_git(app):
app_info['manifest']['lastUpdate'] = app_info['lastUpdate'] app_info['manifest']['lastUpdate'] = app_info['lastUpdate']
manifest = app_info['manifest'] manifest = app_info['manifest']
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('app_unknown')) raise YunohostError('app_unknown')
if 'git' not in app_info: if 'git' not in app_info:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_unsupported_remote_type')
m18n.n('app_unsupported_remote_type'))
url = app_info['git']['url'] url = app_info['git']['url']
if 'github.com' in url: if 'github.com' in url:
@ -1943,8 +1946,7 @@ def _fetch_app_from_git(app):
'wget', '-qO', app_tmp_archive, tarball_url]) 'wget', '-qO', app_tmp_archive, tarball_url])
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
logger.exception('unable to download %s', tarball_url) logger.exception('unable to download %s', tarball_url)
raise MoulinetteError(errno.EIO, raise YunohostError('app_sources_fetch_failed')
m18n.n('app_sources_fetch_failed'))
else: else:
manifest, extracted_app_folder = _extract_app_from_file( manifest, extracted_app_folder = _extract_app_from_file(
app_tmp_archive, remove=True) app_tmp_archive, remove=True)
@ -1960,11 +1962,9 @@ def _fetch_app_from_git(app):
with open(extracted_app_folder + '/manifest.json') as f: with open(extracted_app_folder + '/manifest.json') as f:
manifest = json.loads(str(f.read())) manifest = json.loads(str(f.read()))
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise MoulinetteError(errno.EIO, raise YunohostError('app_sources_fetch_failed')
m18n.n('app_sources_fetch_failed'))
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EIO, raise YunohostError('app_manifest_invalid', error=e.strerror)
m18n.n('app_manifest_invalid', error=e.strerror))
else: else:
logger.debug(m18n.n('done')) logger.debug(m18n.n('done'))
@ -2083,7 +2083,7 @@ def _check_manifest_requirements(manifest, app_instance_name):
yunohost_req = requirements.get('yunohost', None) yunohost_req = requirements.get('yunohost', None)
if (not yunohost_req or if (not yunohost_req or
not packages.SpecifierSet(yunohost_req) & '>= 2.3.6'): 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.g('colon', m18n.n('app_incompatible'), app=app_instance_name),
m18n.n('app_package_need_update', app=app_instance_name))) m18n.n('app_package_need_update', app=app_instance_name)))
elif not requirements: elif not requirements:
@ -2096,18 +2096,15 @@ def _check_manifest_requirements(manifest, app_instance_name):
versions = packages.get_installed_version( versions = packages.get_installed_version(
*requirements.keys(), strict=True, as_dict=True) *requirements.keys(), strict=True, as_dict=True)
except packages.PackageException as e: except packages.PackageException as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_requirements_failed', error=str(e), app=app_instance_name)
m18n.n('app_requirements_failed',
error=str(e), app=app_instance_name))
# Iterate over requirements # Iterate over requirements
for pkgname, spec in requirements.items(): for pkgname, spec in requirements.items():
version = versions[pkgname] version = versions[pkgname]
if version not in packages.SpecifierSet(spec): if version not in packages.SpecifierSet(spec):
raise MoulinetteError( raise YunohostError('app_requirements_unmeet',
errno.EINVAL, m18n.n('app_requirements_unmeet', pkgname=pkgname, version=version,
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): def _parse_args_from_manifest(manifest, action, args={}, auth=None):
@ -2215,7 +2212,6 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
elif arg_type == 'password': elif arg_type == 'password':
msignals.display(m18n.n('good_practices_about_user_password')) msignals.display(m18n.n('good_practices_about_user_password'))
try: try:
input_string = msignals.prompt(ask_string, is_password) input_string = msignals.prompt(ask_string, is_password)
except NotImplementedError: except NotImplementedError:
@ -2231,36 +2227,27 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
# Validate argument value # Validate argument value
if (arg_value is None or arg_value == '') \ if (arg_value is None or arg_value == '') \
and not arg.get('optional', False): and not arg.get('optional', False):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_required', name=arg_name)
m18n.n('app_argument_required', name=arg_name))
elif arg_value is None: elif arg_value is None:
args_dict[arg_name] = '' args_dict[arg_name] = ''
continue continue
# Validate argument choice # Validate argument choice
if arg_choices and arg_value not in arg_choices: if arg_choices and arg_value not in arg_choices:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_choice_invalid', name=arg_name, choices=', '.join(arg_choices))
m18n.n('app_argument_choice_invalid',
name=arg_name, choices=', '.join(arg_choices)))
# Validate argument type # Validate argument type
if arg_type == 'domain': if arg_type == 'domain':
if arg_value not in domain_list(auth)['domains']: if arg_value not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('domain_unknown'))
m18n.n('app_argument_invalid',
name=arg_name, error=m18n.n('domain_unknown')))
elif arg_type == 'user': elif arg_type == 'user':
try: try:
user_info(auth, arg_value) user_info(auth, arg_value)
except MoulinetteError as e: except YunohostError as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_invalid', name=arg_name, error=e.strerror)
m18n.n('app_argument_invalid',
name=arg_name, error=e.strerror))
elif arg_type == 'app': elif arg_type == 'app':
if not _is_installed(arg_value): if not _is_installed(arg_value):
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_invalid', name=arg_name, error=m18n.n('app_unknown'))
m18n.n('app_argument_invalid',
name=arg_name, error=m18n.n('app_unknown')))
elif arg_type == 'boolean': elif arg_type == 'boolean':
if isinstance(arg_value, bool): if isinstance(arg_value, bool):
arg_value = 1 if arg_value else 0 arg_value = 1 if arg_value else 0
@ -2270,9 +2257,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
elif str(arg_value).lower() in ["0", "no", "n"]: elif str(arg_value).lower() in ["0", "no", "n"]:
arg_value = 0 arg_value = 0
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('app_argument_choice_invalid', name=arg_name, choices='yes, no, y, n, 1, 0')
m18n.n('app_argument_choice_invalid',
name=arg_name, choices='yes, no, y, n, 1, 0'))
elif arg_type == 'password': elif arg_type == 'password':
from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.password import assert_password_is_strong_enough
assert_password_is_strong_enough('user', arg_value) assert_password_is_strong_enough('user', arg_value)
@ -2306,7 +2291,7 @@ def _parse_action_args_in_yunohost_format(args, action_args, auth=None):
app_label=app_label, 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 # (We save this normalized path so that the install script have a
# standard path format to deal with no matter what the user inputted) # standard path format to deal with no matter what the user inputted)
@ -2426,7 +2411,7 @@ def _install_appslist_fetch_cron():
with open(cron_job_file, "w") as f: with open(cron_job_file, "w") as f:
f.write('\n'.join(cron_job)) 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 # FIXME - Duplicate from certificate.py, should be moved into a common helper
@ -2456,8 +2441,7 @@ def _read_appslist_list():
try: try:
appslists = json.loads(appslists_json) appslists = json.loads(appslists_json)
except ValueError: except ValueError:
raise MoulinetteError(errno.EBADR, raise YunohostError('appslist_corrupted_json', filename=APPSLISTS_JSON)
m18n.n('appslist_corrupted_json', filename=APPSLISTS_JSON))
return appslists return appslists
@ -2472,9 +2456,8 @@ def _write_appslist_list(appslist_lists):
with open(APPSLISTS_JSON, "w") as f: with open(APPSLISTS_JSON, "w") as f:
json.dump(appslist_lists, f) json.dump(appslist_lists, f)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise YunohostError("Error while writing list of appslist %s: %s" %
"Error while writing list of appslist %s: %s" % (APPSLISTS_JSON, str(e)), raw_msg=True)
(APPSLISTS_JSON, str(e)))
def _register_new_appslist(url, name): def _register_new_appslist(url, name):
@ -2487,15 +2470,13 @@ def _register_new_appslist(url, name):
# Check if name conflicts with an existing list # Check if name conflicts with an existing list
if name in appslist_list: if name in appslist_list:
raise MoulinetteError(errno.EEXIST, raise YunohostError('appslist_name_already_tracked', name=name)
m18n.n('appslist_name_already_tracked', name=name))
# Check if url conflicts with an existing list # Check if url conflicts with an existing list
known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()] known_appslist_urls = [appslist["url"] for _, appslist in appslist_list.items()]
if url in known_appslist_urls: if url in known_appslist_urls:
raise MoulinetteError(errno.EEXIST, raise YunohostError('appslist_url_already_tracked', url=url)
m18n.n('appslist_url_already_tracked', url=url))
logger.debug("Registering new appslist %s at %s" % (name, url)) logger.debug("Registering new appslist %s at %s" % (name, url))
@ -2586,7 +2567,7 @@ def _patch_php5(app_folder):
continue continue
c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' " \ c = "sed -i -e 's@/etc/php5@/etc/php/7.0@g' " \
"-e 's@/var/run/php5-fpm@/var/run/php/php7.0-fpm@g' " \ "-e 's@/var/run/php5-fpm@/var/run/php/php7.0-fpm@g' " \
"-e 's@php5@php7.0@g' " \ "-e 's@php5@php7.0@g' " \
"%s" % filename "%s" % filename
os.system(c) os.system(c)

View file

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

View file

@ -24,7 +24,6 @@
import os import os
import sys import sys
import errno
import shutil import shutil
import pwd import pwd
import grp import grp
@ -37,7 +36,7 @@ from datetime import datetime
from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate 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 moulinette.utils.log import getActionLogger
from yunohost.utils.network import get_public_ip from yunohost.utils.network import get_public_ip
@ -81,9 +80,9 @@ DNS_RESOLVERS = [
"80.67.188.188" # LDN "80.67.188.188" # LDN
] ]
############################################################################### #
# Front-end stuff # # Front-end stuff #
############################################################################### #
def certificate_status(auth, domain_list, full=False): 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: for domain in domain_list:
# Is it in Yunohost domain list? # Is it in Yunohost domain list?
if domain not in yunohost_domains_list: if domain not in yunohost_domains_list:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_unknown', domain=domain)
'certmanager_domain_unknown', domain=domain))
certificates = {} certificates = {}
@ -151,10 +149,10 @@ def _certificate_install_selfsigned(domain_list, force=False):
for domain in domain_list: for domain in domain_list:
operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)], operation_logger = OperationLogger('selfsigned_cert_install', [('domain', domain)],
args={'force': force}) args={'force': force})
# Paths of files and folder we'll need # 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" % ( new_cert_folder = "%s/%s-history/%s-selfsigned" % (
CERT_FOLDER, domain, date_tag) CERT_FOLDER, domain, date_tag)
@ -172,8 +170,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
status = _get_status(domain) status = _get_status(domain)
if status["summary"]["code"] in ('good', 'great'): if status["summary"]["code"] in ('good', 'great'):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_attempt_to_replace_valid_cert', domain=domain)
'certmanager_attempt_to_replace_valid_cert', domain=domain))
operation_logger.start() operation_logger.start()
@ -203,8 +200,7 @@ def _certificate_install_selfsigned(domain_list, force=False):
if p.returncode != 0: if p.returncode != 0:
logger.warning(out) logger.warning(out)
raise MoulinetteError( raise YunohostError('domain_cert_gen_failed')
errno.EIO, m18n.n('domain_cert_gen_failed'))
else: else:
logger.debug(out) logger.debug(out)
@ -219,10 +215,10 @@ def _certificate_install_selfsigned(domain_list, force=False):
crt_pem.write(ca_pem.read()) crt_pem.write(ca_pem.read())
# Set appropriate permissions # Set appropriate permissions
_set_permissions(new_cert_folder, "root", "root", 0755) _set_permissions(new_cert_folder, "root", "root", 0o755)
_set_permissions(key_file, "root", "ssl-cert", 0640) _set_permissions(key_file, "root", "ssl-cert", 0o640)
_set_permissions(crt_file, "root", "ssl-cert", 0640) _set_permissions(crt_file, "root", "ssl-cert", 0o640)
_set_permissions(conf_file, "root", "root", 0600) _set_permissions(conf_file, "root", "root", 0o600)
# Actually enable the certificate we created # Actually enable the certificate we created
_enable_certificate(domain, new_cert_folder) _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: for domain in domain_list:
yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] yunohost_domains_list = yunohost.domain.domain_list(auth)['domains']
if domain not in yunohost_domains_list: if domain not in yunohost_domains_list:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_unknown', domain=domain)
'certmanager_domain_unknown', domain=domain))
# Is it self-signed? # Is it self-signed?
status = _get_status(domain) status = _get_status(domain)
if not force and status["CA_type"]["code"] != "self-signed": if not force and status["CA_type"]["code"] != "self-signed":
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_cert_not_selfsigned', domain=domain)
'certmanager_domain_cert_not_selfsigned', domain=domain))
if staging: if staging:
logger.warning( logger.warning(
@ -279,8 +273,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F
for domain in domain_list: for domain in domain_list:
operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)], operation_logger = OperationLogger('letsencrypt_cert_install', [('domain', domain)],
args={'force': force, 'no_checks': no_checks, args={'force': force, 'no_checks': no_checks,
'staging': staging}) 'staging': staging})
logger.info( logger.info(
"Now attempting install of certificate for domain %s!", domain) "Now attempting install of certificate for domain %s!", domain)
@ -304,6 +298,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F
logger.error(msg) logger.error(msg)
operation_logger.error(msg) operation_logger.error(msg)
def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): 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) 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? # Is it in Yunohost dmomain list?
if domain not in yunohost.domain.domain_list(auth)['domains']: if domain not in yunohost.domain.domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_unknown', domain=domain)
'certmanager_domain_unknown', domain=domain))
status = _get_status(domain) status = _get_status(domain)
# Does it expire soon? # Does it expire soon?
if status["validity"] > VALIDITY_LIMIT and not force: if status["validity"] > VALIDITY_LIMIT and not force:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_attempt_to_renew_valid_cert', domain=domain)
'certmanager_attempt_to_renew_valid_cert', domain=domain))
# Does it have a Let's Encrypt cert? # Does it have a Let's Encrypt cert?
if status["CA_type"]["code"] != "lets-encrypt": if status["CA_type"]["code"] != "lets-encrypt":
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_attempt_to_renew_nonLE_cert', domain=domain)
'certmanager_attempt_to_renew_nonLE_cert', domain=domain))
# Check ACME challenge configured for given domain # Check ACME challenge configured for given domain
if not _check_acme_challenge_configuration(domain): if not _check_acme_challenge_configuration(domain):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_acme_not_configured_for_domain', domain=domain)
'certmanager_acme_not_configured_for_domain', domain=domain))
if staging: if staging:
logger.warning( logger.warning(
@ -377,8 +368,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal
for domain in domain_list: for domain in domain_list:
operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)], operation_logger = OperationLogger('letsencrypt_cert_renew', [('domain', domain)],
args={'force': force, 'no_checks': no_checks, args={'force': force, 'no_checks': no_checks,
'staging': staging, 'email': email}) 'staging': staging, 'email': email})
logger.info( logger.info(
"Now attempting renewing of certificate for domain %s !", domain) "Now attempting renewing of certificate for domain %s !", domain)
@ -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 ...") logger.error("Sending email with details to root ...")
_email_renewing_failed(domain, e, stack.getvalue()) _email_renewing_failed(domain, e, stack.getvalue())
############################################################################### #
# Back-end stuff # # Back-end stuff #
############################################################################### #
def _install_cron(): def _install_cron():
cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" cron_job_file = "/etc/cron.daily/yunohost-certificate-renew"
@ -422,7 +414,7 @@ def _install_cron():
f.write("#!/bin/bash\n") f.write("#!/bin/bash\n")
f.write("yunohost domain cert-renew --email\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): def _email_renewing_failed(domain, exception_message, stack):
@ -484,8 +476,7 @@ location ^~ '/.well-known/acme-challenge/'
contents = f.read() contents = f.read()
if '/.well-known/acme-challenge' in contents: if '/.well-known/acme-challenge' in contents:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_conflicting_nginx_file', filepath=path)
'certmanager_conflicting_nginx_file', filepath=path))
# Write the conf # Write the conf
if os.path.exists(nginx_conf_file): 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): if not os.path.exists(TMP_FOLDER):
os.makedirs(TMP_FOLDER) os.makedirs(TMP_FOLDER)
_set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0o650)
_set_permissions(TMP_FOLDER, "root", "root", 0640) _set_permissions(TMP_FOLDER, "root", "root", 0o640)
# Regen conf for dnsmasq if needed # Regen conf for dnsmasq if needed
_regen_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) domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain)
_generate_key(domain_key_file) _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) _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) CA=certification_authority)
except ValueError as e: except ValueError as e:
if "urn:acme:error:rateLimited" in str(e): if "urn:acme:error:rateLimited" in str(e):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_hit_rate_limit', domain=domain)
'certmanager_hit_rate_limit', domain=domain))
else: else:
logger.error(str(e)) logger.error(str(e))
_display_debug_information(domain) _display_debug_information(domain)
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_cert_signing_failed')
'certmanager_cert_signing_failed'))
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_cert_signing_failed')
'certmanager_cert_signing_failed'))
import requests # lazy loading this module for performance reasons import requests # lazy loading this module for performance reasons
try: try:
intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL, timeout=30).text
except requests.exceptions.Timeout as e: 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 # Now save the key and signed certificate
logger.debug("Saving the key and signed certificate...") logger.debug("Saving the key and signed certificate...")
# Create corresponding directory # 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: if staging:
folder_flag = "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) 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 # Move the private key
domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem") domain_key_file_finaldest = os.path.join(new_cert_folder, "key.pem")
shutil.move(domain_key_file, domain_key_file_finaldest) 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 # Write the cert
domain_cert_file = os.path.join(new_cert_folder, "crt.pem") 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(signed_certificate)
f.write(intermediate_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: if staging:
return return
@ -624,12 +612,11 @@ def _fetch_and_enable_new_certificate(domain, staging=False, no_checks=False):
status_summary = _get_status(domain)["summary"] status_summary = _get_status(domain)["summary"]
if status_summary["code"] != "great": if status_summary["code"] != "great":
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_certificate_fetching_or_enabling_failed', domain=domain)
'certmanager_certificate_fetching_or_enabling_failed', domain=domain))
def _prepare_certificate_signing_request(domain, key_file, output_folder): def _prepare_certificate_signing_request(domain, key_file, output_folder):
from OpenSSL import crypto # lazy loading this module for performance reasons from OpenSSL import crypto # lazy loading this module for performance reasons
# Init a request # Init a request
csr = crypto.X509Req() csr = crypto.X509Req()
@ -658,23 +645,21 @@ def _get_status(domain):
cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem")
if not os.path.isfile(cert_file): if not os.path.isfile(cert_file):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_no_cert_file', domain=domain, file=cert_file)
'certmanager_no_cert_file', domain=domain, file=cert_file))
from OpenSSL import crypto # lazy loading this module for performance reasons from OpenSSL import crypto # lazy loading this module for performance reasons
try: try:
cert = crypto.load_certificate( cert = crypto.load_certificate(
crypto.FILETYPE_PEM, open(cert_file).read()) crypto.FILETYPE_PEM, open(cert_file).read())
except Exception as exception: except Exception as exception:
import traceback import traceback
traceback.print_exc(file=sys.stdout) traceback.print_exc(file=sys.stdout)
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)
'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception))
cert_subject = cert.get_subject().CN cert_subject = cert.get_subject().CN
cert_issuer = cert.get_issuer().CN cert_issuer = cert.get_issuer().CN
valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") 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(): if cert_issuer == _name_self_CA():
CA_type = { CA_type = {
@ -752,19 +737,19 @@ def _get_status(domain):
"ACME_eligible": ACME_eligible "ACME_eligible": ACME_eligible
} }
############################################################################### #
# Misc small stuff ... # # Misc small stuff ... #
############################################################################### #
def _generate_account_key(): def _generate_account_key():
logger.debug("Generating account key ...") logger.debug("Generating account key ...")
_generate_key(ACCOUNT_KEY_FILE) _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): def _generate_key(destination_path):
from OpenSSL import crypto # lazy loading this module for performance reasons from OpenSSL import crypto # lazy loading this module for performance reasons
k = crypto.PKey() k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, KEY_SIZE) k.generate_key(crypto.TYPE_RSA, KEY_SIZE)
@ -810,13 +795,16 @@ def _enable_certificate(domain, new_cert_folder):
_run_service_command("reload", "nginx") _run_service_command("reload", "nginx")
from yunohost.hook import hook_callback
hook_callback('post_cert_update', args=[domain])
def _backup_current_cert(domain): def _backup_current_cert(domain):
logger.debug("Backuping existing certificate for domain %s", domain) logger.debug("Backuping existing certificate for domain %s", domain)
cert_folder_domain = os.path.join(CERT_FOLDER, 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) backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag)
shutil.copytree(cert_folder_domain, backup_folder) shutil.copytree(cert_folder_domain, backup_folder)
@ -827,13 +815,11 @@ def _check_domain_is_ready_for_ACME(domain):
# Check if IP from DNS matches public IP # Check if IP from DNS matches public IP
if not _dns_ip_match_public_ip(public_ip, domain): if not _dns_ip_match_public_ip(public_ip, domain):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)
'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain))
# Check if domain seems to be accessible through HTTP? # Check if domain seems to be accessible through HTTP?
if not _domain_is_accessible_through_HTTP(public_ip, domain): if not _domain_is_accessible_through_HTTP(public_ip, domain):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_domain_http_not_working', domain=domain)
'certmanager_domain_http_not_working', domain=domain))
def _get_dns_ip(domain): def _get_dns_ip(domain):
@ -842,8 +828,7 @@ def _get_dns_ip(domain):
resolver.nameservers = DNS_RESOLVERS resolver.nameservers = DNS_RESOLVERS
answers = resolver.query(domain, "A") answers = resolver.query(domain, "A")
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('certmanager_error_no_A_record', domain=domain)
'certmanager_error_no_A_record', domain=domain))
return str(answers[0]) return str(answers[0])
@ -853,7 +838,7 @@ def _dns_ip_match_public_ip(public_ip, domain):
def _domain_is_accessible_through_HTTP(ip, domain): def _domain_is_accessible_through_HTTP(ip, domain):
import requests # lazy loading this module for performance reasons import requests # lazy loading this module for performance reasons
try: try:
requests.head("http://" + ip, headers={"Host": domain}, timeout=10) requests.head("http://" + ip, headers={"Host": domain}, timeout=10)
except requests.exceptions.Timeout as e: except requests.exceptions.Timeout as e:

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ MIGRATION_COMMENT = "; YunoHost note : this file was automatically moved from {}
class MyMigration(Migration): class MyMigration(Migration):
"Migrate php5-fpm 'pool' conf files to php7 stuff" "Migrate php5-fpm 'pool' conf files to php7 stuff"
def migrate(self): def migrate(self):
@ -58,7 +59,7 @@ class MyMigration(Migration):
_run_service_command("enable", "php7.0-fpm") _run_service_command("enable", "php7.0-fpm")
os.system("systemctl stop php5-fpm") os.system("systemctl stop php5-fpm")
os.system("systemctl disable php5-fpm") os.system("systemctl disable php5-fpm")
os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy os.system("rm /etc/logrotate.d/php5-fpm") # We remove this otherwise the logrotate cron will be unhappy
# Get list of nginx conf file # Get list of nginx conf file
nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf") nginx_conf_files = glob.glob("/etc/nginx/conf.d/*.d/*.conf")

View file

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

View file

@ -5,7 +5,7 @@ import string
import subprocess import subprocess
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.process import run_commands, check_output from moulinette.utils.process import run_commands, check_output
from moulinette.utils.filesystem import append_to_file from moulinette.utils.filesystem import append_to_file
@ -15,7 +15,9 @@ from yunohost.tools import Migration
logger = getActionLogger('yunohost.migration') logger = getActionLogger('yunohost.migration')
SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"] SMALL_PWD_LIST = ["yunohost", "olinuxino", "olinux", "raspberry", "admin", "root", "test", "rpi"]
class MyMigration(Migration): class MyMigration(Migration):
"Synchronize admin and root passwords" "Synchronize admin and root passwords"
def migrate(self): def migrate(self):
@ -23,7 +25,7 @@ class MyMigration(Migration):
new_hash = self._get_admin_hash() new_hash = self._get_admin_hash()
self._replace_root_hash(new_hash) self._replace_root_hash(new_hash)
logger.info(m18n.n("migration_0006_done")) logger.info(m18n.n("root_password_replaced_by_admin_password"))
def backward(self): def backward(self):
pass pass

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,98 @@
import re
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 os
import re import re
import json
import yaml import yaml
import errno
from moulinette import m18n, msettings from moulinette import m18n, msettings
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
import yunohost.certificate import yunohost.certificate
@ -78,7 +77,7 @@ def domain_add(operation_logger, auth, domain, dyndns=False):
try: try:
auth.validate_uniqueness({'virtualdomain': domain}) auth.validate_uniqueness({'virtualdomain': domain})
except MoulinetteError: except MoulinetteError:
raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) raise YunohostError('domain_exists')
operation_logger.start() 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... # Do not allow to subscribe to multiple dyndns domains...
if os.path.exists('/etc/cron.d/yunohost-dyndns'): if os.path.exists('/etc/cron.d/yunohost-dyndns'):
raise MoulinetteError(errno.EPERM, raise YunohostError('domain_dyndns_already_subscribed')
m18n.n('domain_dyndns_already_subscribed'))
from yunohost.dyndns import dyndns_subscribe, _dyndns_provides from yunohost.dyndns import dyndns_subscribe, _dyndns_provides
# Check that this domain can effectively be provided by # Check that this domain can effectively be provided by
# dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st) # dyndns.yunohost.org. (i.e. is it a nohost.me / noho.st)
if not _dyndns_provides("dyndns.yunohost.org", domain): if not _dyndns_provides("dyndns.yunohost.org", domain):
raise MoulinetteError(errno.EINVAL, raise YunohostError('domain_dyndns_root_unknown')
m18n.n('domain_dyndns_root_unknown'))
# Actually subscribe # Actually subscribe
dyndns_subscribe(domain=domain) 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): 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 # Don't regen these conf if we're still in postinstall
if os.path.exists('/etc/yunohost/installed'): if os.path.exists('/etc/yunohost/installed'):
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
app_ssowatconf(auth) app_ssowatconf(auth)
except Exception, e: except Exception:
from sys import exc_info;
t, v, tb = exc_info()
# Force domain removal silently # Force domain removal silently
try: try:
domain_remove(auth, domain, True) domain_remove(auth, domain, True)
except: except:
pass pass
raise t, v, tb raise
hook_callback('post_domain_add', args=[domain]) 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 from yunohost.app import app_ssowatconf
if not force and domain not in domain_list(auth)['domains']: 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 # Check domain is not the main domain
if domain == _get_maindomain(): 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 # Check if apps are installed on the domain
for app in os.listdir('/etc/yunohost/apps/'): for app in os.listdir('/etc/yunohost/apps/'):
@ -162,14 +156,13 @@ def domain_remove(operation_logger, auth, domain, force=False):
continue continue
else: else:
if app_domain == domain: if app_domain == domain:
raise MoulinetteError(errno.EPERM, raise YunohostError('domain_uninstall_app_first')
m18n.n('domain_uninstall_app_first'))
operation_logger.start() operation_logger.start()
if auth.remove('virtualdomain=' + domain + ',ou=domains') or force: if auth.remove('virtualdomain=' + domain + ',ou=domains') or force:
os.system('rm -rf /etc/yunohost/certs/%s' % domain) os.system('rm -rf /etc/yunohost/certs/%s' % domain)
else: else:
raise MoulinetteError(errno.EIO, m18n.n('domain_deletion_failed')) raise YunohostError('domain_deletion_failed')
service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix']) service_regen_conf(names=['nginx', 'metronome', 'dnsmasq', 'postfix'])
app_ssowatconf(auth) app_ssowatconf(auth)
@ -246,7 +239,7 @@ def _get_conflicting_apps(auth, domain, path):
# Abort if domain is unknown # Abort if domain is unknown
if domain not in domain_list(auth)['domains']: 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 # This import cannot be put on top of file because it would create a
# recursive import... # recursive import...
@ -340,7 +333,7 @@ def _build_dns_conf(domain, ttl=3600):
{"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600} {"type": "TXT", "name": "_dmarc", "value": "\"v=DMARC1; p=none\"", "ttl": 3600}
], ],
"extra": [ "extra": [
{"type": "CAA", "name": "@", "value": "128 issue 'letsencrypt.org'", "ttl": 3600}, {"type": "CAA", "name": "@", "value": "128 issue \"letsencrypt.org\"", "ttl": 3600},
], ],
} }
""" """
@ -397,7 +390,7 @@ def _build_dns_conf(domain, ttl=3600):
# Extra # Extra
extra = [ extra = [
["@", ttl, "CAA", "128 issue 'letsencrypt.org'"] ["@", ttl, "CAA", '128 issue "letsencrypt.org"']
] ]
return { return {

View file

@ -27,19 +27,17 @@ import os
import re import re
import json import json
import glob import glob
import time
import base64 import base64
import errno
import subprocess import subprocess
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger 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.network import download_json
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
from yunohost.utils.error import YunohostError
from yunohost.domain import _get_maindomain, _build_dns_conf from yunohost.domain import _get_maindomain, _build_dns_conf
from yunohost.utils.network import get_public_ip from yunohost.utils.network import get_public_ip
from yunohost.log import is_unit_operation from yunohost.log import is_unit_operation
@ -77,9 +75,7 @@ def _dyndns_provides(provider, domain):
dyndomains = download_json('https://%s/domains' % provider, timeout=30) dyndomains = download_json('https://%s/domains' % provider, timeout=30)
except MoulinetteError as e: except MoulinetteError as e:
logger.error(str(e)) logger.error(str(e))
raise MoulinetteError(errno.EIO, raise YunohostError('dyndns_could_not_check_provide', domain=domain, provider=provider)
m18n.n('dyndns_could_not_check_provide',
domain=domain, provider=provider))
# Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me' # Extract 'dyndomain' from 'domain', e.g. 'nohost.me' from 'foo.nohost.me'
dyndomain = '.'.join(domain.split('.')[1:]) dyndomain = '.'.join(domain.split('.')[1:])
@ -106,9 +102,8 @@ def _dyndns_available(provider, domain):
expected_status_code=None) expected_status_code=None)
except MoulinetteError as e: except MoulinetteError as e:
logger.error(str(e)) logger.error(str(e))
raise MoulinetteError(errno.EIO, raise YunohostError('dyndns_could_not_check_available',
m18n.n('dyndns_could_not_check_available', domain=domain, provider=provider)
domain=domain, provider=provider))
return r == u"Domain %s is available" % domain 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 # Verify if domain is provided by subscribe_host
if not _dyndns_provides(subscribe_host, domain): if not _dyndns_provides(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT, raise YunohostError('dyndns_domain_not_provided', domain=domain, provider=subscribe_host)
m18n.n('dyndns_domain_not_provided',
domain=domain, provider=subscribe_host))
# Verify if domain is available # Verify if domain is available
if not _dyndns_available(subscribe_host, domain): if not _dyndns_available(subscribe_host, domain):
raise MoulinetteError(errno.ENOENT, raise YunohostError('dyndns_unavailable', domain=domain)
m18n.n('dyndns_unavailable', domain=domain))
operation_logger.start() operation_logger.start()
@ -156,19 +148,18 @@ def dyndns_subscribe(operation_logger, subscribe_host="dyndns.yunohost.org", dom
with open(key_file) as f: with open(key_file) as f:
key = f.readline().strip().split(' ', 6)[-1] key = f.readline().strip().split(' ', 6)[-1]
import requests # lazy loading this module for performance reasons import requests # lazy loading this module for performance reasons
# Send subscription # Send subscription
try: try:
r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30) r = requests.post('https://%s/key/%s?key_algo=hmac-sha512' % (subscribe_host, base64.b64encode(key)), data={'subdomain': domain}, timeout=30)
except requests.ConnectionError: except requests.ConnectionError:
raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) raise YunohostError('no_internet_connection')
if r.status_code != 201: if r.status_code != 201:
try: try:
error = json.loads(r.text)['error'] error = json.loads(r.text)['error']
except: except:
error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text) error = "Server error, code: %s. (Message: \"%s\")" % (r.status_code, r.text)
raise MoulinetteError(errno.EPERM, raise YunohostError('dyndns_registration_failed', error=error)
m18n.n('dyndns_registration_failed', error=error))
logger.success(m18n.n('dyndns_registered')) logger.success(m18n.n('dyndns_registered'))
@ -202,7 +193,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain)) keys = glob.glob('/etc/yunohost/dyndns/K{0}.+*.private'.format(domain))
if not keys: if not keys:
raise MoulinetteError(errno.EIO, m18n.n('dyndns_key_not_found')) raise YunohostError('dyndns_key_not_found')
key = keys[0] key = keys[0]
@ -219,7 +210,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
exception=e, exception=e,
number=migration.number, number=migration.number,
name=migration.name), name=migration.name),
exc_info=1) exc_info=1)
return return
# Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me' # Extract 'host', e.g. 'nohost.me' from 'foo.nohost.me'
@ -233,7 +224,6 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
'zone %s' % host, 'zone %s' % host,
] ]
old_ipv4 = check_output("dig @%s +short %s" % (dyn_host, domain)).strip() or None 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 old_ipv6 = check_output("dig @%s +short aaaa %s" % (dyn_host, domain)).strip() or None
@ -260,7 +250,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
logger.info("Updated needed, going on...") logger.info("Updated needed, going on...")
dns_conf = _build_dns_conf(domain) dns_conf = _build_dns_conf(domain)
del dns_conf["extra"] # Ignore records from the 'extra' category del dns_conf["extra"] # Ignore records from the 'extra' category
# Delete the old records for all domain/subdomains # Delete the old records for all domain/subdomains
@ -281,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 # should be muc.the.domain.tld. or the.domain.tld
if record["value"] == "@": if record["value"] == "@":
record["value"] = domain 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 = "update add {name}.{domain}. {ttl} {type} {value}".format(domain=domain, **record)
action = action.replace(" @.", " ") action = action.replace(" @.", " ")
@ -302,8 +292,7 @@ def dyndns_update(operation_logger, dyn_host="dyndns.yunohost.org", domain=None,
command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE] command = ["/usr/bin/nsupdate", "-k", key, DYNDNS_ZONE]
subprocess.check_call(command) subprocess.check_call(command)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
raise MoulinetteError(errno.EPERM, raise YunohostError('dyndns_ip_update_failed')
m18n.n('dyndns_ip_update_failed'))
logger.success(m18n.n('dyndns_ip_updated')) logger.success(m18n.n('dyndns_ip_updated'))
@ -329,7 +318,7 @@ def dyndns_removecron():
try: try:
os.remove("/etc/cron.d/yunohost-dyndns") os.remove("/etc/cron.d/yunohost-dyndns")
except: except:
raise MoulinetteError(errno.EIO, m18n.n('dyndns_cron_remove_failed')) raise YunohostError('dyndns_cron_remove_failed')
logger.success(m18n.n('dyndns_cron_removed')) logger.success(m18n.n('dyndns_cron_removed'))
@ -359,5 +348,4 @@ def _guess_current_dyndns_domain(dyn_host):
else: else:
return (_domain, path) return (_domain, path)
raise MoulinetteError(errno.EINVAL, raise YunohostError('dyndns_no_domain_registered')
m18n.n('dyndns_no_domain_registered'))

View file

@ -26,7 +26,6 @@
import os import os
import sys import sys
import yaml import yaml
import errno
try: try:
import miniupnpc import miniupnpc
except ImportError: except ImportError:
@ -34,7 +33,7 @@ except ImportError:
sys.exit(1) sys.exit(1)
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError
from moulinette.utils import process from moulinette.utils import process
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from moulinette.utils.text import prependlines from moulinette.utils.text import prependlines
@ -268,7 +267,7 @@ def firewall_reload(skip_upnp=False):
reloaded = True reloaded = True
if not reloaded: if not reloaded:
raise MoulinetteError(errno.ESRCH, m18n.n('firewall_reload_failed')) raise YunohostError('firewall_reload_failed')
hook_callback('post_iptable_rules', hook_callback('post_iptable_rules',
args=[upnp, os.path.exists("/proc/net/if_inet6")]) args=[upnp, os.path.exists("/proc/net/if_inet6")])
@ -338,7 +337,7 @@ def firewall_upnp(action='status', no_refresh=False):
if action == 'status': if action == 'status':
no_refresh = True no_refresh = True
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('action_invalid', action=action)) raise YunohostError('action_invalid', action=action)
# Refresh port mapping using UPnP # Refresh port mapping using UPnP
if not no_refresh: if not no_refresh:
@ -375,10 +374,10 @@ def firewall_upnp(action='status', no_refresh=False):
try: try:
# Add new port mapping # Add new port mapping
upnpc.addportmapping(port, protocol, upnpc.lanaddr, upnpc.addportmapping(port, protocol, upnpc.lanaddr,
port, 'yunohost firewall: port %d' % port, '') port, 'yunohost firewall: port %d' % port, '')
except: except:
logger.debug('unable to add port %d using UPnP', logger.debug('unable to add port %d using UPnP',
port, exc_info=1) port, exc_info=1)
enabled = False enabled = False
if enabled != firewall['uPnP']['enabled']: if enabled != firewall['uPnP']['enabled']:
@ -407,7 +406,7 @@ def firewall_upnp(action='status', no_refresh=False):
firewall_reload(skip_upnp=True) firewall_reload(skip_upnp=True)
if action == 'enable' and not enabled: 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} return {'enabled': enabled}
@ -419,7 +418,7 @@ def firewall_stop():
""" """
if os.system("iptables -w -P INPUT ACCEPT") != 0: 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 -F")
os.system("iptables -w -X") os.system("iptables -w -X")

View file

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

View file

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

View file

@ -31,14 +31,13 @@ import calendar
import subprocess import subprocess
import xmlrpclib import xmlrpclib
import os.path import os.path
import errno
import os import os
import dns.resolver import dns.resolver
import cPickle as pickle import cPickle as pickle
from datetime import datetime from datetime import datetime
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.utils.network import get_public_ip 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 result_dname = dn
if len(devices) == 0: if len(devices) == 0:
if mountpoint is not None: if mountpoint is not None:
raise MoulinetteError(errno.ENODEV, m18n.n('mountpoint_unknown')) raise YunohostError('mountpoint_unknown')
return result return result
# Retrieve monitoring for unit(s) # Retrieve monitoring for unit(s)
@ -141,7 +140,7 @@ def monitor_disk(units=None, mountpoint=None, human_readable=False):
for dname in devices_names: for dname in devices_names:
_set(dname, 'not-available') _set(dname, 'not-available')
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) raise YunohostError('unit_unknown', unit=u)
if result_dname is not None: if result_dname is not None:
return result[result_dname] return result[result_dname]
@ -237,7 +236,7 @@ def monitor_network(units=None, human_readable=False):
'gateway': gateway, 'gateway': gateway,
} }
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('unit_unknown', unit=u)) raise YunohostError('unit_unknown', unit=u)
if len(units) == 1: if len(units) == 1:
return result[units[0]] return result[units[0]]
@ -287,9 +286,9 @@ def monitor_system(units=None, human_readable=False):
elif u == 'infos': elif u == 'infos':
result[u] = json.loads(glances.getSystem()) result[u] = json.loads(glances.getSystem())
else: 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[units[0]]
return result return result
@ -303,7 +302,7 @@ def monitor_update_stats(period):
""" """
if period not in ['day', 'week', 'month']: 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) stats = _retrieve_stats(period)
if not stats: if not stats:
@ -321,7 +320,7 @@ def monitor_update_stats(period):
else: else:
monitor = _monitor_all(p, 0) monitor = _monitor_all(p, 0)
if not monitor: if not monitor:
raise MoulinetteError(errno.ENODATA, m18n.n('monitor_stats_no_update')) raise YunohostError('monitor_stats_no_update')
stats['timestamp'].append(time.time()) stats['timestamp'].append(time.time())
@ -386,15 +385,13 @@ def monitor_show_stats(period, date=None):
""" """
if period not in ['day', 'week', 'month']: 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) result = _retrieve_stats(period, date)
if result is False: if result is False:
raise MoulinetteError(errno.ENOENT, raise YunohostError('monitor_stats_file_not_found')
m18n.n('monitor_stats_file_not_found'))
elif result is None: elif result is None:
raise MoulinetteError(errno.EINVAL, raise YunohostError('monitor_stats_period_unavailable')
m18n.n('monitor_stats_period_unavailable'))
return result return result
@ -407,7 +404,7 @@ def monitor_enable(with_stats=False):
""" """
from yunohost.service import (service_status, service_enable, from yunohost.service import (service_status, service_enable,
service_start) service_start)
glances = service_status('glances') glances = service_status('glances')
if glances['status'] != 'running': if glances['status'] != 'running':
@ -417,7 +414,7 @@ def monitor_enable(with_stats=False):
# Install crontab # Install crontab
if with_stats: if with_stats:
# day: every 5 min # week: every 1 h # month: every 4 h # # day: every 5 min # week: every 1 h # month: every 4 h #
rules = ('*/5 * * * * root {cmd} day >> /dev/null\n' rules = ('*/5 * * * * root {cmd} day >> /dev/null\n'
'3 * * * * root {cmd} week >> /dev/null\n' '3 * * * * root {cmd} week >> /dev/null\n'
'6 */4 * * * root {cmd} month >> /dev/null').format( '6 */4 * * * root {cmd} month >> /dev/null').format(
@ -434,7 +431,7 @@ def monitor_disable():
""" """
from yunohost.service import (service_status, service_disable, from yunohost.service import (service_status, service_disable,
service_stop) service_stop)
glances = service_status('glances') glances = service_status('glances')
if glances['status'] != 'inactive': if glances['status'] != 'inactive':
@ -442,7 +439,7 @@ def monitor_disable():
if glances['loaded'] != 'disabled': if glances['loaded'] != 'disabled':
try: try:
service_disable('glances') service_disable('glances')
except MoulinetteError as e: except YunohostError as e:
logger.warning(e.strerror) logger.warning(e.strerror)
# Remove crontab # Remove crontab
@ -470,8 +467,8 @@ def _get_glances_api():
from yunohost.service import service_status from yunohost.service import service_status
if service_status('glances')['status'] != 'running': if service_status('glances')['status'] != 'running':
raise MoulinetteError(errno.EPERM, m18n.n('monitor_not_enabled')) raise YunohostError('monitor_not_enabled')
raise MoulinetteError(errno.EIO, m18n.n('monitor_glances_con_failed')) raise YunohostError('monitor_glances_con_failed')
def _extract_inet(string, skip_netmask=False, skip_loopback=True): def _extract_inet(string, skip_netmask=False, skip_loopback=True):

View file

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

View file

@ -1,12 +1,11 @@
import os import os
import json import json
import errno
from datetime import datetime from datetime import datetime
from collections import OrderedDict from collections import OrderedDict
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
logger = getActionLogger('yunohost.settings') logger = getActionLogger('yunohost.settings')
@ -39,6 +38,7 @@ DEFAULTS = OrderedDict([
# -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
("security.password.admin.strength", {"type": "int", "default": 1}), ("security.password.admin.strength", {"type": "int", "default": 1}),
("security.password.user.strength", {"type": "int", "default": 1}), ("security.password.user.strength", {"type": "int", "default": 1}),
("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
]) ])
@ -53,8 +53,7 @@ def settings_get(key, full=False):
settings = _get_settings() settings = _get_settings()
if key not in settings: if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
'global_settings_key_doesnt_exists', settings_key=key))
if full: if full:
return settings[key] return settings[key]
@ -82,39 +81,39 @@ def settings_set(key, value):
settings = _get_settings() settings = _get_settings()
if key not in settings: if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
'global_settings_key_doesnt_exists', settings_key=key))
key_type = settings[key]["type"] key_type = settings[key]["type"]
if key_type == "bool": if key_type == "bool":
if not isinstance(value, bool): if not isinstance(value, bool):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_bad_type_for_setting', setting=key,
'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)
received_type=type(value).__name__, expected_type=key_type))
elif key_type == "int": elif key_type == "int":
if not isinstance(value, int) or isinstance(value, bool): if not isinstance(value, int) or isinstance(value, bool):
if isinstance(value, str): if isinstance(value, str):
value=int(value) 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: else:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_bad_type_for_setting', setting=key,
'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)
received_type=type(value).__name__, expected_type=key_type))
elif key_type == "string": elif key_type == "string":
if not isinstance(value, basestring): if not isinstance(value, basestring):
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_bad_type_for_setting', setting=key,
'global_settings_bad_type_for_setting', setting=key, received_type=type(value).__name__, expected_type=key_type)
received_type=type(value).__name__, expected_type=key_type))
elif key_type == "enum": elif key_type == "enum":
if value not in settings[key]["choices"]: if value not in settings[key]["choices"]:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_bad_choice_for_enum', setting=key,
'global_settings_bad_choice_for_enum', setting=key, received_type=type(value).__name__,
received_type=type(value).__name__, expected_type=", ".join(settings[key]["choices"]))
expected_type=", ".join(settings[key]["choices"])))
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_unknown_type', setting=key,
'global_settings_unknown_type', setting=key, unknown_type=key_type)
unknown_type=key_type))
settings[key]["value"] = value settings[key]["value"] = value
@ -132,8 +131,7 @@ def settings_reset(key):
settings = _get_settings() settings = _get_settings()
if key not in settings: if key not in settings:
raise MoulinetteError(errno.EINVAL, m18n.n( raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)
'global_settings_key_doesnt_exists', settings_key=key))
settings[key]["value"] = settings[key]["default"] settings[key]["value"] = settings[key]["default"]
_save_settings(settings) _save_settings(settings)
@ -154,7 +152,7 @@ def settings_reset_all():
# addition but we'll see if this is a common need. # addition but we'll see if this is a common need.
# Another solution would be to use etckeeper and integrate those # Another solution would be to use etckeeper and integrate those
# modification inside of it and take advantage of its git history # 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) _save_settings(settings, location=old_settings_backup_path)
for value in settings.values(): for value in settings.values():
@ -209,8 +207,7 @@ def _get_settings():
setting_key=key)) setting_key=key))
unknown_settings[key] = value unknown_settings[key] = value
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, m18n.n('global_settings_cant_open_settings', reason=e), raise YunohostError('global_settings_cant_open_settings', reason=e)
exc_info=1)
if unknown_settings: if unknown_settings:
try: try:
@ -231,14 +228,10 @@ def _save_settings(settings, location=SETTINGS_PATH):
try: try:
result = json.dumps(settings_without_description, indent=4) result = json.dumps(settings_without_description, indent=4)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('global_settings_cant_serialize_settings', reason=e)
m18n.n('global_settings_cant_serialize_settings', reason=e),
exc_info=1)
try: try:
with open(location, "w") as settings_fd: with open(location, "w") as settings_fd:
settings_fd.write(result) settings_fd.write(result)
except Exception as e: except Exception as e:
raise MoulinetteError(errno.EIO, raise YunohostError('global_settings_cant_write_settings', reason=e)
m18n.n('global_settings_cant_write_settings', reason=e),
exc_info=1)

View file

@ -2,12 +2,10 @@
import re import re
import os import os
import errno
import pwd import pwd
import subprocess import subprocess
from moulinette import m18n from yunohost.utils.error import YunohostError
from moulinette.core import MoulinetteError
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config" 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 # TODO it would be good to support different kind of shells
if not _get_user_for_ssh(auth, username): 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'}) 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 # TODO it would be good to support different kind of shells
if not _get_user_for_ssh(auth, username): 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'}) 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 # create empty file to set good permissions
write_to_file(authorized_keys_file, "") write_to_file(authorized_keys_file, "")
chown(authorized_keys_file, uid=user["uid"][0]) 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) authorized_keys_content = read_file(authorized_keys_file)

View file

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

View file

@ -5,7 +5,7 @@ import requests_mock
import glob import glob
import time 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 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): def setup_function(function):
# Clear all appslist # Clear all appslist
files = glob.glob(REPO_PATH+"/*") files = glob.glob(REPO_PATH + "/*")
for f in files: for f in files:
os.remove(f) os.remove(f)
@ -42,9 +42,9 @@ def cron_job_is_there():
return r == 0 return r == 0
############################################################################### #
# Test listing of appslists and registering of appslists # # Test listing of appslists and registering of appslists #
############################################################################### #
def test_appslist_list_empty(): 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") _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") _register_new_appslist("https://lol.com/appslist2.json", "dummy")
appslist_dict = app_listlists() appslist_dict = app_listlists()
@ -94,7 +94,7 @@ def test_appslist_list_register_conflict_url():
""" """
_register_new_appslist("https://lol.com/appslist.json", "dummy") _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") _register_new_appslist("https://lol.com/appslist.json", "plopette")
appslist_dict = app_listlists() appslist_dict = app_listlists()
@ -103,9 +103,9 @@ def test_appslist_list_register_conflict_url():
assert "plopette" not in appslist_dict.keys() assert "plopette" not in appslist_dict.keys()
############################################################################### #
# Test fetching of appslists # # Test fetching of appslists #
############################################################################### #
def test_appslist_fetch(): def test_appslist_fetch():
@ -161,7 +161,7 @@ def test_appslist_fetch_unknownlist():
assert app_listlists() == {} assert app_listlists() == {}
with pytest.raises(MoulinetteError): with pytest.raises(YunohostError):
app_fetchlist(name="swag") 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 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) app_fetchlist(url=URL_OFFICIAL_APP_LIST)
@ -244,9 +244,9 @@ def test_appslist_fetch_timeout():
app_fetchlist() app_fetchlist()
############################################################################### #
# Test remove of appslist # # Test remove of appslist #
############################################################################### #
def test_appslist_remove(): def test_appslist_remove():
@ -270,13 +270,13 @@ def test_appslist_remove_unknown():
Attempt to remove an unknown list Attempt to remove an unknown list
""" """
with pytest.raises(MoulinetteError): with pytest.raises(YunohostError):
app_removelist("dummy") app_removelist("dummy")
############################################################################### #
# Test migration from legacy appslist system # # Test migration from legacy appslist system #
############################################################################### #
def add_legacy_cron(name, url): def add_legacy_cron(name, url):

View file

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

View file

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

View file

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

View file

@ -27,8 +27,6 @@ import re
import os import os
import yaml import yaml
import json import json
import errno
import logging
import subprocess import subprocess
import pwd import pwd
import socket import socket
@ -40,7 +38,8 @@ import apt
import apt.progress import apt.progress
from moulinette import msettings, msignals, m18n 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.log import getActionLogger
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_json, write_to_json from moulinette.utils.filesystem import read_json, write_to_json
@ -112,7 +111,7 @@ def tools_ldapinit():
pwd.getpwnam("admin") pwd.getpwnam("admin")
except KeyError: except KeyError:
logger.error(m18n.n('ldap_init_failed_to_create_admin')) 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')) logger.success(m18n.n('ldap_initialized'))
return auth return auth
@ -136,11 +135,10 @@ def tools_adminpw(auth, new_password, check_strength=True):
new_hash = _hash_user_password(new_password) new_hash = _hash_user_password(new_password)
try: try:
auth.update("cn=admin", { "userPassword": new_hash, }) auth.update("cn=admin", {"userPassword": new_hash, })
except: except:
logger.exception('unable to change admin password') logger.exception('unable to change admin password')
raise MoulinetteError(errno.EPERM, raise YunohostError('admin_password_change_failed')
m18n.n('admin_password_change_failed'))
else: else:
# Write as root password # Write as root password
try: try:
@ -152,9 +150,11 @@ def tools_adminpw(auth, new_password, check_strength=True):
with open('/etc/shadow', 'w') as after_file: with open('/etc/shadow', 'w') as after_file:
after_file.write(before.replace("root:" + hash_root, after_file.write(before.replace("root:" + hash_root,
"root:" + new_hash.replace('{CRYPT}', ''))) "root:" + new_hash.replace('{CRYPT}', '')))
except IOError as e: except IOError:
logger.warning(m18n.n('root_password_desynchronized')) logger.warning(m18n.n('root_password_desynchronized'))
return return
logger.info(m18n.n("root_password_replaced_by_admin_password"))
logger.success(m18n.n('admin_password_changed')) logger.success(m18n.n('admin_password_changed'))
@ -174,7 +174,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
# Check domain exists # Check domain exists
if new_domain not in domain_list(auth)['domains']: 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.related_to.append(('domain', new_domain))
operation_logger.start() operation_logger.start()
@ -197,7 +197,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
_set_maindomain(new_domain) _set_maindomain(new_domain)
except Exception as e: except Exception as e:
logger.warning("%s" % e, exc_info=1) 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) _set_hostname(new_domain)
@ -206,7 +206,7 @@ def tools_maindomain(operation_logger, auth, new_domain=None):
# Regen configurations # Regen configurations
try: try:
with open('/etc/yunohost/installed', 'r') as f: with open('/etc/yunohost/installed', 'r'):
service_regen_conf() service_regen_conf()
except IOError: except IOError:
pass pass
@ -246,7 +246,7 @@ def _set_hostname(hostname, pretty_hostname=None):
if p.returncode != 0: if p.returncode != 0:
logger.warning(command) logger.warning(command)
logger.warning(out) logger.warning(out)
raise MoulinetteError(errno.EIO, m18n.n('domain_hostname_failed')) raise YunohostError('domain_hostname_failed')
else: else:
logger.debug(out) logger.debug(out)
@ -264,7 +264,7 @@ def _is_inside_container():
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
out, _ = p.communicate() out, _ = p.communicate()
container = ['lxc','lxd','docker'] container = ['lxc', 'lxd', 'docker']
return out.split()[0] in container return out.split()[0] in container
@ -287,8 +287,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
# Do some checks at first # Do some checks at first
if os.path.isfile('/etc/yunohost/installed'): if os.path.isfile('/etc/yunohost/installed'):
raise MoulinetteError(errno.EPERM, raise YunohostError('yunohost_already_installed')
m18n.n('yunohost_already_installed'))
# Check password # Check password
if not force_password: if not force_password:
@ -317,15 +316,12 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
dyndns = True dyndns = True
# If not, abort the postinstall # If not, abort the postinstall
else: else:
raise MoulinetteError(errno.EEXIST, raise YunohostError('dyndns_unavailable', domain=domain)
m18n.n('dyndns_unavailable',
domain=domain))
else: else:
dyndns = False dyndns = False
else: else:
dyndns = False dyndns = False
operation_logger.start() operation_logger.start()
logger.info(m18n.n('yunohost_installing')) logger.info(m18n.n('yunohost_installing'))
@ -362,8 +358,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('ssowat_persistent_conf_read_error', error=str(e))
m18n.n('ssowat_persistent_conf_read_error', error=str(e)))
except IOError: except IOError:
ssowat_conf = {} ssowat_conf = {}
@ -376,8 +371,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EPERM, raise YunohostError('ssowat_persistent_conf_write_error', error=str(e))
m18n.n('ssowat_persistent_conf_write_error', error=str(e)))
os.system('chmod 644 /etc/ssowat/conf.json.persistent') os.system('chmod 644 /etc/ssowat/conf.json.persistent')
@ -404,8 +398,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
if p.returncode != 0: if p.returncode != 0:
logger.warning(out) logger.warning(out)
raise MoulinetteError(errno.EPERM, raise YunohostError('yunohost_ca_creation_failed')
m18n.n('yunohost_ca_creation_failed'))
else: else:
logger.debug(out) logger.debug(out)
@ -432,7 +425,7 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
_install_appslist_fetch_cron() _install_appslist_fetch_cron()
# Init migrations (skip them, no need to run them on a fresh system) # 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') os.system('touch /etc/yunohost/installed')
@ -441,6 +434,24 @@ def tools_postinstall(operation_logger, domain, password, ignore_dyndns=False,
service_start("yunohost-firewall") service_start("yunohost-firewall")
service_regen_conf(force=True) 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.success(m18n.n('yunohost_configured'))
logger.warning(m18n.n('recommend_to_add_first_user')) logger.warning(m18n.n('recommend_to_add_first_user'))
@ -463,7 +474,7 @@ def tools_update(ignore_apps=False, ignore_packages=False):
# Update APT cache # Update APT cache
logger.debug(m18n.n('updating_apt_cache')) logger.debug(m18n.n('updating_apt_cache'))
if not cache.update(): if not cache.update():
raise MoulinetteError(errno.EPERM, m18n.n('update_cache_failed')) raise YunohostError('update_cache_failed')
cache.open(None) cache.open(None)
cache.upgrade(True) cache.upgrade(True)
@ -482,7 +493,7 @@ def tools_update(ignore_apps=False, ignore_packages=False):
if not ignore_apps: if not ignore_apps:
try: try:
app_fetchlist() app_fetchlist()
except MoulinetteError: except YunohostError:
# FIXME : silent exception !? # FIXME : silent exception !?
pass pass
@ -526,7 +537,7 @@ def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=Fal
# If API call # If API call
if is_api: if is_api:
critical_packages = ("moulinette", "yunohost", critical_packages = ("moulinette", "yunohost",
"yunohost-admin", "ssowat", "python") "yunohost-admin", "ssowat", "python")
critical_upgrades = set() critical_upgrades = set()
for pkg in cache.get_changes(): for pkg in cache.get_changes():
@ -562,7 +573,6 @@ def tools_upgrade(operation_logger, auth, ignore_apps=False, ignore_packages=Fal
else: else:
logger.info(m18n.n('packages_no_upgrade')) logger.info(m18n.n('packages_no_upgrade'))
if not ignore_apps: if not ignore_apps:
try: try:
app_upgrade(auth) app_upgrade(auth)
@ -613,7 +623,7 @@ def tools_diagnosis(auth, private=False):
diagnosis['system'] = OrderedDict() diagnosis['system'] = OrderedDict()
try: try:
disks = monitor_disk(units=['filesystem'], human_readable=True) 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) logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
else: else:
diagnosis['system']['disks'] = {} diagnosis['system']['disks'] = {}
@ -629,7 +639,7 @@ def tools_diagnosis(auth, private=False):
try: try:
system = monitor_system(units=['cpu', 'memory'], human_readable=True) 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) logger.warning(m18n.n('diagnosis_monitor_system_error', error=format(e)), exc_info=1)
else: else:
diagnosis['system']['memory'] = { diagnosis['system']['memory'] = {
@ -655,7 +665,7 @@ def tools_diagnosis(auth, private=False):
# YNH Applications # YNH Applications
try: try:
applications = app_list()['apps'] applications = app_list()['apps']
except MoulinetteError as e: except YunohostError as e:
diagnosis['applications'] = m18n.n('diagnosis_no_apps') diagnosis['applications'] = m18n.n('diagnosis_no_apps')
else: else:
diagnosis['applications'] = {} diagnosis['applications'] = {}
@ -706,7 +716,7 @@ def _check_if_vulnerable_to_meltdown():
try: try:
call = subprocess.Popen("bash %s --batch json --variant 3" % call = subprocess.Popen("bash %s --batch json --variant 3" %
SCRIPT_PATH, shell=True, SCRIPT_PATH, shell=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
output, _ = call.communicate() output, _ = call.communicate()
@ -787,7 +797,7 @@ def tools_migrations_list(pending=False, done=False):
# Check for option conflict # Check for option conflict
if pending and done: 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 # Get all migrations
migrations = _get_migrations_list() migrations = _get_migrations_list()
@ -802,12 +812,12 @@ def tools_migrations_list(pending=False, done=False):
migrations = [m for m in migrations if m.number > last_migration] migrations = [m for m in migrations if m.number > last_migration]
# Reduce to dictionnaries # Reduce to dictionnaries
migrations = [{ "id": migration.id, migrations = [{"id": migration.id,
"number": migration.number, "number": migration.number,
"name": migration.name, "name": migration.name,
"mode": migration.mode, "mode": migration.mode,
"description": migration.description, "description": migration.description,
"disclaimer": migration.disclaimer } for migration in migrations ] "disclaimer": migration.disclaimer} for migration in migrations]
return {"migrations": migrations} return {"migrations": migrations}
@ -844,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 # validate input, target must be "0" or a valid number
elif target != 0 and target not in all_migration_numbers: 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)) logger.debug(m18n.n('migrations_current_target', target))
@ -872,38 +882,41 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
else: # can't happen, this case is handle before else: # can't happen, this case is handle before
raise Exception() 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 # effectively run selected migrations
for migration in 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 # Start register change on system
operation_logger= OperationLogger('tools_migrations_migrate_' + mode) operation_logger = OperationLogger('tools_migrations_migrate_' + mode)
operation_logger.start() operation_logger.start()
if not skip: 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)) number=migration.number, name=migration.name))
try: try:
@ -918,12 +931,15 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
# migration failed, let's stop here but still update state because # migration failed, let's stop here but still update state because
# we managed to run the previous ones # we managed to run the previous ones
msg = m18n.n('migrations_migration_has_failed', msg = m18n.n('migrations_migration_has_failed',
exception=e, exception=e,
number=migration.number, number=migration.number,
name=migration.name) name=migration.name)
logger.error(msg, exc_info=1) logger.error(msg, exc_info=1)
operation_logger.error(msg) operation_logger.error(msg)
break break
else:
logger.success(m18n.n('migrations_success',
number=migration.number, name=migration.name))
else: # if skip else: # if skip
logger.warn(m18n.n('migrations_skip_migration', logger.warn(m18n.n('migrations_skip_migration',
@ -938,6 +954,10 @@ def tools_migrations_migrate(target=None, skip=False, auto=False, accept_disclai
operation_logger.success() operation_logger.success()
# Skip migrations one at a time
if skip:
break
# special case where we want to go back from the start # special case where we want to go back from the start
if target == 0: if target == 0:
state["last_run_migration"] = None state["last_run_migration"] = None
@ -987,7 +1007,7 @@ def _get_migrations_list():
migrations = [] migrations = []
try: try:
import data_migrations from . import data_migrations
except ImportError: except ImportError:
# not data migrations present, return empty list # not data migrations present, return empty list
return migrations return migrations
@ -1010,7 +1030,7 @@ def _get_migration_by_name(migration_name):
""" """
try: try:
import data_migrations from . import data_migrations
except ImportError: except ImportError:
raise AssertionError("Unable to find migration with name %s" % migration_name) raise AssertionError("Unable to find migration with name %s" % migration_name)
@ -1029,7 +1049,7 @@ def _load_migration(migration_file):
number, name = migration_id.split("_", 1) number, name = migration_id.split("_", 1)
logger.debug(m18n.n('migrations_loading_migration', logger.debug(m18n.n('migrations_loading_migration',
number=number, name=name)) number=number, name=name))
try: try:
# this is python builtin method to import a module using a name, we # this is python builtin method to import a module using a name, we
@ -1041,8 +1061,28 @@ def _load_migration(migration_file):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise MoulinetteError(errno.EINVAL, m18n.n('migrations_error_failed_to_load_migration', raise YunohostError('migrations_error_failed_to_load_migration',
number=number, name=name)) 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): class Migration(object):
@ -1074,4 +1114,3 @@ class Migration(object):
@property @property
def description(self): def description(self):
return m18n.n("migration_description_%s" % self.id) return m18n.n("migration_description_%s" % self.id)

View file

@ -27,20 +27,20 @@ import os
import re import re
import pwd import pwd
import json import json
import errno
import crypt import crypt
import random import random
import string import string
import subprocess import subprocess
from moulinette import m18n from moulinette import m18n
from moulinette.core import MoulinetteError from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
from yunohost.service import service_status from yunohost.service import service_status
from yunohost.log import is_unit_operation from yunohost.log import is_unit_operation
logger = getActionLogger('yunohost.user') logger = getActionLogger('yunohost.user')
def user_list(auth, fields=None): def user_list(auth, fields=None):
""" """
List users List users
@ -71,8 +71,7 @@ def user_list(auth, fields=None):
if attr in keys: if attr in keys:
attrs.append(attr) attrs.append(attr)
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('field_invalid', attr)
m18n.n('field_invalid', attr))
else: else:
attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell'] attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell']
@ -100,7 +99,7 @@ def user_list(auth, fields=None):
@is_unit_operation([('username', 'user')]) @is_unit_operation([('username', 'user')])
def user_create(operation_logger, auth, username, firstname, lastname, mail, password, def user_create(operation_logger, auth, username, firstname, lastname, mail, password,
mailbox_quota="0"): mailbox_quota="0"):
""" """
Create user Create user
@ -130,7 +129,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
# Validate uniqueness of username in system users # Validate uniqueness of username in system users
all_existing_usernames = {x.pw_name for x in pwd.getpwall()} all_existing_usernames = {x.pw_name for x in pwd.getpwall()}
if username in all_existing_usernames: if username in all_existing_usernames:
raise MoulinetteError(errno.EEXIST, m18n.n('system_username_exists')) raise YunohostError('system_username_exists')
main_domain = _get_maindomain() main_domain = _get_maindomain()
aliases = [ aliases = [
@ -141,13 +140,11 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
] ]
if mail in aliases: if mail in aliases:
raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) raise YunohostError('mail_unavailable')
# Check that the mail domain exists # Check that the mail domain exists
if mail.split("@")[1] not in domain_list(auth)['domains']: if mail.split("@")[1] not in domain_list(auth)['domains']:
raise MoulinetteError(errno.EINVAL, raise YunohostError('mail_domain_unknown', domain=mail.split("@")[1])
m18n.n('mail_domain_unknown',
domain=mail.split("@")[1]))
operation_logger.start() operation_logger.start()
@ -188,8 +185,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
with open('/etc/ssowat/conf.json.persistent') as json_conf: with open('/etc/ssowat/conf.json.persistent') as json_conf:
ssowat_conf = json.loads(str(json_conf.read())) ssowat_conf = json.loads(str(json_conf.read()))
except ValueError as e: except ValueError as e:
raise MoulinetteError(errno.EINVAL, raise YunohostError('ssowat_persistent_conf_read_error', error=e.strerror)
m18n.n('ssowat_persistent_conf_read_error', error=e.strerror))
except IOError: except IOError:
ssowat_conf = {} ssowat_conf = {}
@ -199,8 +195,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
with open('/etc/ssowat/conf.json.persistent', 'w+') as f: with open('/etc/ssowat/conf.json.persistent', 'w+') as f:
json.dump(ssowat_conf, f, sort_keys=True, indent=4) json.dump(ssowat_conf, f, sort_keys=True, indent=4)
except IOError as e: except IOError as e:
raise MoulinetteError(errno.EPERM, raise YunohostError('ssowat_persistent_conf_write_error', error=e.strerror)
m18n.n('ssowat_persistent_conf_write_error', error=e.strerror))
if auth.add('uid=%s,ou=users' % username, attr_dict): if auth.add('uid=%s,ou=users' % username, attr_dict):
# Invalidate passwd to take user creation into account # Invalidate passwd to take user creation into account
@ -226,7 +221,7 @@ def user_create(operation_logger, auth, username, firstname, lastname, mail, pas
return {'fullname': fullname, 'username': username, 'mail': mail} 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')]) @is_unit_operation([('username', 'user')])
@ -258,7 +253,7 @@ def user_delete(operation_logger, auth, username, purge=False):
subprocess.call(['rm', '-rf', '/home/{0}'.format(username)]) subprocess.call(['rm', '-rf', '/home/{0}'.format(username)])
subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)]) subprocess.call(['rm', '-rf', '/var/mail/{0}'.format(username)])
else: else:
raise MoulinetteError(169, m18n.n('user_deletion_failed')) raise YunohostError('user_deletion_failed')
app_ssowatconf(auth) app_ssowatconf(auth)
@ -269,8 +264,8 @@ def user_delete(operation_logger, auth, username, purge=False):
@is_unit_operation([('username', 'user')], exclude=['auth', 'change_password']) @is_unit_operation([('username', 'user')], exclude=['auth', 'change_password'])
def user_update(operation_logger, auth, username, firstname=None, lastname=None, mail=None, def user_update(operation_logger, auth, username, firstname=None, lastname=None, mail=None,
change_password=None, add_mailforward=None, remove_mailforward=None, change_password=None, add_mailforward=None, remove_mailforward=None,
add_mailalias=None, remove_mailalias=None, mailbox_quota=None): add_mailalias=None, remove_mailalias=None, mailbox_quota=None):
""" """
Update user informations Update user informations
@ -286,7 +281,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
remove_mailalias -- Mail aliases to remove 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.app import app_ssowatconf
from yunohost.utils.password import assert_password_is_strong_enough from yunohost.utils.password import assert_password_is_strong_enough
@ -297,7 +292,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
# Populate user informations # Populate user informations
result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch) result = auth.search(base='ou=users,dc=yunohost,dc=org', filter='uid=' + username, attrs=attrs_to_fetch)
if not result: if not result:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) raise YunohostError('user_unknown', user=username)
user = result[0] user = result[0]
# Get modifications from arguments # Get modifications from arguments
@ -328,11 +323,9 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
] ]
auth.validate_uniqueness({'mail': mail}) auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@') + 1:] not in domains: if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL, raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
m18n.n('mail_domain_unknown',
domain=mail[mail.find('@') + 1:]))
if mail in aliases: if mail in aliases:
raise MoulinetteError(errno.EEXIST,m18n.n('mail_unavailable')) raise YunohostError('mail_unavailable')
del user['mail'][0] del user['mail'][0]
new_attr_dict['mail'] = [mail] + user['mail'] new_attr_dict['mail'] = [mail] + user['mail']
@ -343,9 +336,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
for mail in add_mailalias: for mail in add_mailalias:
auth.validate_uniqueness({'mail': mail}) auth.validate_uniqueness({'mail': mail})
if mail[mail.find('@') + 1:] not in domains: if mail[mail.find('@') + 1:] not in domains:
raise MoulinetteError(errno.EINVAL, raise YunohostError('mail_domain_unknown', domain=mail[mail.find('@') + 1:])
m18n.n('mail_domain_unknown',
domain=mail[mail.find('@') + 1:]))
user['mail'].append(mail) user['mail'].append(mail)
new_attr_dict['mail'] = user['mail'] new_attr_dict['mail'] = user['mail']
@ -356,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:]: if len(user['mail']) > 1 and mail in user['mail'][1:]:
user['mail'].remove(mail) user['mail'].remove(mail)
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('mail_alias_remove_failed', mail=mail)
m18n.n('mail_alias_remove_failed', mail=mail))
new_attr_dict['mail'] = user['mail'] new_attr_dict['mail'] = user['mail']
if add_mailforward: if add_mailforward:
@ -376,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:]: if len(user['maildrop']) > 1 and mail in user['maildrop'][1:]:
user['maildrop'].remove(mail) user['maildrop'].remove(mail)
else: else:
raise MoulinetteError(errno.EINVAL, raise YunohostError('mail_forward_remove_failed', mail=mail)
m18n.n('mail_forward_remove_failed', mail=mail))
new_attr_dict['maildrop'] = user['maildrop'] new_attr_dict['maildrop'] = user['maildrop']
if mailbox_quota is not None: if mailbox_quota is not None:
@ -390,7 +379,7 @@ def user_update(operation_logger, auth, username, firstname=None, lastname=None,
app_ssowatconf(auth) app_ssowatconf(auth)
return user_info(auth, username) return user_info(auth, username)
else: else:
raise MoulinetteError(169, m18n.n('user_update_failed')) raise YunohostError('user_update_failed')
def user_info(auth, username): def user_info(auth, username):
@ -415,7 +404,7 @@ def user_info(auth, username):
if result: if result:
user = result[0] user = result[0]
else: else:
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username)) raise YunohostError('user_unknown', user=username)
result_dict = { result_dict = {
'username': user['uid'][0], 'username': user['uid'][0],
@ -471,7 +460,7 @@ def user_info(auth, username):
if result: if result:
return result_dict return result_dict
else: else:
raise MoulinetteError(167, m18n.n('user_info_failed')) raise YunohostError('user_info_failed')
# #
# SSH subcategory # SSH subcategory
@ -479,18 +468,23 @@ def user_info(auth, username):
# #
import yunohost.ssh import yunohost.ssh
def user_ssh_allow(auth, username): def user_ssh_allow(auth, username):
return yunohost.ssh.user_ssh_allow(auth, username) return yunohost.ssh.user_ssh_allow(auth, username)
def user_ssh_disallow(auth, username): def user_ssh_disallow(auth, username):
return yunohost.ssh.user_ssh_disallow(auth, username) return yunohost.ssh.user_ssh_disallow(auth, username)
def user_ssh_list_keys(auth, username): def user_ssh_list_keys(auth, username):
return yunohost.ssh.user_ssh_list_keys(auth, username) return yunohost.ssh.user_ssh_list_keys(auth, username)
def user_ssh_add_key(auth, username, key, comment): def user_ssh_add_key(auth, username, key, comment):
return yunohost.ssh.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): def user_ssh_remove_key(auth, username, key):
return yunohost.ssh.user_ssh_remove_key(auth, username, key) return yunohost.ssh.user_ssh_remove_key(auth, username, key)
@ -498,6 +492,7 @@ def user_ssh_remove_key(auth, username, key):
# End SSH subcategory # End SSH subcategory
# #
def _convertSize(num, suffix=''): def _convertSize(num, suffix=''):
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']: for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0: if abs(num) < 1024.0:

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 import os
def free_space_in_directory(dirpath): def free_space_in_directory(dirpath):
stat = os.statvfs(dirpath) stat = os.statvfs(dirpath)
return stat.f_frsize * stat.f_bavail return stat.f_frsize * stat.f_bavail
def space_used_by_directory(dirpath): def space_used_by_directory(dirpath):
stat = os.statvfs(dirpath) stat = os.statvfs(dirpath)
return stat.f_frsize * stat.f_blocks 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 return addr.popitem()[1] if len(addr) == 1 else None
############################################################################### #
def _extract_inet(string, skip_netmask=False, skip_loopback=True): def _extract_inet(string, skip_netmask=False, skip_loopback=True):

View file

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

View file

@ -38,9 +38,11 @@ STRENGTH_LEVELS = [
(12, 1, 1, 1, 1), (12, 1, 1, 1, 1),
] ]
def assert_password_is_strong_enough(profile, password): def assert_password_is_strong_enough(profile, password):
PasswordValidator(profile).validate(password) PasswordValidator(profile).validate(password)
class PasswordValidator(object): class PasswordValidator(object):
def __init__(self, profile): def __init__(self, profile):
@ -81,17 +83,15 @@ class PasswordValidator(object):
# on top (at least not the moulinette ones) # on top (at least not the moulinette ones)
# because the moulinette needs to be correctly initialized # because the moulinette needs to be correctly initialized
# as well as modules available in python's path. # as well as modules available in python's path.
import errno
import logging import logging
from moulinette import m18n from yunohost.utils.error import YunohostError
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
logger = logging.getLogger('yunohost.utils.password') logger = logging.getLogger('yunohost.utils.password')
status, msg = self.validation_summary(password) status, msg = self.validation_summary(password)
if status == "error": if status == "error":
raise MoulinetteError(1, m18n.n(msg)) raise YunohostError(msg)
def validation_summary(self, password): def validation_summary(self, password):
""" """
@ -159,7 +159,7 @@ class PasswordValidator(object):
# and the strength of the password (e.g. [11, 2, 7, 2, 0]) # and the strength of the password (e.g. [11, 2, 7, 2, 0])
# and compare the values 1-by-1. # and compare the values 1-by-1.
# If one False is found, the password does not satisfy the level # If one False is found, the password does not satisfy the level
if False in [s>=c for s, c in zip(strength, level_criterias)]: if False in [s >= c for s, c in zip(strength, level_criterias)]:
break break
# Otherwise, the strength of the password is at least of the current level. # Otherwise, the strength of the password is at least of the current level.
strength_level = level + 1 strength_level = level + 1
@ -188,7 +188,7 @@ if __name__ == '__main__':
if len(sys.argv) < 2: if len(sys.argv) < 2:
import getpass import getpass
pwd = getpass.getpass("") pwd = getpass.getpass("")
#print("usage: password.py PASSWORD") # print("usage: password.py PASSWORD")
else: else:
pwd = sys.argv[1] pwd = sys.argv[1]
status, msg = PasswordValidator('user').validation_summary(pwd) status, msg = PasswordValidator('user').validation_summary(pwd)

View file

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